From 1f430c22a2f8785ca33b231215584af3fa5fc2fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:39:30 +0000 Subject: [PATCH 01/14] chore(deps): bump strum from 0.27.2 to 0.28.0 Bumps [strum](https://github.com/Peternator7/strum) from 0.27.2 to 0.28.0. - [Release notes](https://github.com/Peternator7/strum/releases) - [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md) - [Commits](https://github.com/Peternator7/strum/compare/v0.27.2...v0.28.0) --- updated-dependencies: - dependency-name: strum dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 79 ++++++++++++++++++++++++++++++++++-------------------- Cargo.toml | 2 +- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfc0874cf1fd3..65a696a8be26d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ dependencies = [ "alloy-rlp", "num_enum", "serde", - "strum", + "strum 0.27.2", ] [[package]] @@ -717,7 +717,7 @@ dependencies = [ "jsonwebtoken", "rand 0.8.5", "serde", - "strum", + "strum 0.27.2", ] [[package]] @@ -1194,7 +1194,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1218,7 +1218,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3021,7 +3021,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3174,7 +3174,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3958,7 +3958,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4263,7 +4263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4630,7 +4630,7 @@ dependencies = [ "solar-compiler", "soldeer-commands", "soldeer-core", - "strum", + "strum 0.28.0", "svm-rs", "tempfile", "tempo-alloy", @@ -4964,7 +4964,7 @@ dependencies = [ "serde_json", "solar-compiler", "strsim", - "strum", + "strum 0.28.0", "tempfile", "tempo-primitives", "tikv-jemallocator", @@ -6588,7 +6588,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7514,7 +7514,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7735,7 +7735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b685c8311c9171d1bd2895222965d25616b2de2cb5819dd3504ed9250df9fecd" dependencies = [ "ahash", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "parking_lot", "stable_deref_trait", ] @@ -8813,7 +8813,7 @@ dependencies = [ "itertools 0.14.0", "kasuari", "lru", - "strum", + "strum 0.27.2", "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", @@ -8845,7 +8845,7 @@ dependencies = [ "itertools 0.14.0", "line-clipping", "ratatui-core", - "strum", + "strum 0.27.2", "time", "unicode-segmentation", "unicode-width 0.2.2", @@ -9398,7 +9398,7 @@ dependencies = [ "modular-bitfield", "reth-codecs 0.3.0", "serde", - "strum", + "strum 0.27.2", "thiserror 2.0.18", "tracing", ] @@ -9489,7 +9489,7 @@ dependencies = [ "fixed-map", "reth-stages-types", "serde", - "strum", + "strum 0.27.2", "tracing", ] @@ -10048,7 +10048,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10107,7 +10107,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10810,7 +10810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10826,7 +10826,7 @@ dependencies = [ "solar-data-structures", "solar-interface", "solar-macros", - "strum", + "strum 0.27.2", ] [[package]] @@ -10850,7 +10850,7 @@ version = "0.1.8" source = "git+https://github.com/paradigmxyz/solar?rev=530f129#530f129b1b2d7138df973dd71d2fc1e592b593d7" dependencies = [ "colorchoice", - "strum", + "strum 0.27.2", ] [[package]] @@ -10947,7 +10947,7 @@ dependencies = [ "solar-interface", "solar-macros", "solar-parse", - "strum", + "strum 0.27.2", "thread_local", "tracing", ] @@ -11104,7 +11104,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -11119,6 +11128,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -11337,7 +11358,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11542,7 +11563,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11552,7 +11573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12769,7 +12790,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12919,7 +12940,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 33ba7f64fab10..7cc434c28e278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -483,7 +483,7 @@ serde_json = { version = "1.0", features = ["arbitrary_precision"] } similar-asserts = "1.7" soldeer-commands = "=0.10.0" soldeer-core = { version = "=0.10.1", features = ["serde"] } -strum = "0.27" +strum = "0.28" tempfile = "3.23" tokio = "1" toml = "0.9" From fb01017d511bbe31f8b7a746a752d406c8efa48d Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:31:34 +0700 Subject: [PATCH 02/14] Update crates/script/src/simulate.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/script/src/simulate.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 4ee9c3fb53cb6..43b529a8c3e98 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -363,10 +363,9 @@ impl FilledTransactionsState { // Get the native token symbol for the chain using NamedChain let token_symbol = NamedChain::try_from(provider_info.chain) - .unwrap_or_default() + let token_symbol = alloy_chains::Chain::from_id(provider_info.chain) .native_currency_symbol() .unwrap_or("ETH"); - // We don't store it in the transactions, since we want the most updated value. // Right before broadcasting. let per_gas = if let Some(gas_price) = self.args.with_gas_price { From 5dbd7b227f598a9f5b3678fc6b9bab46089e03ab Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:04:37 +0700 Subject: [PATCH 03/14] Update crates/anvil/server/src/handler.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/anvil/server/src/handler.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/anvil/server/src/handler.rs b/crates/anvil/server/src/handler.rs index 250c486986240..95659d9eefbf6 100644 --- a/crates/anvil/server/src/handler.rs +++ b/crates/anvil/server/src/handler.rs @@ -49,7 +49,9 @@ pub async fn handle_request( Request::Single(call) => handle_call(call, handler).await.map(Response::Single), Request::Batch(calls) => { if calls.is_empty() { - return Some(Response::error(RpcError::invalid_request())); + return Some(Response::Batch(vec![anvil_rpc::response::RpcResponse::from( + RpcError::invalid_request(), + )])); } future::join_all(calls.into_iter().map(move |call| handle_call(call, handler.clone()))) .map(responses_as_batch) From 5e16eb3fc44328c4a0b5b8ff3e0dec0e37d81d7c Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:22:07 +0700 Subject: [PATCH 04/14] fix(forge): adjust gas assertion CounterWithFallback (foundry-rs#14465 ) (#498) * chore(deps): bump rui314/setup-mold from 725a8794d15fc7563f59595bd9556495c0564878 to 9c9c13bf4c3f1adef0cc596abc155580bcb04444 (#14442) chore(deps): bump rui314/setup-mold Bumps [rui314/setup-mold](https://github.com/rui314/setup-mold) from 725a8794d15fc7563f59595bd9556495c0564878 to 9c9c13bf4c3f1adef0cc596abc155580bcb04444. - [Commits](https://github.com/rui314/setup-mold/compare/725a8794d15fc7563f59595bd9556495c0564878...9c9c13bf4c3f1adef0cc596abc155580bcb04444) --- updated-dependencies: - dependency-name: rui314/setup-mold dependency-version: 9c9c13bf4c3f1adef0cc596abc155580bcb04444 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update flake.lock (#14458) Co-authored-by: github-actions[bot] * fix(forge): adjust gas assertion `CounterWithFallback` (#14465) * chore: update latest benchmarks (#14467) * ci: split MPP e2e into its own workflow (#14468) * ci: split MPP e2e into its own workflow Move the MPP e2e step from ci-tempo.yml into a standalone ci-mpp.yml workflow so transient HTTP 402 failures from the MPP RPC do not block the Tempo CI workflow. Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp * ci: rename sanity-check job to tempo-check Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp * ci: rename mpp-e2e job to mpp-check Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp --------- Co-authored-by: Amp * Improve GH actions (#14473) * fix(benches): add repos + extra args support to prevent blocking errors (#14470) * fix(benches): add repos + extra args support to prevent blocking errors * fix(ci): set `inputs.repos` default to empty * fix: remove `--verbose` flags * fix: exclude `uniswap/v4-core` `TickMathTestTest` --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> --- .github/workflows/benchmarks.yml | 50 ++++++++++++--- .github/workflows/ci-mpp.yml | 55 ++++++++++++++++ .github/workflows/ci-tempo.yml | 17 +---- .github/workflows/ci.yml | 6 +- .github/workflows/crate-checks.yml | 2 +- .github/workflows/docker-publish.yml | 1 + .github/workflows/docs.yml | 5 +- .github/workflows/release.yml | 7 +- .github/workflows/test-flaky.yml | 2 +- .github/workflows/test-isolate.yml | 2 +- .github/workflows/test.yml | 2 +- benches/LATEST.md | 79 ++++++++++++----------- benches/src/lib.rs | 96 +++++++++++++++++----------- benches/src/main.rs | 12 +++- crates/forge/tests/cli/cmd.rs | 2 +- flake.lock | 18 +++--- 16 files changed, 226 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/ci-mpp.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 8465874854551..9b7f834c420c7 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -15,15 +15,44 @@ on: type: string default: "stable,nightly" repos: - description: "Comma-separated list of repos to benchmark (e.g., ithacaxyz/account:main,Vectorized/solady)" + description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev][ ] (e.g. vectorized/solady:v0.1.26 --nmc BrokenTest). Leave empty to use the per-benchmark default repo lists." required: false type: string - default: "ithacaxyz/account:v0.3.2,Vectorized/solady:v0.1.22" + default: "" env: - ITHACAXYZ_ACCOUNT: "ithacaxyz/account:v0.3.2" - VECTORIZED_SOLADY: "Vectorized/solady:v0.1.22" - DEFAULT_REPOS: "ithacaxyz/account:v0.3.2,Vectorized/solady:v0.1.22" + # Repos to benchmark per step. Each comma-separated entry has the form + # org/repo[:rev][ ] + # where anything after the first whitespace is appended to every benchmark + # command for that repo (use this to skip a broken test contract via e.g. + # `--nmc BrokenTest`, so a single failing test does not fail the whole CI). + TEST_REPOS: >- + ithacaxyz/account:v0.5.7, + vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test', + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', + sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting + + ISOLATE_TEST_REPOS: >- + ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest, + vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test', + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', + sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting + + BUILD_REPOS: >- + ithacaxyz/account:v0.5.7, + vectorized/solady:v0.1.26, + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, + sparkdotfi/spark-psm:v1.0.0 + + COVERAGE_REPOS: >- + ithacaxyz/account:v0.5.7, + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, + sparkdotfi/spark-psm:v1.0.0 + RUSTC_WRAPPER: "sccache" jobs: @@ -48,7 +77,7 @@ jobs: with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -78,7 +107,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.DEFAULT_REPOS }} + REPOS: ${{ github.event.inputs.repos || env.TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -90,7 +119,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.VECTORIZED_SOLADY }} + REPOS: ${{ github.event.inputs.repos || env.ISOLATE_TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -102,7 +131,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.DEFAULT_REPOS }} + REPOS: ${{ github.event.inputs.repos || env.BUILD_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -114,10 +143,11 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + REPOS: ${{ github.event.inputs.repos || env.COVERAGE_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ - --repos ${{ env.ITHACAXYZ_ACCOUNT }} \ + --repos "$REPOS" \ --benchmarks forge_coverage \ --output-file forge_coverage_bench.md diff --git a/.github/workflows/ci-mpp.yml b/.github/workflows/ci-mpp.yml new file mode 100644 index 0000000000000..00ec35f8f8aba --- /dev/null +++ b/.github/workflows/ci-mpp.yml @@ -0,0 +1,55 @@ +name: CI MPP + +permissions: {} + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTC_WRAPPER: "sccache" + +jobs: + mpp-check: + runs-on: depot-ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + # Checkout the repository + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + + # Build and install binaries + - name: Build and install Foundry binaries + run: | + cargo build --profile dev --locked -p forge -p cast -p anvil -p chisel + echo "${{ github.workspace }}/target/debug" >> "$GITHUB_PATH" + + - name: Run MPP e2e test + env: + TEMPO_KEYS_TOML_B64: ${{ secrets.TEMPO_KEYS_TOML_B64 }} + MPP_API_KEY: ${{ secrets.MPP_API_KEY }} + MPP_DEPOSIT: "1000000" + run: | + if [ -z "${TEMPO_KEYS_TOML_B64:-}" ]; then + echo "::warning::TEMPO_KEYS_TOML_B64 secret not set, skipping MPP e2e" + exit 0 + fi + mkdir -p ~/.tempo/wallet + echo "$TEMPO_KEYS_TOML_B64" | tr -d '[:space:]' | base64 -d > ~/.tempo/wallet/keys.toml + ./.github/scripts/tempo-mpp.sh "$(which cast | xargs dirname)" diff --git a/.github/workflows/ci-tempo.yml b/.github/workflows/ci-tempo.yml index 942f595a34977..b4bc98391e72d 100644 --- a/.github/workflows/ci-tempo.yml +++ b/.github/workflows/ci-tempo.yml @@ -36,7 +36,7 @@ env: RUSTC_WRAPPER: "sccache" jobs: - sanity-check: + tempo-check: runs-on: depot-ubuntu-latest timeout-minutes: 60 permissions: @@ -59,21 +59,6 @@ jobs: cargo build --profile dev --locked -p forge -p cast -p anvil -p chisel echo "${{ github.workspace }}/target/debug" >> "$GITHUB_PATH" - - name: Run MPP e2e test - if: github.event_name == 'push' || github.event_name == 'pull_request' - env: - TEMPO_KEYS_TOML_B64: ${{ secrets.TEMPO_KEYS_TOML_B64 }} - MPP_API_KEY: ${{ secrets.MPP_API_KEY }} - MPP_DEPOSIT: "1000000" - run: | - if [ -z "${TEMPO_KEYS_TOML_B64:-}" ]; then - echo "::warning::TEMPO_KEYS_TOML_B64 secret not set, skipping MPP e2e" - exit 0 - fi - mkdir -p ~/.tempo/wallet - echo "$TEMPO_KEYS_TOML_B64" | tr -d '[:space:]' | base64 -d > ~/.tempo/wallet/keys.toml - ./.github/scripts/tempo-mpp.sh "$(which cast | xargs dirname)" - # TODO(upstream): re-enable when flaky devnet faucet is fixed # - name: Run Tempo check on devnet # if: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d5564f94a7e2..9eb90a76cdbfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test --workspace --doc --locked @@ -89,7 +89,7 @@ jobs: with: toolchain: nightly components: clippy - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo clippy --workspace --all-targets --all-features --locked @@ -123,7 +123,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: forge fmt diff --git a/.github/workflows/crate-checks.yml b/.github/workflows/crate-checks.yml index ce2988f3e0260..eb865bddc10e3 100644 --- a/.github/workflows/crate-checks.yml +++ b/.github/workflows/crate-checks.yml @@ -28,7 +28,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: cargo-hack diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 3372e816328b7..5b7cf1631fea8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -108,3 +108,4 @@ jobs: labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 push: true + no-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 288d9d127592f..45d00708394f5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,6 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: full - RUSTC_WRAPPER: "sccache" jobs: docs: @@ -30,9 +29,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: nightly - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Build documentation run: cargo doc --workspace --all-features --no-deps --document-private-items --locked env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 351d8a334362d..9cb1cf51dda24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,12 +148,7 @@ jobs: with: toolchain: stable targets: ${{ matrix.target }} - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - if: ${{ contains(matrix.runner, 'depot') }} - - run: printf 'RUSTC_WRAPPER=sccache\n' >> "$GITHUB_ENV" - if: ${{ contains(matrix.runner, 'depot') }} + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Apple M1 setup if: matrix.target == 'aarch64-apple-darwin' diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index 7e3fa21d64b9a..d6244f826887e 100644 --- a/.github/workflows/test-flaky.yml +++ b/.github/workflows/test-flaky.yml @@ -32,7 +32,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index 4446cdbba8ce6..141e3a049a73b 100644 --- a/.github/workflows/test-isolate.yml +++ b/.github/workflows/test-isolate.yml @@ -36,7 +36,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 80a32068a21e3..eaa46c8e79f7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,7 +72,7 @@ jobs: with: toolchain: stable target: ${{ matrix.target }} - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest diff --git a/benches/LATEST.md b/benches/LATEST.md index 7ea1049a2ac41..238a691229389 100644 --- a/benches/LATEST.md +++ b/benches/LATEST.md @@ -1,6 +1,6 @@ # Foundry Benchmark Results -**Date**: 2025-10-02 12:14:23 +**Date**: 2026-04-24 23:10:24 ## Repositories Tested @@ -8,66 +8,67 @@ 2. [Vectorized/solady](https://github.com/Vectorized/solady) 3. [Uniswap/v4-core](https://github.com/Uniswap/v4-core) 4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +5. [aave/aave-v4](https://github.com/aave/aave-v4) ## Foundry Versions -- **v1.3.6**: forge Version: 1.3.6-v1.3.6 (d241588 2025-09-16) -- **v1.4.0-rc1**: forge Version: 1.4.0-v1.4.0-rc1 (bd0e4a7 2025-10-01) +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **nightly**: forge Version: 1.6.0-nightly (a249f5c 2026-04-24) ## Forge Test -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 3.17 s | 2.94 s | -| solady | 2.28 s | 2.10 s | -| Uniswap-v4-core | 7.27 s | 6.13 s | -| sparkdotfi-spark-psm | 43.04 s | 44.08 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| vectorized-solady | 1.46 s | 1.38 s | +| aave-aave-v4 | 4m 14.2s | 3m 29.1s | ## Forge Fuzz Test -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------ | ---------- | -| ithacaxyz-account | 3.18 s | 3.02 s | -| solady | 2.39 s | 2.24 s | -| Uniswap-v4-core | 6.84 s | 6.20 s | -| sparkdotfi-spark-psm | 3.07 s | 2.72 s | +| Repository | v1.5.1 | nightly | +| -------------------- | --------- | -------- | +| ithacaxyz-account | 2.81 s | 1.59 s | +| vectorized-solady | 1.40 s | 1.34 s | +| Uniswap-v4-core | 3.01 s | 2.87 s | +| sparkdotfi-spark-psm | 2.04 s | 1.87 s | +| aave-aave-v4 | 3m 46.0s | 3m 17.3s | ## Forge Test (Isolated) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| solady | 2.26 s | 2.41 s | -| Uniswap-v4-core | 7.22 s | 7.71 s | -| sparkdotfi-spark-psm | 45.53 s | 50.49 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| Uniswap-v4-core | 3.50 s | 3.48 s. | +| aave-aave-v4 | 4m 14.0s | 3m 53.4s | ## Forge Build (No Cache) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 9.16 s | 9.08 s | -| solady | 14.62 s | 14.69 s | -| Uniswap-v4-core | 2m 3.8s | 2m 5.3s | -| sparkdotfi-spark-psm | 13.17 s | 13.14 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| ithacaxyz-account | 26.06 s | 26.61 s | +| vectorized-solady | 14.20 s | 14.26 s | +| Uniswap-v4-core | 2m 1.3s | 2m 5.0s | +| sparkdotfi-spark-psm | 15.16 s | 15.30 s | +| aave-aave-v4 | 3m 37.0s | 3m 35.1s | ## Forge Build (With Cache) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 0.156 s | 0.113 s | -| solady | 0.089 s | 0.094 s | -| Uniswap-v4-core | 0.133 s | 0.127 s | -| sparkdotfi-spark-psm | 0.173 s | 0.131 s | +| Repository | v1.5.1 | nightly | +| -------------------- | ------- | ------- | +| ithacaxyz-account | 0.167 s | 0.201 s | +| vectorized-solady | 0.099 s | 0.098 s | +| Uniswap-v4-core | 0.139 s | 0.140 s | +| sparkdotfi-spark-psm | 0.168 s | 0.173 s | +| aave-aave-v4 | 0.370 s | 0.357 s | ## Forge Coverage -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | -------- | ---------- | -| ithacaxyz-account | 14.91 s | 13.34 s | -| Uniswap-v4-core | 1m 34.8s | 1m 30.3s | -| sparkdotfi-spark-psm | 3m 49.3s | 3m 40.2s | +| Repository | v1.5.1 | nightly | +| -------------------- | --------- | ---------- | +| Uniswap-v4-core | 1m 13.9s | 1m 10.3s | +| sparkdotfi-spark-psm | 2m 54.7s | 2m 50.0s | +| aave-aave-v4 | 11m 20.8s | 10m 58.7s | ## System Information - **OS**: macos -- **CPU**: 8 -- **Rustc**: rustc 1.90.0-nightly (3014e79f9 2025-07-15) +- **CPU**: 12 +- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) \ No newline at end of file diff --git a/benches/src/lib.rs b/benches/src/lib.rs index ab3bec614e3cd..7ed8807cbf0f5 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -24,46 +24,53 @@ pub struct RepoConfig { pub org: String, pub repo: String, pub rev: String, + /// Optional extra arguments appended to every benchmark command for this + /// repo (e.g. `--nmc BrokenTest` to skip a broken test contract). + pub extra_args: Option, } impl FromStr for RepoConfig { type Err = eyre::Error; + /// Parse a repo spec of the form `org/repo[:rev][ ]`. + /// + /// Anything after the first whitespace is treated as extra arguments + /// appended to every benchmark command for this repo. fn from_str(spec: &str) -> Result { - // Split by ':' first to separate repo path from optional rev - let parts: Vec<&str> = spec.splitn(2, ':').collect(); - let repo_path = parts[0]; - let custom_rev = parts.get(1).copied(); - - // Now split the repo path by '/' - let path_parts: Vec<&str> = repo_path.split('/').collect(); - if path_parts.len() != 2 { - eyre::bail!("Invalid repo format '{}'. Expected 'org/repo' or 'org/repo:rev'", spec); - } - - let org = path_parts[0]; - let repo = path_parts[1]; + let spec = spec.trim(); + // Anything after the first whitespace is per-repo extra args. + let (head, extra_args) = match spec.split_once(char::is_whitespace) { + Some((head, rest)) => (head, Some(rest.trim().to_string())), + None => (spec, None), + }; - // Try to find this repo in BENCHMARK_REPOS to get the full config - let existing_config = BENCHMARK_REPOS.iter().find(|r| r.org == org && r.repo == repo); + let (repo_path, custom_rev) = match head.split_once(':') { + Some((path, rev)) => (path, Some(rev)), + None => (head, None), + }; - let config = if let Some(existing) = existing_config { - // Use existing config but allow custom rev to override - let mut config = existing.clone(); - if let Some(rev) = custom_rev { - config.rev = rev.to_string(); - } - config - } else { - // Create new config with custom rev or default - // Name should follow the format: org-repo (with hyphen) - Self { + let (org, repo) = repo_path.split_once('/').ok_or_else(|| { + eyre::eyre!("Invalid repo format '{spec}'. Expected 'org/repo' or 'org/repo:rev'") + })?; + + // Inherit defaults from BENCHMARK_REPOS when available, otherwise build + // a fresh config. Custom rev / extra args always override. + let mut config = BENCHMARK_REPOS + .iter() + .find(|r| r.org == org && r.repo == repo) + .cloned() + .unwrap_or_else(|| Self { name: format!("{org}-{repo}"), org: org.to_string(), repo: repo.to_string(), - rev: custom_rev.unwrap_or("main").to_string(), - } - }; + rev: "main".to_string(), + extra_args: None, + }); + + if let Some(rev) = custom_rev { + config.rev = rev.to_string(); + } + config.extra_args = extra_args; let _ = sh_println!("Parsed repo spec '{spec}' -> {config:?}"); Ok(config) @@ -78,12 +85,14 @@ pub fn default_benchmark_repos() -> Vec { org: "ithacaxyz".to_string(), repo: "account".to_string(), rev: "main".to_string(), + extra_args: None, }, RepoConfig { name: "solady".to_string(), org: "Vectorized".to_string(), repo: "solady".to_string(), rev: "main".to_string(), + extra_args: None, }, ] } @@ -113,6 +122,8 @@ pub struct BenchmarkProject { pub name: String, pub temp_project: TempProject, pub root_path: PathBuf, + /// Optional extra arguments appended to every benchmark command. + pub extra_args: Option, } impl BenchmarkProject { @@ -169,7 +180,20 @@ impl BenchmarkProject { Self::install_npm_dependencies(&root_path)?; sh_println!(" ✅ Project {} setup complete at {}", config.name, root); - Ok(Self { name: config.name.clone(), root_path, temp_project }) + Ok(Self { + name: config.name.clone(), + root_path, + temp_project, + extra_args: config.extra_args.clone(), + }) + } + + /// Append `self.extra_args` to a benchmark shell command, if any. + fn cmd(&self, base: &str) -> String { + match self.extra_args.as_deref().map(str::trim).filter(|s| !s.is_empty()) { + Some(extra) => format!("{base} {extra}"), + None => base.to_string(), + } } /// Install npm dependencies if package.json exists @@ -296,7 +320,7 @@ impl BenchmarkProject { self.hyperfine( "forge_test", version, - "forge test", + &self.cmd("forge test"), runs, Some("forge build"), None, @@ -315,7 +339,7 @@ impl BenchmarkProject { self.hyperfine( "forge_build_with_cache", version, - "FOUNDRY_LINT_LINT_ON_BUILD=false forge build", + &self.cmd("FOUNDRY_LINT_LINT_ON_BUILD=false forge build"), runs, None, Some("forge build"), @@ -335,7 +359,7 @@ impl BenchmarkProject { self.hyperfine( "forge_build_no_cache", version, - "FOUNDRY_LINT_LINT_ON_BUILD=false forge build", + &self.cmd("FOUNDRY_LINT_LINT_ON_BUILD=false forge build"), runs, Some("forge clean"), None, @@ -355,7 +379,7 @@ impl BenchmarkProject { self.hyperfine( "forge_fuzz_test", version, - r#"forge test --match-test "test[^(]*\([^)]+\)""#, + &self.cmd(r#"forge test --match-test "test[^(]*\([^)]+\)""#), runs, Some("forge build"), None, @@ -376,7 +400,7 @@ impl BenchmarkProject { self.hyperfine( "forge_coverage", version, - "forge coverage --ir-minimum", + &self.cmd("forge coverage --ir-minimum"), runs, None, None, @@ -396,7 +420,7 @@ impl BenchmarkProject { self.hyperfine( "forge_isolate_test", version, - "forge test --isolate", + &self.cmd("forge test --isolate"), runs, Some("forge build"), None, diff --git a/benches/src/main.rs b/benches/src/main.rs index f3361bf30b6e6..60e815cecb0ec 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -48,8 +48,16 @@ struct Cli { #[clap(long, value_delimiter = ',')] benchmarks: Option>, - /// Run only on specific repositories (comma-separated in org/repo[:rev] format: - /// ithacaxyz/account,Vectorized/solady:main,foundry-rs/foundry:v1.0.0) + /// Comma-separated list of repositories to benchmark. + /// + /// Each entry has the form `org/repo[:rev][ ]`. Anything + /// after the first whitespace is appended to every benchmark command for + /// that repo (handy to skip a broken test contract via e.g. + /// `--nmc BrokenTest`). + /// + /// Examples: + /// `ithacaxyz/account:v0.5.7` + /// `vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest'` #[clap(long, value_delimiter = ',')] repos: Option>, } diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 13bf5cfdbacce..d3f1503faf569 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3016,7 +3016,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/FallbackWithCalldataTest.sol:CounterWithFallback", "deployment": { - "gas": 132471, + "gas": 132459, "size": 396 }, "functions": { diff --git a/flake.lock b/flake.lock index 4fa81efa24f11..27f426f491da6 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1776497206, - "narHash": "sha256-Em+RSdFnwyyKPGUBFtQYtVjm+1UvIc9gOR91Y22zlzg=", + "lastModified": 1777102577, + "narHash": "sha256-ycoy9svZOQgyInu/lwO7IEQtlP5liqYhEcF9m9hPRbM=", "owner": "nix-community", "repo": "fenix", - "rev": "df2295365fb081fe0745449762a771290782c22d", + "rev": "f37403486c59376cd285f9685a8ef8ff25c09a3c", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776329215, - "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", + "lastModified": 1776949667, + "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b86751bc4085f48661017fa226dee99fab6c651b", + "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1776441750, - "narHash": "sha256-1rVfG+mj8R4ze+lSYCa4iAv7FzrB03Cprtxmd1MfZak=", + "lastModified": 1776800521, + "narHash": "sha256-f8YJfwAOsLFpIoqZuX3yF69UvMLrkx7iVzMH1pJU7cM=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "251df518d73abb5c5d573c4d5d266a3edae9ca5a", + "rev": "8954b66d43225e62c92e8bbcc8500191b5cceb1e", "type": "github" }, "original": { From 6c44631ab8e2dfa35ccf2b7a26f1eb70a038eeaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 15:47:16 +0700 Subject: [PATCH 05/14] chore(deps): bump strum from 0.27.2 to 0.28.0 (#509) Bumps [strum](https://github.com/Peternator7/strum) from 0.27.2 to 0.28.0. - [Release notes](https://github.com/Peternator7/strum/releases) - [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md) - [Commits](https://github.com/Peternator7/strum/compare/v0.27.2...v0.28.0) --- updated-dependencies: - dependency-name: strum dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From b8e3a0c2d38faa684c0a8b9cce4dd5504eb28149 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Sat, 2 May 2026 15:56:21 +0700 Subject: [PATCH 06/14] gas-snapshot --- counter/.gas-snapshot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/counter/.gas-snapshot b/counter/.gas-snapshot index ef525c09384e6..797ceebb2f595 100644 --- a/counter/.gas-snapshot +++ b/counter/.gas-snapshot @@ -1,2 +1,2 @@ -CounterTest:testFuzz_SetNumber(uint256) (runs: 256, μ: 30177, ~: 32354) +CounterTest:testFuzz_SetNumber(uint256) (runs: 256, μ: 30410, ~: 32354) CounterTest:test_Increment() (gas: 31851) \ No newline at end of file From 432d718616712d3586c067e773eeef120e222dd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 16:25:58 +0700 Subject: [PATCH 07/14] chore(deps): bump similar-asserts from 1.7.0 to 2.0.0 (#508) Bumps [similar-asserts](https://github.com/mitsuhiko/similar-asserts) from 1.7.0 to 2.0.0. - [Changelog](https://github.com/mitsuhiko/similar-asserts/blob/main/CHANGELOG.md) - [Commits](https://github.com/mitsuhiko/similar-asserts/compare/1.7.0...2.0.0) --- updated-dependencies: - dependency-name: similar-asserts dependency-version: 2.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 +++++++++++++------- Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65a696a8be26d..7b52c5f580fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4625,7 +4625,7 @@ dependencies = [ "semver 1.0.28", "serde", "serde_json", - "similar", + "similar 2.7.0", "similar-asserts", "solar-compiler", "soldeer-commands", @@ -4678,7 +4678,7 @@ dependencies = [ "foundry-config", "foundry-test-utils", "itertools 0.14.0", - "similar", + "similar 2.7.0", "snapbox", "solar-compiler", "toml", @@ -10708,6 +10708,12 @@ name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "similar" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d93e861ede2e497b47833469b8ec9d5c07fa4c78ce7a00f6eb7dd8168b4b3f" dependencies = [ "bstr", "unicode-segmentation", @@ -10715,12 +10721,12 @@ dependencies = [ [[package]] name = "similar-asserts" -version = "1.7.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" +checksum = "997e6ca38e97437973fc9f7f50a50d1274cacd874341a4960fea90067291038c" dependencies = [ - "console 0.15.11", - "similar", + "console 0.16.3", + "similar 3.1.0", ] [[package]] @@ -10790,7 +10796,7 @@ dependencies = [ "regex", "serde", "serde_json", - "similar", + "similar 2.7.0", "snapbox-macros", ] diff --git a/Cargo.toml b/Cargo.toml index c93dc44c47884..469d5bea793e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -481,7 +481,7 @@ rustls = "0.23" semver = "1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["arbitrary_precision"] } -similar-asserts = "1.7" +similar-asserts = "2.0" soldeer-commands = "=0.10.0" soldeer-core = { version = "=0.10.1", features = ["serde"] } strum = "0.28" From 5a4d2893e23eb711512ca89ccef6e978bd7d5a6b Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 01:00:41 +0700 Subject: [PATCH 08/14] ci: sign release archives, docker images, and publish SBOMs (#520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * anvil: unify Tempo nonce markers across send RPCs (#14536) Co-authored-by: Amp Co-authored-by: steven Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Matthias Seitz * fix(forge): `flaky_gas_report_fallback_with_calldata` deployment cost (#14545) * chore(lint): add missing lints to README (#14551) * chore(bench): update `benchmark.sh` (#14548) Co-authored-by: Matthias Seitz * chore(clippy): fix for_kv_map and useless_borrows_in_formatting (#14554) * chore(clippy): fix for_kv_map and useless_borrows_in_formatting Amp-Thread-ID: https://ampcode.com/threads/T-019df0f9-62e7-74b8-bd5e-da2acce678fb Co-authored-by: Amp * chore(clippy): drop redundant borrows in cheatcodes assert formatters Amp-Thread-ID: https://ampcode.com/threads/T-019df0f9-62e7-74b8-bd5e-da2acce678fb Co-authored-by: Amp --------- Co-authored-by: Amp * fix(ci): use `PATH_USD` fallback fee token in Mail templates (#14546) * chore(deps): bump the actions-weekly group with 3 updates (#14497) * refactor(chisel): migrate to solar (#14532) * feat(lint): add too-many-digits lint (#14549) * feat: feature-gate optimism deps in common-fmt, common, cast (#14539) * feat(forge): support per-test network selection via inline config (#14530) * feat(cli): `--tempo.expires` retry-safe mode (TIP-1009 expiring nonces) (#14521) * fix(forge): `per_test_network_routing` match undeterministic order (#14557) output * chore(ci): run tempo mainnet and testnet checks before devnet (#14556) * Update flake.lock (#14553) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/f374034' (2026-04-25) → 'github:nix-community/fenix/74c1591' (2026-05-02) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/8954b66' (2026-04-21) → 'github:rust-lang/rust-analyzer/64cdaeb' (2026-05-01) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/01fbdee' (2026-04-23) → 'github:NixOS/nixpkgs/c6d6588' (2026-05-01) Co-authored-by: github-actions[bot] * chore(bench): update benchmark results (#14552) * fix(forge): ignore ETH_RPC_URL for test forking (#14555) Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(cast): add Tempo keychain policy commands (#14531) * feat(cast): add tempo keychain policy commands * fix(cast): address keychain policy review * fix(cli): fix jsonwebtoken panic (#14562) `cast` panicked with this message coming from jsonwebtoken: ``` Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'rust_crypto' and 'aws_lc_rs' features is enabled. See the documentation of the CryptoProvider type for more information. ``` This seemingly was introduced with the bump of jsonwebtoken to 10. Now it requires you to pick one backend used by default controlled by the compile time cargo features or call `CryptoProvider::install_default()` at the beginning. I realized that probably it would be better to just select the feature and I picked `aws_lc_rs` as it seems to be increasingly a default and we already are using the C toolchain. Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore(cli): tidy ETH_RPC_URL handling and add forge regression test (#14559) Follow-up to #14555: - Drop the redundant flashbots branch in RpcOpts::dict; self.url(None) already returns FLASHBOTS_URL when --flashbots is set, so the subsequent overwrite was dead code. - Inline the resolve_rpc_url helper back into RpcCommonOpts::url; it was only called from one place and added unneeded surface area. - Restore the doc comment on RpcCommonOpts and document why ETH_RPC_URL is intentionally not a clap env on the shared field (so EvmArgs cannot inherit it). - Add an integration test that runs forge config with ETH_RPC_URL set in the environment and asserts that eth_rpc_url stays None, directly exercising the regression scenario from #14538. Amp-Thread-ID: https://ampcode.com/threads/T-019df243-267f-7779-93e1-5d6686082444 Co-authored-by: zerosnacks Co-authored-by: Amp * feat(cast): open Tempo wallet fund flow for MPP failures (#14505) * feat(cast): open Tempo wallet fund flow for MPP failures * ci(tempo): skip network checks without rpc secrets * Revert "ci(tempo): skip network checks without rpc secrets" This reverts commit f8dd70163f850b854888fd1c962174e1663284f4. * fix(common): address mpp funding review --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * ci: sign release archives, docker images, and publish SBOMs (#14563) - release.yml: emit per-archive sha256 + SPDX SBOM (Syft), cosign keyless sign-blob of the archive, and use actions/attest@v4 for both build provenance and SBOM attestations. Upload all artifacts to the draft release. - docker-publish.yml: enable BuildKit SBOM, capture the build digest, cosign keyless sign each pushed tag, and publish a Sigstore-signed SLSA provenance attestation via actions/attest with push-to-registry. - SECURITY.md: document how external users verify archives and the docker image (gh attestation, cosign, plain sha256, buildx imagetools). - README.md: link to the new verification section. * perf(common): short-circuit `find_by_name_or_identifier` instead of `collect` (#14514) * feat(foundryup): retry GitHub API fetches on transient errors (#14566) GitHub api.github.com occasionally returns transient 403s on certain VMs (per-IP rate limiting / WAF hiccups), causing foundryup to fail to resolve the latest stable / nightly release tag, e.g.: foundryup: fetching latest nightly releases from foundry-rs/foundry... Error: curl: (56) The requested URL returned error: 403 foundryup: failed to fetch releases from GitHub API Add curl/wget retry logic to the `fetch` helper (used exclusively for GitHub API releases endpoints): - curl: --retry 5 --retry-delay 2 --retry-max-time 60, plus --retry-all-errors when supported (curl 7.71+, feature-detected so older curl does not hard-fail). --retry-all-errors is required to retry HTTP 403, which is not in curl's default retryable set. - wget fallback: --tries=5 --waitretry=2 --retry-on-http-error=403,408,429,5xx. `fetch` now buffers to a temp file before emitting to stdout, since curl's --retry-all-errors is unsafe with piped consumers (mid-stream retries can duplicate bytes). Existing callers pipe into awk/grep. Tunable via FOUNDRYUP_MAX_RETRIES (default 5). `download` (binary tarballs, attestations, manpages) is intentionally left unchanged — those rarely fail and changing them affects the attestation existence check semantics. Bumps installer version 1.8.1 -> 1.8.2. Amp-Thread-ID: https://ampcode.com/threads/T-019df2f5-9b97-717a-b959-cf7cbc7ca3bb Co-authored-by: Amp * feat(lint): project-wide passes + pragma-inconsistent (#14543) * feat(lint): project-wide passes + pragma-inconsistent * rm hashset, msg * test(lint): exhaustive pragma-inconsistent coverage + clearer testdata names (#14561) * test(lint): exhaustive coverage for pragma-inconsistent Follow-up to #14543 expanding test coverage for the cross-file `pragma-inconsistent` lint across the syntax variants users encounter in real Solidity projects. Multi-file scenarios (added as `forgetest!` cases in `crates/forge/tests/cli/lint.rs`, since they cannot be expressed in a single `.sol` testdata file): - Negative (must NOT warn): - all files use the same exact pragma (`0.8.20`) - all files use the same caret pragma (`^0.8.20`) - single file in the project - Positive (must warn): - duplicates among a conflict -- two identical files plus one different pragma still emits three warnings - Mixed: - file without an explicit pragma uses the test-utils default (`add_raw_source` is used to bypass the auto-injected pragma) Source bodies are pulled out into module-level `const` raw strings so rustfmt does not collapse the inline `\n`-escaped strings into wide horizontal blobs. Single-file scenarios (added as `.sol` files under `crates/lint/testdata/` in the existing `//~NOTE:` annotation style): - `PragmaInconsistentCaretVsTilde.sol`: `^0.8.20` vs `~0.8.20` - `PragmaInconsistentRangeVsExact.sol`: `>=0.8.0 <0.9.0` vs `0.8.20` -- range satisfies exact but lint is intentionally string-based, matching SLITHER-W1078 - `PragmaInconsistentOrVsExact.sol`: `0.8.20 || 0.8.21` vs `0.8.20` - `PragmaInconsistentThreeDistinct.sol`: `>=0.8.0`, `^0.8.0`, `~0.8.0` -- verifies the `others` list contains every other variant * test(lint): rename pragma-inconsistent testdata to describe the case under test The two testdata files added in #14543 were named `PragmaInconsistent.sol` and `PragmaInconsistent2.sol`, which made them look like duplicates. They actually exercise distinct edge cases of the same string-based detection: - `PragmaInconsistentCaretAboveExact.sol` (was `PragmaInconsistent.sol`): caret range whose lower bound is strictly below the exact version (`^0.8.0` + `0.8.18`). - `PragmaInconsistentCaretMatchesExact.sol` (was `PragmaInconsistent2.sol`): caret range whose lower bound equals the exact version (`^0.8.20` + `0.8.20`) -- the looks-the-same-but-still-distinct case that guards SLITHER-W1078 parity (no semver intersection). Amp-Thread-ID: https://ampcode.com/threads/T-019df243-267f-7779-93e1-5d6686082444 Co-authored-by: Amp --------- Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * refactor(script): reuse shared Tempo CLI opts (#14558) * deps: bump tempo to 6bf9903 (T6 hardfork) + fix alloy-evm 0.34 compat (#14567) * deps: bump tempo to 6bf9903 (T6 hardfork) Bumps tempo crates to 6bf9903d, adding the T6 hardfork variant to TempoHardfork. Without this, cast's tempo_forkSchedule lookup parses the chain's reported active fork ("T6") into TempoHardfork::FromStr, fails because T6 was unknown to the enum, and silently returns is_hardfork_active(T3) = false. That made 'cast keychain auth' fall back to the legacy authorizeKey selector and revert with LegacyAuthorizeKeySelectorChanged on any T6 chain. Also bumps alloy-evm to 0.34 and the optimism git pin to develop (e3b59e7) so alloy-op-evm picks up an EvmFactory impl built against alloy-evm 0.34. Removes the now-unused paradigmxyz/reth-core [patch] entries. No source changes; lockfile churn is transitive only. * fix: adapt AnvilBlockExecutor to alloy-evm 0.34.0 breaking changes - Add Send + 'static bounds to TxResult impl for AnvilTxResult - Change commit_transaction return type from Result to GasOutput - Remove .expect() on commit_transaction call site Amp-Thread-ID: https://ampcode.com/threads/T-019df322-c0f1-73e7-858c-5ca2d242ddb4 * style: rustfmt commit_transaction signature Amp-Thread-ID: https://ampcode.com/threads/T-019df322-c0f1-73e7-858c-5ca2d242ddb4 --------- Co-authored-by: Centaur AI * docs: add forge lint rule docs (#14571) * feat(forge): add fuzz run selection (#14522) * feat(forge): add fuzz run selection * fix(fuzz): make metadata builder const * test(fuzz): cover generated seed replay * fix(forge): persist fuzz worker for run replay * fix(evm): satisfy clippy in fuzz replay * fix(fuzz): reuse fuzz run metadata * forge(lint/docs): validate deployed forge lint docs (#14573) test: validate deployed forge lint docs * feat: gate foundry-primitives behind optimism feature (#14572) * fix(ci): increase permissions for the enhanced attestation writing (#14584) * increase permissions for artifact writing * apply write permissions to release-docker * feat(hardforks, networks): gate optimism behind cargo feature (#14581) * fix(forge): encode Tempo creates as AA calls (#14585) * feat(anvil): gate optimism behind cargo feature (#14577) Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(cast): introduce `vaddr` cmd for TIP-1022 (#14508) * feat(cast): introduce `vaddr` cmd for tip-1022 * fix: doc * chore: touch-ups * add tests * chore: move tests to tempo ci * feat: add vaddr watch test * feat: count 0 hadling, add `no_register` flag * fix: remove sweep subcommand * fix: make clippy happy * feat(bench): nightly regression tracking workflow (#14586) * fix(cli): fix release version strings for immutable tags, bump to 1.7.1 (#14496) * Fix release version metadata for immutable tags Amp-Thread-ID: https://ampcode.com/threads/T-019dd617-b29f-7409-8523-9858a1504f17 Co-authored-by: Amp * Derive nightly release suffix from commit SHA Amp-Thread-ID: https://ampcode.com/threads/T-019dd617-b29f-7409-8523-9858a1504f17 Co-authored-by: Amp * Apply suggestion from @zerosnacks * Apply suggestion from @zerosnacks * Apply suggestion from @zerosnacks * bump to v1.7.1 * avoid appending whole sha hash, not necessary, handle version cmp correctly. after v1.7.1 release we need to bump to v1.7.2 for nightlies following it to compare correctly * Make foundryVersionCmp tolerate new version format and add tests - Strip both pre-release ('-nightly', '-dev') and build metadata ('+..') from SEMVER_VERSION before comparison so the cheatcode keeps working for tagged releases (which have no '-' separator). - Extract strip_semver_metadata helper and add Rust unit tests covering all SEMVER_VERSION shapes, version_cmp ordering, and parse_version rejection of pre-release/build/garbage input. - Extend the Solidity test suite for vm.getFoundryVersion()/foundryVersionCmp/foundryVersionAtLeast: validate MAJOR.MINOR.PATCH parseability, build profile value, cmp/atLeast invariant, and error paths for invalid user-supplied versions. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * fix(test): drop view from solidity tests using assert helpers and fix fmt - assertTrue/assertEq aren't view, so testGetFoundryVersionBuildProfile and testFoundryVersionCmpAndAtLeastAreConsistent can't be view either. - Collapse the buildType assertion onto one line to satisfy forge fmt. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * test(version): assert build profile is non-empty instead of debug|release The dist profile (used for distributed release binaries) is also valid; just require non-empty so any future profile works. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * Normalize nightly- to nightly in release_version Ensures tarball and Docker nightly artifacts produce the same version string. The commit identifier is already included in the SemVer build metadata (after `+`), so collapsing `nightly-` to `nightly` avoids duplicating the SHA in the pre-release tag. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019df79e-d4c9-707c-85eb-2efbf59160b3 --------- Co-authored-by: Centaur AI Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks * fix(evm): query `state_snapshot.storage` in `ForkDbStateSnapshot::storage_ref` (#14007) * fix(evm): query `state_snapshot.storage` in `ForkDbStateSnapshot::storage_ref` * test(evm): cover `ForkDbStateSnapshot::storage_ref` snapshot lookup * fix(cast): consistent `--json` output for `keychain` subcommands (#14590) - `keychain rl`: wrap remaining limit in `{"remaining":"..."}` object instead of emitting a bare JSON string - `keychain policy add-call`: emit `{"status":"already_present","target":"..."}` when the rule already exists, instead of plain text - `send_keychain_tx`: wrap sponsor hash in `{"sponsor_hash":"0x..."}` object when --tempo.print-sponsor-hash is used with --json Add CLI tests covering the rl and sponsor-hash JSON output shapes. * feat(tempo): add sponsored transaction plumbing (#14560) * feat(tempo): add sponsored transaction plumbing * addressing mablr comments * fix tempo sponsor signer future layout * preserve json output for tempo sponsor preview * fix(cast): `--json` output support for `vaddr` (#14591) * feat(tempo): add named nonce lanes (#14527) * fix(cheatcodes): transfer value for payable mock calls (#14547) * test: updated tests * fix: execute value transfer * test: improve * imp: review item * test: vm.prank test * imp: moved mocked-call handling after prank application --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(lint): add inline-assembly lint (#14575) * feat(lint): add inline-assembly lint * lint(inline-assembly): also recognize `/// @solidity memory-safe-assembly` NatSpec Amp-Thread-ID: https://ampcode.com/threads/T-019df4b6-1b76-734c-9a9b-29db9fb7d461 Co-authored-by: Amp --------- Co-authored-by: Amp * refactor(script): remove `ScriptConfig::{fee_token,expires_at}` in favour of `TempoOpts` (#14594) * feat(evm-core): gate optimism behind cargo feature (#14593) * fix(cli): resolve Tempo expires once (#14595) fix(cli): resolve tempo expires once * feat(cli): gate optimism behind cargo feature (#14596) * fix(anvil): classify EVM halts as transaction rejections (#14592) Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat: drop optimism deps under no-default-features (#14600) * fix(forge): `--fuzz-seed` parameter is not effective in `forge coverage` (#14610) fix --fuzz-seed not effective in forge coverage * fix(foundryup): mirror tag resolution for install & use (#14611) * fix(foundryup): mirror tag resolution for install & use * fix(foundryup): mirror semver version normalization in `use` `install` auto-prepends `v` to bare semver versions (e.g. `1.7.0` -> `v1.7.0`) so the on-disk directory is always `v`-prefixed. `use` was doing a literal lookup, so `foundryup -u 1.7.0` failed even though `foundryup -i 1.7.0` had succeeded. Broaden the channel `case` in `use()` to also match bare semver inputs (`MAJOR.MINOR.PATCH[-prerelease]`) so they go through the same `resolve_version_and_tag` normalizer. The pattern is intentionally tighter than `install`'s `[[:digit:]]*` so locally-built versions whose names happen to start with a digit are still looked up literally. Amp-Thread-ID: https://ampcode.com/threads/T-019dfc78-8557-712b-9944-bbff9a4a3b76 Co-authored-by: Amp * chore(foundryup): clarify tag-resolution log and error messages Distinguish the GitHub API tag-resolution phase from the actual binary download by consistently referring to "release tag(s)" in the `resolve_version_and_tag` helper's `say` and `err` messages. Amp-Thread-ID: https://ampcode.com/threads/T-019dfc78-8557-712b-9944-bbff9a4a3b76 Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * fix(ci): keep no-default builds free of op deps (#14612) * feat: cast unauthorized flow → wallet.tempo access-key authorization (#14517) * feat: cast unauthorized flow → wallet.tempo access-key authorization Amp-Thread-ID: https://ampcode.com/threads/T-019df174-9538-713b-b8c9-5001b1ad4719 Co-authored-by: Amp * fmt * feat(cast): replace TEMPO_NO_BROWSER env with flag * revert token addresses --------- Co-authored-by: Amp Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * docs(expect-emit): clarify next-call semantics and warn about caught-revert leak (#14620) docs(cheatcodes): clarify expectEmit next-call semantics and caught-revert leak expectEmit is a 'next call' assertion. If the call immediately after expectEmit reverts and the revert is swallowed by the caller (low-level call or try/catch), the unmatched expectation can leak forward and be satisfied by a later unrelated emission, silently turning a broken test green. Document the constraint on the natspec for both no-arg and topic-checking overloads, and regenerate cheatcodes.json. Refs: https://github.com/foundry-rs/foundry/issues/14618 Amp-Thread-ID: https://ampcode.com/threads/T-019dfd96-7a03-7249-8c10-af20ee2729f5 Co-authored-by: Amp * fix(cheatcodes): enforce `expectRevert` reverter address for CREATE frames (#14615) * fix(cheatcodes): enforce `expectRevert` reverter address for CREATE frames The reverter address argument to `vm.expectRevert` was silently ignored when the innermost reverting frame was a CREATE (top-level or nested), because create_end never populated `expected_revert.reverted_by`. Mirror call_end's logic in create_end: when the outcome reverts and a reverter address is expected, record outcome.address (revm guarantees this is Some(would-be address) whenever the constructor executed). Adds positive regression tests for top-level and nested-CREATE reverts, and a negative regression test asserting wrong-reverter now fails. Co-authored-by: Amp * improve coverage * add Derek's suggested test cases * fix: forge fmt for ExpectRevert.t.sol Amp-Thread-ID: https://ampcode.com/threads/T-019dfdc5-5414-70b6-9f49-cb5797a37a29 Co-authored-by: Amp --------- Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(script): keep plain Tempo broadcasts non-AA (#14616) * fix(script): don't force Tempo AA fee_token from --network tempo alone Plain --network tempo (or any selection that just sets the network to Tempo) does not by itself imply a Tempo AA / type 0x76 transaction. Defaulting tempo.common.fee_token to PATH_USD_ADDRESS solely from evm_opts.networks.is_tempo() caused every unsigned broadcast tx to flow through TempoOpts::apply, which set fee_token on the request and promoted it to the Tempo AA tx envelope. Signers that only know how to sign ordinary Ethereum transactions (e.g. the Ledger Ethereum app) then rejected the transaction with 'received an unexpected empty response'. Gate the default on an actual Tempo AA opt-in: - --batch (Tempo batch txs are themselves AA and need a fee token), or - any explicit --tempo.* flag (sponsor, expiring nonce, nonce key/lane, ...) which already forces an AA tx and benefits from a default fee token. Explicit --tempo.fee-token continues to win over the default in all cases, and non-Tempo networks never default the fee token. Add unit tests for each scenario. Amp-Thread-ID: https://ampcode.com/threads/T-019dfd37-2354-712f-95b1-2584fd47ad5e Co-authored-by: Amp * fix(script): don't force eth_estimateGas on plain Tempo broadcasts Plain --network tempo produces an ordinary EIP-1559/legacy transaction (see tempo-alloy::TempoTransactionRequest::output_tx_type), so the local simulation gas estimate is sufficient. Forcing RPC re-estimation in this case can surface node-side errors such as 'gas required exceeds allowance (0)' (Geth-style balance/gasPrice cap from eth_estimateGas) on flows that previously worked, including Ledger-signed broadcasts that just got unblocked from the type 0x76 regression. Match tempo-foundry's behaviour: only force eth_estimateGas on Tempo when the user has actually opted into Tempo AA semantics (--batch or any explicit --tempo.* flag). Extract the gating into needs_tempo_aa_rpc_estimate(...) and add focused unit tests mirroring the fee-token gating tests. Amp-Thread-ID: https://ampcode.com/threads/T-019dfd37-2354-712f-95b1-2584fd47ad5e Co-authored-by: Amp * fix(script): don't re-estimate plain Tempo chain broadcasts --------- Co-authored-by: Amp * fix(cheatcodes): preserve reverts with `expectEmit` (#14619) * test: added regression test * fix: re-order revert handling * refactor: simplify * lint: fmt * polish: tighten comment, extend test with revert reason and custom error Amp-Thread-ID: https://ampcode.com/threads/T-019dfd96-7a03-7249-8c10-af20ee2729f5 Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * feat(lint): add tx-origin detector (#14589) * feat(lint): add tx-origin detector * test(lint): address tx-origin review feedback * fix: ui bless * fix(lint): cover tx-origin index and ternary predicates * test(lint): bless tx-origin snapshot --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * refactor(tempo): prepare batch access key txs w/ helper (#14597) fix(tempo): prepare batch access key txs before estimation * fix(anvil): respect non-zero genesis block in Otterscan APIs (#14490) fix(anvil): respect non-zero genesis block in Otterscan APIs The three Otterscan address-history endpoints (`ots_searchTransactionsBefore`/`After`, `ots_getTransactionBySenderAndNonce`) hardcoded `unwrap_or(1)` / `unwrap_or_default()` as the lower bound of their block scan, which breaks when `genesis_block_number` is non-zero (e.g. `genesis.json` `number: 73`). Expose `Backend::genesis_number()` and fall back to `genesis_number() + 1` in non-fork mode, mirroring the existing post-fork `f.block_number() + 1` convention. --------- Co-authored-by: Isagi Yates Co-authored-by: Amp Co-authored-by: steven Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: figtracer Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Sergei Shulepov Co-authored-by: zerosnacks Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: cui Co-authored-by: Centaur AI Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Nikki Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: Mikhail Mikheev <16622558+mmv08@users.noreply.github.com> Co-authored-by: lazymio Co-authored-by: Emma Jamieson-Hoare Co-authored-by: VIkions <99107287+vikions@users.noreply.github.com> Co-authored-by: Aïssata --- .github/scripts/commit-and-read-benchmarks.sh | 114 -- .github/scripts/commit-benchmark-results.sh | 75 + .github/scripts/compare-nightly.sh | 56 + .github/scripts/read-benchmark-results.sh | 37 + .github/scripts/tempo-check.sh | 86 +- .github/workflows/benchmarks-nightly.yml | 217 +++ .github/workflows/benchmarks.yml | 73 +- .github/workflows/ci-tempo.yml | 44 +- .github/workflows/ci.yml | 17 + .github/workflows/crate-checks.yml | 2 +- .github/workflows/docker-publish.yml | 30 + .github/workflows/nix.yml | 4 +- .github/workflows/npm.yml | 4 +- .github/workflows/release.yml | 78 +- .github/workflows/test-flaky.yml | 2 +- .github/workflows/test-isolate.yml | 2 +- .github/workflows/test.yml | 4 +- Cargo.lock | 1011 +++++------ Cargo.toml | 98 +- README.md | 2 + SECURITY.md | 109 ++ benches/LATEST.md | 134 +- benches/src/main.rs | 40 +- benches/src/results.rs | 19 + benchmark.sh | 60 +- crates/anvil/Cargo.toml | 34 +- crates/anvil/core/Cargo.toml | 10 +- crates/anvil/src/cmd.rs | 27 +- crates/anvil/src/config.rs | 6 +- crates/anvil/src/eth/api.rs | 44 +- crates/anvil/src/eth/backend/executor.rs | 23 +- crates/anvil/src/eth/backend/mem/mod.rs | 267 ++- crates/anvil/src/eth/backend/mem/optimism.rs | 61 + .../anvil/src/eth/{error.rs => error/mod.rs} | 69 +- crates/anvil/src/eth/error/optimism.rs | 62 + crates/anvil/src/eth/otterscan/api.rs | 19 +- crates/anvil/src/eth/pool/transactions.rs | 4 +- crates/anvil/src/eth/sign.rs | 8 +- crates/anvil/src/{evm.rs => evm/mod.rs} | 84 +- crates/anvil/src/evm/optimism.rs | 87 + crates/anvil/src/lib.rs | 3 + crates/anvil/tests/it/main.rs | 1 + crates/anvil/tests/it/revert.rs | 50 + crates/cast/Cargo.toml | 17 +- crates/cast/src/args.rs | 7 + crates/cast/src/cmd/batch_mktx.rs | 27 +- crates/cast/src/cmd/batch_send.rs | 32 +- crates/cast/src/cmd/call.rs | 25 +- crates/cast/src/cmd/keychain.rs | 1022 ++++++++++- crates/cast/src/cmd/mktx.rs | 44 +- crates/cast/src/cmd/mod.rs | 3 + crates/cast/src/cmd/run.rs | 17 +- crates/cast/src/cmd/send.rs | 72 +- crates/cast/src/cmd/tempo.rs | 45 + crates/cast/src/cmd/tip20/mine.rs | 23 +- crates/cast/src/cmd/tip20/mod.rs | 2 +- crates/cast/src/cmd/vaddr/create.rs | 181 ++ crates/cast/src/cmd/vaddr/mod.rs | 131 ++ crates/cast/src/cmd/vaddr/resolve.rs | 52 + crates/cast/src/cmd/vaddr/watch.rs | 108 ++ crates/cast/src/cmd/wallet/mod.rs | 13 +- crates/cast/src/lib.rs | 4 +- crates/cast/src/opts.rs | 25 +- crates/cast/src/tempo.rs | 3 + crates/cast/src/tx.rs | 30 +- crates/cast/tests/cli/keychain.rs | 76 + crates/cast/tests/cli/main.rs | 119 ++ crates/cheatcodes/Cargo.toml | 10 + crates/cheatcodes/assets/cheatcodes.json | 4 +- crates/cheatcodes/spec/src/vm.rs | 6 + crates/cheatcodes/src/inspector.rs | 145 +- crates/cheatcodes/src/test/assert.rs | 4 +- crates/cheatcodes/src/version.rs | 67 +- crates/chisel/Cargo.toml | 8 +- crates/chisel/src/executor.rs | 1617 ++++++++--------- crates/chisel/src/source.rs | 561 ++---- crates/chisel/tests/it/repl/mod.rs | 20 + crates/cli/Cargo.toml | 7 + crates/cli/src/opts/evm.rs | 11 + crates/cli/src/opts/rpc.rs | 56 +- crates/cli/src/opts/rpc_common.rs | 7 +- crates/cli/src/opts/tempo.rs | 320 +++- crates/cli/src/utils/tempo.rs | 193 +- crates/common/Cargo.toml | 20 +- crates/common/build.rs | 20 +- crates/common/fmt/Cargo.toml | 8 +- crates/common/fmt/src/ui.rs | 8 + crates/common/src/contracts.rs | 14 +- crates/common/src/provider/mpp/keys.rs | 73 +- crates/common/src/provider/mpp/session.rs | 10 + crates/common/src/provider/mpp/transport.rs | 922 +++++++++- crates/common/src/provider/mpp/ws.rs | 4 + .../common/src/provider/runtime_transport.rs | 6 +- crates/common/src/tempo/auth.rs | 494 +++++ crates/common/src/tempo/keystore.rs | 147 +- crates/common/src/tempo/mod.rs | 186 ++ crates/common/src/transactions/builder.rs | 57 +- crates/common/src/transactions/receipt.rs | 2 + crates/config/src/fuzz.rs | 6 + crates/config/src/inline/mod.rs | 39 + crates/debugger/Cargo.toml | 8 + crates/doc/Cargo.toml | 4 + crates/doc/src/writer/as_doc.rs | 4 +- crates/evm/core/Cargo.toml | 24 +- crates/evm/core/src/decode.rs | 4 +- crates/evm/core/src/env.rs | 605 +++--- crates/evm/core/src/evm/mod.rs | 21 +- crates/evm/core/src/evm/op.rs | 22 +- crates/evm/core/src/fork/database.rs | 53 +- crates/evm/core/src/lib.rs | 3 + crates/evm/core/src/opts.rs | 7 +- crates/evm/coverage/Cargo.toml | 4 + crates/evm/evm/Cargo.toml | 13 + crates/evm/evm/src/executors/fuzz/mod.rs | 125 +- crates/evm/evm/src/executors/invariant/mod.rs | 2 +- crates/evm/fuzz/Cargo.toml | 9 + crates/evm/fuzz/src/lib.rs | 35 +- crates/evm/hardforks/Cargo.toml | 8 +- crates/evm/hardforks/src/lib.rs | 74 +- crates/evm/networks/Cargo.toml | 8 +- crates/evm/networks/src/lib.rs | 238 ++- crates/evm/networks/src/optimism.rs | 25 + crates/evm/traces/Cargo.toml | 4 + crates/fmt/Cargo.toml | 4 + crates/fmt/src/state/mod.rs | 2 +- crates/forge/Cargo.toml | 14 +- crates/forge/assets/tempo/MailTemplate.s.sol | 2 +- crates/forge/assets/tempo/MailTemplate.t.sol | 2 +- crates/forge/src/cmd/coverage.rs | 7 +- crates/forge/src/cmd/create.rs | 43 +- crates/forge/src/cmd/snapshot.rs | 7 +- crates/forge/src/cmd/test/mod.rs | 226 ++- crates/forge/src/cmd/test/summary.rs | 4 +- crates/forge/src/gas_report.rs | 2 +- crates/forge/src/multi_runner.rs | 32 + crates/forge/src/runner.rs | 24 +- crates/forge/tests/cli/cmd.rs | 4 +- crates/forge/tests/cli/config.rs | 28 + crates/forge/tests/cli/failure_assertions.rs | 7 +- crates/forge/tests/cli/inline_config.rs | 104 ++ crates/forge/tests/cli/lint.rs | 289 ++- crates/forge/tests/cli/lint/geiger.rs | 10 +- crates/forge/tests/cli/script.rs | 2 +- crates/forge/tests/cli/test_cmd/fuzz.rs | 145 ++ crates/forge/tests/cli/test_cmd/repros.rs | 60 + .../tests/fixtures/ExpectRevertFailures.t.sol | 57 + crates/lint/Cargo.toml | 4 + crates/lint/README.md | 8 + crates/lint/docs/README.md | 52 + crates/lint/docs/_template.md | 28 + crates/lint/docs/asm-keccak256.md | 42 + crates/lint/docs/block-timestamp.md | 44 + crates/lint/docs/boolean-cst.md | 37 + crates/lint/docs/boolean-equal.md | 34 + crates/lint/docs/could-be-immutable.md | 42 + crates/lint/docs/custom-errors.md | 45 + crates/lint/docs/divide-before-multiply.md | 32 + crates/lint/docs/erc20-unchecked-transfer.md | 43 + crates/lint/docs/incorrect-erc20-interface.md | 42 + .../lint/docs/incorrect-erc721-interface.md | 48 + crates/lint/docs/incorrect-shift.md | 37 + crates/lint/docs/inline-assembly.md | 69 + crates/lint/docs/interface-file-naming.md | 31 + crates/lint/docs/interface-naming.md | 31 + crates/lint/docs/missing-zero-check.md | 39 + crates/lint/docs/mixed-case-function.md | 32 + crates/lint/docs/mixed-case-variable.md | 36 + crates/lint/docs/multi-contract-file.md | 37 + crates/lint/docs/named-struct-fields.md | 31 + crates/lint/docs/pascal-case-struct.md | 31 + crates/lint/docs/pragma-inconsistent.md | 41 + crates/lint/docs/rtlo.md | 32 + .../lint/docs/screaming-snake-case-const.md | 30 + .../docs/screaming-snake-case-immutable.md | 31 + crates/lint/docs/too-many-digits.md | 32 + crates/lint/docs/tx-origin.md | 34 + crates/lint/docs/unaliased-plain-import.md | 34 + crates/lint/docs/unchecked-call.md | 34 + crates/lint/docs/unsafe-cheatcode.md | 35 + crates/lint/docs/unsafe-typecast.md | 40 + crates/lint/docs/unused-import.md | 40 + crates/lint/docs/unused-state-variables.md | 39 + crates/lint/docs/unwrapped-modifier-logic.md | 51 + crates/lint/src/linter/mod.rs | 2 + crates/lint/src/linter/project.rs | 92 + crates/lint/src/sol/info/inline_assembly.rs | 71 + crates/lint/src/sol/info/mod.rs | 12 + crates/lint/src/sol/info/pragma_directive.rs | 71 + crates/lint/src/sol/info/too_many_digits.rs | 50 + crates/lint/src/sol/macros.rs | 42 +- crates/lint/src/sol/med/mod.rs | 4 + crates/lint/src/sol/med/tx_origin.rs | 101 + crates/lint/src/sol/mod.rs | 133 +- crates/lint/testdata/BlockTimestamp.stderr | 24 +- crates/lint/testdata/BooleanCst.stderr | 10 +- crates/lint/testdata/BooleanEqual.stderr | 14 +- crates/lint/testdata/CouldBeImmutable.stderr | 14 +- crates/lint/testdata/CustomErrors.stderr | 10 +- .../lint/testdata/DivideBeforeMultiply.stderr | 12 +- crates/lint/testdata/Imports.stderr | 26 +- .../testdata/IncorrectERC20Interface.stderr | 30 +- .../testdata/IncorrectERC721Interface.stderr | 38 +- crates/lint/testdata/IncorrectShift.stderr | 10 +- crates/lint/testdata/InlineAssembly.sol | 110 ++ crates/lint/testdata/InlineAssembly.stderr | 96 + crates/lint/testdata/Keccak256.sol | 1 + crates/lint/testdata/Keccak256.stderr | 36 +- crates/lint/testdata/MissingZeroCheck.stderr | 46 +- crates/lint/testdata/MixedCase.stderr | 38 +- crates/lint/testdata/MultiContractFile.stderr | 10 +- .../MultiContractFile_InterfaceLibrary.stderr | 6 +- crates/lint/testdata/NamedStructFields.stderr | 2 +- .../PragmaInconsistentCaretAboveExact.sol | 7 + .../PragmaInconsistentCaretAboveExact.stderr | 16 + .../PragmaInconsistentCaretMatchesExact.sol | 7 + ...PragmaInconsistentCaretMatchesExact.stderr | 16 + .../PragmaInconsistentCaretVsTilde.sol | 7 + .../PragmaInconsistentCaretVsTilde.stderr | 16 + .../testdata/PragmaInconsistentOrVsExact.sol | 7 + .../PragmaInconsistentOrVsExact.stderr | 16 + .../PragmaInconsistentRangeVsExact.sol | 7 + .../PragmaInconsistentRangeVsExact.stderr | 16 + .../PragmaInconsistentThreeDistinct.sol | 8 + .../PragmaInconsistentThreeDistinct.stderr | 24 + crates/lint/testdata/Rtlo.stderr | 48 +- crates/lint/testdata/RtloCommentsOnly.stderr | 8 +- .../lint/testdata/ScreamingSnakeCase.stderr | 16 +- crates/lint/testdata/StructPascalCase.stderr | 12 +- crates/lint/testdata/TooManyDigits.sol | 73 + crates/lint/testdata/TooManyDigits.stderr | 72 + crates/lint/testdata/TxOrigin.sol | 65 + crates/lint/testdata/TxOrigin.stderr | 72 + crates/lint/testdata/UncheckedCall.stderr | 16 +- .../testdata/UncheckedTransferERC20.stderr | 22 +- crates/lint/testdata/UnsafeCheatcodes.stderr | 26 +- crates/lint/testdata/UnsafeTypecast.stderr | 330 ++-- .../lint/testdata/UnusedStateVariables.stderr | 10 +- .../testdata/UnwrappedModifierLogic.stderr | 22 +- crates/primitives/Cargo.toml | 17 +- crates/primitives/src/network/mod.rs | 10 +- crates/primitives/src/network/optimism.rs | 47 + crates/primitives/src/network/receipt.rs | 40 +- crates/primitives/src/transaction/envelope.rs | 281 +-- crates/primitives/src/transaction/mod.rs | 6 +- crates/primitives/src/transaction/optimism.rs | 300 +++ crates/primitives/src/transaction/receipt.rs | 114 +- crates/primitives/src/transaction/request.rs | 110 +- crates/script-sequence/Cargo.toml | 4 + crates/script/Cargo.toml | 12 + crates/script/src/broadcast.rs | 143 +- crates/script/src/lib.rs | 131 +- crates/script/src/runner.rs | 10 +- crates/script/src/verify.rs | 2 +- crates/sol-macro-gen/Cargo.toml | 4 + crates/test-utils/Cargo.toml | 4 + crates/verify/Cargo.toml | 9 + docs/dev/lintrules.md | 2 + flake.lock | 18 +- foundryup/README.md | 4 +- foundryup/foundryup | 135 +- testdata/default/cheats/ExpectRevert.t.sol | 85 + .../default/cheats/GetFoundryVersion.t.sol | 51 + testdata/default/cheats/MockCall.t.sol | 41 +- testdata/default/cheats/MockCalls.t.sol | 4 + 264 files changed, 13431 insertions(+), 4233 deletions(-) delete mode 100755 .github/scripts/commit-and-read-benchmarks.sh create mode 100755 .github/scripts/commit-benchmark-results.sh create mode 100755 .github/scripts/compare-nightly.sh create mode 100755 .github/scripts/read-benchmark-results.sh create mode 100644 .github/workflows/benchmarks-nightly.yml create mode 100644 crates/anvil/src/eth/backend/mem/optimism.rs rename crates/anvil/src/eth/{error.rs => error/mod.rs} (91%) create mode 100644 crates/anvil/src/eth/error/optimism.rs rename crates/anvil/src/{evm.rs => evm/mod.rs} (64%) create mode 100644 crates/anvil/src/evm/optimism.rs create mode 100644 crates/cast/src/cmd/tempo.rs create mode 100644 crates/cast/src/cmd/vaddr/create.rs create mode 100644 crates/cast/src/cmd/vaddr/mod.rs create mode 100644 crates/cast/src/cmd/vaddr/resolve.rs create mode 100644 crates/cast/src/cmd/vaddr/watch.rs create mode 100644 crates/cast/src/tempo.rs create mode 100644 crates/cast/tests/cli/keychain.rs create mode 100644 crates/common/src/tempo/auth.rs create mode 100644 crates/evm/networks/src/optimism.rs create mode 100644 crates/lint/docs/README.md create mode 100644 crates/lint/docs/_template.md create mode 100644 crates/lint/docs/asm-keccak256.md create mode 100644 crates/lint/docs/block-timestamp.md create mode 100644 crates/lint/docs/boolean-cst.md create mode 100644 crates/lint/docs/boolean-equal.md create mode 100644 crates/lint/docs/could-be-immutable.md create mode 100644 crates/lint/docs/custom-errors.md create mode 100644 crates/lint/docs/divide-before-multiply.md create mode 100644 crates/lint/docs/erc20-unchecked-transfer.md create mode 100644 crates/lint/docs/incorrect-erc20-interface.md create mode 100644 crates/lint/docs/incorrect-erc721-interface.md create mode 100644 crates/lint/docs/incorrect-shift.md create mode 100644 crates/lint/docs/inline-assembly.md create mode 100644 crates/lint/docs/interface-file-naming.md create mode 100644 crates/lint/docs/interface-naming.md create mode 100644 crates/lint/docs/missing-zero-check.md create mode 100644 crates/lint/docs/mixed-case-function.md create mode 100644 crates/lint/docs/mixed-case-variable.md create mode 100644 crates/lint/docs/multi-contract-file.md create mode 100644 crates/lint/docs/named-struct-fields.md create mode 100644 crates/lint/docs/pascal-case-struct.md create mode 100644 crates/lint/docs/pragma-inconsistent.md create mode 100644 crates/lint/docs/rtlo.md create mode 100644 crates/lint/docs/screaming-snake-case-const.md create mode 100644 crates/lint/docs/screaming-snake-case-immutable.md create mode 100644 crates/lint/docs/too-many-digits.md create mode 100644 crates/lint/docs/tx-origin.md create mode 100644 crates/lint/docs/unaliased-plain-import.md create mode 100644 crates/lint/docs/unchecked-call.md create mode 100644 crates/lint/docs/unsafe-cheatcode.md create mode 100644 crates/lint/docs/unsafe-typecast.md create mode 100644 crates/lint/docs/unused-import.md create mode 100644 crates/lint/docs/unused-state-variables.md create mode 100644 crates/lint/docs/unwrapped-modifier-logic.md create mode 100644 crates/lint/src/linter/project.rs create mode 100644 crates/lint/src/sol/info/inline_assembly.rs create mode 100644 crates/lint/src/sol/info/pragma_directive.rs create mode 100644 crates/lint/src/sol/info/too_many_digits.rs create mode 100644 crates/lint/src/sol/med/tx_origin.rs create mode 100644 crates/lint/testdata/InlineAssembly.sol create mode 100644 crates/lint/testdata/InlineAssembly.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.sol create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr create mode 100644 crates/lint/testdata/TooManyDigits.sol create mode 100644 crates/lint/testdata/TooManyDigits.stderr create mode 100644 crates/lint/testdata/TxOrigin.sol create mode 100644 crates/lint/testdata/TxOrigin.stderr create mode 100644 crates/primitives/src/network/optimism.rs create mode 100644 crates/primitives/src/transaction/optimism.rs diff --git a/.github/scripts/commit-and-read-benchmarks.sh b/.github/scripts/commit-and-read-benchmarks.sh deleted file mode 100755 index 358b53a73155a..0000000000000 --- a/.github/scripts/commit-and-read-benchmarks.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Script to commit benchmark results and read them for GitHub Actions output -# Usage: ./commit-and-read-benchmarks.sh - -OUTPUT_DIR="${1:-benches}" -GITHUB_EVENT_NAME="${2:-pull_request}" -GITHUB_REPOSITORY="${3:-}" - -# Global variable for branch name -BRANCH_NAME="" - -# Function to commit benchmark results -commit_results() { - echo "Configuring git..." - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # For PR runs, fetch and checkout the PR branch to ensure we're up to date - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" - git fetch origin "$GITHUB_HEAD_REF" - git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" - fi - - echo "Adding benchmark file..." - git add "$OUTPUT_DIR/LATEST.md" - - if git diff --staged --quiet; then - echo "No changes to commit" - else - echo "Committing benchmark results..." - git commit -m "chore(\`benches\`): update benchmark results - -🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) - -Co-Authored-By: github-actions " - - echo "Pushing to repository..." - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - # For manual runs, we're on a new branch - git push origin "$BRANCH_NAME" - elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - # For PR runs, push to the PR branch - if [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Pushing to PR branch: $GITHUB_HEAD_REF" - git push origin "$GITHUB_HEAD_REF" - else - echo "Error: GITHUB_HEAD_REF not set for pull_request event" - exit 1 - fi - else - # This workflow should only run on workflow_dispatch or pull_request - echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" - echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" - exit 1 - fi - echo "Successfully pushed benchmark results" - fi -} - -# Function to read benchmark results and output for GitHub Actions -read_results() { - if [ -f "$OUTPUT_DIR/LATEST.md" ]; then - echo "Reading benchmark results..." - - # Output full results - { - echo 'results<> "$GITHUB_OUTPUT" - - # Format results for PR comment - echo "Formatting results for PR comment..." - FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") - - { - echo 'pr_comment<> "$GITHUB_OUTPUT" - - echo "Successfully read and formatted benchmark results" - else - echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" - fi -} - -# Main execution -echo "Starting benchmark results processing..." - -# Create new branch for manual runs -if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - echo "Manual workflow run detected, creating new branch..." - BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$BRANCH_NAME" - echo "Created branch: $BRANCH_NAME" - - # Output branch name for later use - echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" -fi - -# Always commit benchmark results -echo "Committing benchmark results..." -commit_results - -# Always read results for output -read_results - -echo "Benchmark results processing complete" \ No newline at end of file diff --git a/.github/scripts/commit-benchmark-results.sh b/.github/scripts/commit-benchmark-results.sh new file mode 100755 index 0000000000000..f7dba8980fd64 --- /dev/null +++ b/.github/scripts/commit-benchmark-results.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -euo pipefail + +# Script to commit and push benchmark results. +# +# This script is intended to run from the lightweight `publish-results` job, +# which checks out the repo with credentials and only operates on the +# trusted artifact produced by the benchmark job. Keeping the write-scoped +# token away from the bench job (which runs untrusted third-party builds) +# limits the blast radius of a compromised dependency. +# +# Usage: ./commit-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" +GITHUB_EVENT_NAME="${2:-workflow_dispatch}" +GITHUB_REPOSITORY="${3:-}" + +if [ ! -f "$OUTPUT_DIR/LATEST.md" ]; then + echo "Error: $OUTPUT_DIR/LATEST.md not found, nothing to commit" + exit 1 +fi + +echo "Configuring git..." +git config --local user.email "action@github.com" +git config --local user.name "GitHub Action" + +# Decide which branch to commit to based on the event. +BRANCH_NAME="" +case "$GITHUB_EVENT_NAME" in + workflow_dispatch) + echo "Manual workflow run detected, creating new branch..." + BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH_NAME" + echo "Created branch: $BRANCH_NAME" + ;; + pull_request) + if [ -z "${GITHUB_HEAD_REF:-}" ]; then + echo "Error: GITHUB_HEAD_REF not set for pull_request event" + exit 1 + fi + echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" + git fetch origin "$GITHUB_HEAD_REF" + git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" + BRANCH_NAME="$GITHUB_HEAD_REF" + ;; + *) + echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" + echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" + exit 1 + ;; +esac + +# Always emit the branch name so downstream steps (e.g. PR creation) can use it. +echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + +echo "Adding benchmark file..." +git add "$OUTPUT_DIR/LATEST.md" + +if git diff --staged --quiet; then + echo "No changes to commit" + echo "committed=false" >> "$GITHUB_OUTPUT" + exit 0 +fi + +echo "Committing benchmark results..." +git commit -m "chore(\`benches\`): update benchmark results + +🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) + +Co-Authored-By: github-actions " + +echo "Pushing to repository..." +git push origin "$BRANCH_NAME" +echo "Successfully pushed benchmark results to $BRANCH_NAME" +echo "committed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/compare-nightly.sh b/.github/scripts/compare-nightly.sh new file mode 100755 index 0000000000000..674cc0fe01754 --- /dev/null +++ b/.github/scripts/compare-nightly.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Compare two nightly benchmark JSON summaries and report regressions. +# +# Usage: compare-nightly.sh [warn_pct] [fail_pct] +# Exits 0 if no regressions, 1 if any metric exceeds fail_pct. +# Exits 0 gracefully when prev.json is missing (first run / gap > 7 days). +set -euo pipefail + +PREV_JSON="${1:-}" +TODAY_JSON="${2:-}" +WARN="${3:-1}" +FAIL="${4:-3}" + +PREV_JSON="$PREV_JSON" TODAY_JSON="$TODAY_JSON" WARN="$WARN" FAIL="$FAIL" \ +python3 - <<'EOF' +import json, os, sys + +warn = float(os.environ["WARN"]) +fail = float(os.environ["FAIL"]) + +prev_path = os.environ.get("PREV_JSON", "") +prev = json.load(open(prev_path)) if prev_path and os.path.isfile(prev_path) else {} +with open(os.environ["TODAY_JSON"]) as f: + today = json.load(f) + +print("## Nightly Benchmark Regression Report\n") +print("| Benchmark | Previous | Today | Δ | Status |") +print("|-----------|----------|-------|---|--------|") + +has_regression = False +all_keys = sorted(prev.keys() | today.keys()) +for key in all_keys: + t = today.get(key) + p = prev.get(key) + if t is None: + print(f"| `{key}` | {p:.5f}s | N/A | — | ⚠️ Missing |") + has_regression = True + continue + if p is None: + print(f"| `{key}` | N/A | {t:.5f}s | — | 🆕 New |") + continue + delta = (t - p) / p * 100 + if delta >= fail: + status = "🔴 Regression" + has_regression = True + elif delta >= warn: + status = "🟡 Warning" + elif delta <= -warn: + status = "🟢 Improvement" + else: + status = "➡️ OK" + sign = "+" if delta > 0 else "" + print(f"| `{key}` | {p}s | {t}s | {sign}{delta:.1f}% | {status} |") + +sys.exit(1 if has_regression else 0) +EOF diff --git a/.github/scripts/read-benchmark-results.sh b/.github/scripts/read-benchmark-results.sh new file mode 100755 index 0000000000000..548611a7d204a --- /dev/null +++ b/.github/scripts/read-benchmark-results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# Script to read benchmark results and emit them as GitHub Actions outputs. +# This script performs no git operations — it only reads the combined +# benchmark file and writes outputs for the workflow to consume. +# +# Usage: ./read-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" + +echo "Reading benchmark results from $OUTPUT_DIR..." + +if [ -f "$OUTPUT_DIR/LATEST.md" ]; then + # Output full results + { + echo 'results<> "$GITHUB_OUTPUT" + + # Format results for PR comment + echo "Formatting results for PR comment..." + FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") + + { + echo 'pr_comment<> "$GITHUB_OUTPUT" + + echo "Successfully read and formatted benchmark results" +else + echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" +fi diff --git a/.github/scripts/tempo-check.sh b/.github/scripts/tempo-check.sh index b730c466bde55..3caea992cfe7e 100755 --- a/.github/scripts/tempo-check.sh +++ b/.github/scripts/tempo-check.sh @@ -445,7 +445,7 @@ echo -e "\n=== CAST SEND WITH SPONSOR (--tempo.sponsor-signature) ===" # Test sponsored transactions using pre-signed signature. # Step 1: Get the fee_payer_signature_hash using --tempo.print-sponsor-hash # Step 2: Sign it with the sponsor's private key -# Step 3: Send with --tempo.sponsor-signature +# Step 3: Send with --tempo.sponsor and --tempo.sponsor-signature # Step 1: Get the hash that the sponsor needs to sign FEE_PAYER_HASH=$(cast mktx ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ @@ -460,7 +460,7 @@ printf "Sponsor signature: %s\n" "$SPONSOR_SIG" # Step 3: Send the sponsored transaction with the signature RECEIPT=$(cast send ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ 0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D 'increment()' --private-key "$PK" \ - --tempo.sponsor-signature "$SPONSOR_SIG" --json) + --tempo.sponsor "$SPONSOR_ADDR" --tempo.sponsor-signature "$SPONSOR_SIG" --json) # Verify the fee_payer in the receipt matches the sponsor address RECEIPT_FEE_PAYER=$(echo "$RECEIPT" | jq -r '.feePayer // .fee_payer // empty') @@ -897,3 +897,85 @@ check_has_code "Nonce" "0x4e4F4E4345000000000000000000000000000000" check_has_code "AccountKeychain" "0xaAAAaaAA00000000000000000000000000000000" echo -e "\n=== CHISEL FORK TESTS COMPLETE ===" + +# --- cast virtual-address (TIP-1022) tests --- + +echo -e "\n=== CAST VIRTUAL-ADDRESS: SETUP MASTER WALLET ===" +vaddr_master_json="$(cast wallet new --json)" +VADDR_MASTER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_master_json")" +VADDR_MASTER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_master_json")" +printf "Master address: %s\n" "$VADDR_MASTER_ADDR" +fund_and_wait "$VADDR_MASTER_ADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: CREATE (mine + register) ===" +# Use the `vaddr` alias to also exercise it. +VADDR_CREATE_OUT=$(cast vaddr create \ +--owner "$VADDR_MASTER_ADDR" \ +--private-key "$VADDR_MASTER_PK" \ +--rpc-url "$ETH_RPC_URL") +echo "$VADDR_CREATE_OUT" +VADDR=$(echo "$VADDR_CREATE_OUT" | sed -n 's/^ tag=0x000000000000 \(0x[a-fA-F0-9]\{40\}\).*/\1/p' | head -1) +if [[ -z "$VADDR" ]]; then +echo "ERROR: failed to parse virtual address from create output" +exit 1 +fi +echo "Virtual address: $VADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: RESOLVE ===" +VADDR_RESOLVE_OUT=$(cast virtual-address resolve "$VADDR" --rpc-url "$ETH_RPC_URL") +echo "$VADDR_RESOLVE_OUT" +RESOLVED_MASTER=$(echo "$VADDR_RESOLVE_OUT" | sed -n 's/^Master address: \(0x[a-fA-F0-9]\{40\}\).*/\1/p') +RESOLVED_LOWER=$(echo "$RESOLVED_MASTER" | tr '[:upper:]' '[:lower:]') +EXPECTED_LOWER=$(echo "$VADDR_MASTER_ADDR" | tr '[:upper:]' '[:lower:]') +if [[ "$RESOLVED_LOWER" != "$EXPECTED_LOWER" ]]; then +echo "ERROR: resolve returned master $RESOLVED_MASTER, expected $VADDR_MASTER_ADDR" +exit 1 +fi +echo "OK: resolve returned the registered master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: AUTO-FORWARD TO MASTER ===" +# Create a separate sender, fund it, and transfer the fee token to the +# virtual address. The protocol must auto-forward to the master wallet. +vaddr_sender_json="$(cast wallet new --json)" +VADDR_SENDER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_sender_json")" +VADDR_SENDER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_sender_json")" +fund_and_wait "$VADDR_SENDER_ADDR" + +BAL_BEFORE=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance before: $BAL_BEFORE" + +# Capture the current block before the transfer so `cast vaddr watch` can +# replay the Transfer log via --from-block. +BLOCK_BEFORE_TRANSFER=$(cast block-number --rpc-url "$ETH_RPC_URL") + +AMOUNT=1000000 +cast send "$FEE_TOKEN" 'transfer(address,uint256)' "$VADDR" "$AMOUNT" \ +--rpc-url "$ETH_RPC_URL" --private-key "$VADDR_SENDER_PK" + +BAL_AFTER=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance after: $BAL_AFTER" + +EXPECTED=$((BAL_BEFORE + AMOUNT)) +if [[ "$BAL_AFTER" != "$EXPECTED" ]]; then +echo "ERROR: master balance grew by $((BAL_AFTER - BAL_BEFORE)), expected $AMOUNT" +exit 1 +fi +echo "OK: transfer to virtual address auto-forwarded to master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: WATCH ===" +# Tail incoming TIP-20 transfers to the virtual address. `cast vaddr watch` +# polls indefinitely, so we cap it with `timeout`; the historical fetch via +# --from-block emits the prior Transfer log immediately at startup. +WATCH_OUT=$(timeout 5 cast vaddr watch "$VADDR" \ + --token "$FEE_TOKEN" \ + --from-block "$BLOCK_BEFORE_TRANSFER" \ + --rpc-url "$ETH_RPC_URL" 2>&1 || true) +echo "$WATCH_OUT" + +EXPECTED_PATTERN="token=$FEE_TOKEN from=$VADDR_SENDER_ADDR amount=$AMOUNT" +echo "Expected pattern: $EXPECTED_PATTERN" +if ! echo "$WATCH_OUT" | grep -iqF "$EXPECTED_PATTERN"; then + echo "ERROR: cast vaddr watch output did not contain expected '$EXPECTED_PATTERN'" + exit 1 +fi +echo "OK: cast vaddr watch reported correct token/from/amount" diff --git a/.github/workflows/benchmarks-nightly.yml b/.github/workflows/benchmarks-nightly.yml new file mode 100644 index 0000000000000..8569f52ce3b93 --- /dev/null +++ b/.github/workflows/benchmarks-nightly.yml @@ -0,0 +1,217 @@ +name: Nightly Benchmarks (AAVE v4) + +permissions: {} + +on: + schedule: + - cron: "0 2 * * *" # 2am UTC nightly + workflow_dispatch: # allow manual triggering for testing + +env: + AAVE_V4_REPO: "aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" + RUSTC_WRAPPER: "sccache" + +jobs: + run-benchmarks: + name: Run Nightly Benchmarks + runs-on: depot-ubuntu-24.04-32 + permissions: + contents: read + actions: read # needed to download artifacts from previous runs + outputs: + has_regression: ${{ steps.compare.outputs.has_regression }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + + - name: Setup Foundry + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + ./.github/scripts/setup-foundryup.sh + printf '%s\n' "$GITHUB_WORKSPACE/.foundry/bin" >> "$GITHUB_PATH" + + - name: Build benchmark binary + run: cargo build --locked --release --bin foundry-bench + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24" + + - name: Install hyperfine + run: | + curl -L https://github.com/sharkdp/hyperfine/releases/download/v1.19.0/hyperfine-v1.19.0-x86_64-unknown-linux-gnu.tar.gz | tar xz + sudo mv hyperfine-v1.19.0-x86_64-unknown-linux-gnu/hyperfine /usr/local/bin/ + rm -rf hyperfine-v1.19.0-x86_64-unknown-linux-gnu + + - name: Download previous benchmark results + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p prev-results + PREV_RUN_ID=$(gh run list \ + --workflow=benchmarks-nightly.yml \ + --status=success \ + --limit=1 \ + --json databaseId \ + -q '.[0].databaseId // empty' 2>/dev/null || true) + if [[ -n "$PREV_RUN_ID" ]]; then + echo "Downloading results from previous run $PREV_RUN_ID" + gh run download "$PREV_RUN_ID" \ + --name nightly-bench-results \ + --dir prev-results/ || true + else + echo "No previous successful run found, skipping download." + fi + + - name: Run forge test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches --force-install \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_test \ + --json-output "nightly-${DATE}-forge_test.json" \ + --verbose + + - name: Run forge fuzz test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_fuzz_test \ + --json-output "nightly-${DATE}-forge_fuzz_test.json" \ + --verbose + + - name: Run forge isolate test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_isolate_test \ + --json-output "nightly-${DATE}-forge_isolate_test.json" \ + --verbose + + - name: Run forge build benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --json-output "nightly-${DATE}-forge_build.json" \ + --verbose + + - name: Run forge coverage benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_coverage \ + --json-output "nightly-${DATE}-forge_coverage.json" \ + --verbose + + - name: Merge benchmark JSON results + run: | + DATE=$(date -u +%Y-%m-%d) + shopt -s nullglob + parts=( benches/nightly-${DATE}-*.json ) + if [[ ${#parts[@]} -eq 0 ]]; then + echo "No benchmark results produced — all steps failed." + exit 1 + fi + jq -s 'add' "${parts[@]}" > "benches/nightly-${DATE}.json" + echo "Merged ${#parts[@]} result file(s) into nightly-${DATE}.json" + + - name: Compare with previous results + id: compare + run: | + DATE=$(date -u +%Y-%m-%d) + PREV_JSON=$(ls prev-results/nightly-*.json 2>/dev/null | head -1 || true) + TODAY_JSON="benches/nightly-${DATE}.json" + if ./.github/scripts/compare-nightly.sh "$PREV_JSON" "$TODAY_JSON" > regression.md 2>&1; then + echo "has_regression=false" >> "$GITHUB_OUTPUT" + else + echo "has_regression=true" >> "$GITHUB_OUTPUT" + fi + cat regression.md + + - name: Upload benchmark results + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nightly-bench-results + retention-days: 7 + path: | + benches/nightly-*.json + regression.md + + report-regression: + name: Report Regression + needs: run-benchmarks + if: needs.run-benchmarks.outputs.has_regression == 'true' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Download benchmark results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nightly-bench-results + path: results/ + + - name: Open regression issue + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + DATE=$(date -u +%Y-%m-%d) + BODY="$(cat results/regression.md) + + --- + + **Run**: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + **Date**: ${DATE} + **Repo benchmarked**: \`aave/aave-v4\` (pinned commit) + **Threshold**: 🔴 >=3% regression, 🟡 >=1% warning" + + gh issue create \ + --title "[Nightly Regression] ${DATE}" \ + --body "$BODY" \ + --label "regression" \ + --repo "$GH_REPO" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5d4767ad3b554..a136703abc294 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -10,17 +10,17 @@ on: required: false type: string versions: - description: "Comma-separated list of Foundry versions to benchmark (e.g., stable,nightly,v1.0.0)" + description: "Comma-separated list of Foundry versions to benchmark (optional, defaults to 'v1.5.1,v1.7.0')" required: false type: string - default: "stable,nightly" repos: - description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev][ ] (e.g. vectorized/solady:v0.1.26 --nmc BrokenTest). Leave empty to use the per-benchmark default repo lists." + description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev] (e.g. aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35). Leave empty to use the per-benchmark default repo lists." required: false type: string - default: "" env: + DEFAULT_VERSIONS: "v1.5.1,v1.7.0" + # Repos to benchmark per step. Each comma-separated entry has the form # org/repo[:rev][ ] # where anything after the first whitespace is appended to every benchmark @@ -29,27 +29,23 @@ env: TEST_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting ISOLATE_TEST_REPOS: >- ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest, - vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, + vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest', uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting BUILD_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 COVERAGE_REPOS: >- ithacaxyz/account:v0.5.7, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 @@ -60,7 +56,7 @@ jobs: name: Run All Benchmarks runs-on: depot-ubuntu-24.04-32 permissions: - contents: write + contents: read steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -93,7 +89,7 @@ jobs: run: cargo build --locked --release --bin foundry-bench - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" @@ -106,59 +102,61 @@ jobs: - name: Run forge test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_test,forge_fuzz_test \ - --output-file forge_test_bench.md + --output-file forge_test_bench.md \ + --verbose - name: Run forge isolate test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.ISOLATE_TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_isolate_test \ - --output-file forge_isolate_test_bench.md + --output-file forge_isolate_test_bench.md \ + --verbose - name: Run forge build benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.BUILD_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_build_no_cache,forge_build_with_cache \ - --output-file forge_build_bench.md + --output-file forge_build_bench.md \ + --verbose - name: Run forge coverage benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.COVERAGE_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_coverage \ - --output-file forge_coverage_bench.md + --output-file forge_coverage_bench.md \ + --verbose - name: Combine benchmark results run: ./.github/scripts/combine-benchmarks.sh benches - - name: Commit and read benchmark results + - name: Read benchmark results id: benchmark_results - env: - GITHUB_HEAD_REF: ${{ github.head_ref }} - run: ./.github/scripts/commit-and-read-benchmarks.sh benches "${{ github.event_name }}" "${{ github.repository }}" + run: ./.github/scripts/read-benchmark-results.sh benches - name: Upload benchmark results as artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -172,21 +170,21 @@ jobs: benches/LATEST.md outputs: - branch_name: ${{ steps.benchmark_results.outputs.branch_name }} pr_comment: ${{ steps.benchmark_results.outputs.pr_comment }} publish-results: name: Publish Results needs: run-benchmarks runs-on: ubuntu-latest + # All git writes happen here, on a clean ubuntu-latest runner that has + # never executed third-party benchmark code. permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + # persist-credentials defaults to true so we can push. - name: Download benchmark results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -194,19 +192,22 @@ jobs: name: benchmark-results path: benches/ - - name: Push branch for manual runs - if: github.event_name == 'workflow_dispatch' - run: | - git push origin "${{ needs.run-benchmarks.outputs.branch_name }}" - echo "Pushed branch: ${{ needs.run-benchmarks.outputs.branch_name }}" + - name: Commit benchmark results + id: commit_results + env: + GITHUB_HEAD_REF: ${{ github.head_ref }} + run: ./.github/scripts/commit-benchmark-results.sh benches "${{ github.event_name }}" "${{ github.repository }}" - name: Create PR for manual runs - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.commit_results.outputs.committed == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + BRANCH_NAME: ${{ steps.commit_results.outputs.branch_name }} + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | - const branchName = '${{ needs.run-benchmarks.outputs.branch_name }}'; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const branchName = process.env.BRANCH_NAME; + const prComment = process.env.PR_COMMENT; // Create the pull request const { data: pr } = await github.rest.pulls.create({ @@ -231,10 +232,12 @@ jobs: - name: Comment on PR if: github.event.inputs.pr_number != '' || github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | const prNumber = ${{ github.event.inputs.pr_number || github.event.pull_request.number }}; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const prComment = process.env.PR_COMMENT; const comment = `${prComment} diff --git a/.github/workflows/ci-tempo.yml b/.github/workflows/ci-tempo.yml index ad4c424b7e8b2..1ef4c760f324e 100644 --- a/.github/workflows/ci-tempo.yml +++ b/.github/workflows/ci-tempo.yml @@ -69,14 +69,16 @@ jobs: run: | cargo test --locked -p foundry-common --lib tempo::tests::test_fork_schedule_parses_configured_rpcs -- --exact --nocapture - - name: Run Tempo check on devnet + - name: Run Tempo check on mainnet if: | - github.event_name == 'push' || - github.event_name == 'pull_request' || - github.event.inputs.network == 'devnet' || + github.event_name == 'schedule' || + github.event.inputs.network == 'mainnet' || github.event.inputs.network == 'all' env: - ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} + ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} + TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" + VERIFIER_URL: ${{ secrets.VERIFIER_URL }} + PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} run: | if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then @@ -86,16 +88,6 @@ jobs: ./.github/scripts/tempo-deploy.sh fi - - name: Run Tempo wallet tests on devnet - if: | - github.event_name == 'push' || - github.event_name == 'pull_request' || - github.event.inputs.network == 'devnet' || - github.event.inputs.network == 'all' - env: - ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} - run: ./.github/scripts/tempo-wallet.sh - - name: Run Tempo check on testnet if: | github.event_name == 'schedule' || @@ -113,16 +105,14 @@ jobs: ./.github/scripts/tempo-deploy.sh fi - - name: Run Tempo check on mainnet + - name: Run Tempo check on devnet if: | - github.event_name == 'schedule' || - github.event.inputs.network == 'mainnet' || + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || github.event.inputs.network == 'all' env: - ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} - TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" - VERIFIER_URL: ${{ secrets.VERIFIER_URL }} - PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} run: | if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then @@ -132,6 +122,16 @@ jobs: ./.github/scripts/tempo-deploy.sh fi + - name: Run Tempo wallet tests on devnet + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || + github.event.inputs.network == 'all' + env: + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} + run: ./.github/scripts/tempo-wallet.sh + # If the nightly run fails, this will create an issue to signal so. issue: name: Open an issue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eb90a76cdbfd..a434028fdd18c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,23 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test --workspace --doc --locked + no-default-features: + runs-on: depot-ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - run: cargo build --workspace --no-default-features --locked + typos: runs-on: depot-ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/crate-checks.yml b/.github/workflows/crate-checks.yml index eb865bddc10e3..f0d460da6fbb1 100644 --- a/.github/workflows/crate-checks.yml +++ b/.github/workflows/crate-checks.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: cargo-hack - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b97a99d5310a4..6120735657ee6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -32,6 +32,8 @@ jobs: name: build and push runs-on: depot-ubuntu-latest permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -92,6 +94,7 @@ jobs: uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1 - name: Build and push Foundry image + id: build uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0 with: build-args: | @@ -106,3 +109,30 @@ jobs: platforms: linux/amd64,linux/arm64 push: true no-cache: true + sbom: true + provenance: mode=max + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign image with cosign (keyless) + env: + DOCKER_TAGS: ${{ steps.docker_tagging.outputs.docker_tags }} + DIGEST: ${{ steps.build.outputs.digest }} + shell: bash + run: | + set -euo pipefail + IFS=',' read -r -a tags <<< "$DOCKER_TAGS" + for tag in "${tags[@]}"; do + # Strip any tag suffix and pin to immutable digest, then sign. + ref="${tag%%:*}@${DIGEST}" + printf 'Signing %s\n' "$ref" + cosign sign --yes "$ref" + done + + - name: Image build provenance attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 0a18c99a41f82..8528b71f299a9 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -19,7 +19,7 @@ jobs: contents: write pull-requests: write steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -38,7 +38,7 @@ jobs: permissions: contents: read steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index fdc5e5716c577..323059e99e6b6 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -143,7 +143,7 @@ jobs: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -259,7 +259,7 @@ jobs: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 682c9214284f6..38fa791fb655f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Build changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2 + uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2.1 with: configuration: "./.github/changelog.json" fromTag: ${{ steps.release_info.outputs.from_tag || '' }} @@ -117,6 +117,8 @@ jobs: needs: prepare uses: ./.github/workflows/docker-publish.yml permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -129,9 +131,10 @@ jobs: # way, GitHub's immutable-releases setting seals the release at publish. release: permissions: - id-token: write - contents: write attestations: write + artifact-metadata: write + contents: write + id-token: write name: release ${{ matrix.target }} (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} timeout-minutes: 240 @@ -264,6 +267,38 @@ jobs: printf "file_name=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.zip" >> "$GITHUB_OUTPUT" fi printf "foundry_attestation=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.attestation.txt" >> "$GITHUB_OUTPUT" + printf "foundry_sbom=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.spdx.json" >> "$GITHUB_OUTPUT" + printf "foundry_checksum=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sha256" >> "$GITHUB_OUTPUT" + printf "foundry_signature=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sigstore.json" >> "$GITHUB_OUTPUT" + + - name: Generate archive checksum + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + shell: bash + run: | + set -euo pipefail + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + else + shasum -a 256 "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + fi + cat "$FOUNDRY_CHECKSUM" + + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + + - name: Generate SBOM (SPDX) + env: + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + VERSION_NAME: ${{ (env.IS_NIGHTLY == 'true' && 'nightly') || needs.prepare.outputs.tag_name }} + shell: bash + run: | + set -euo pipefail + syft scan dir:. \ + --source-name foundry \ + --source-version "$VERSION_NAME" \ + -o spdx-json="$FOUNDRY_SBOM" - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -292,15 +327,37 @@ jobs: tar -czvf "foundry_man_${VERSION_NAME}.tar.gz" forge.1.gz cast.1.gz anvil.1.gz chisel.1.gz printf 'foundry_man=%s\n' "foundry_man_${VERSION_NAME}.tar.gz" >> "$GITHUB_OUTPUT" - - name: Binaries attestation + - name: Binaries and archive provenance attestation id: attestation - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: | ${{ env.anvil_bin_path }} ${{ env.cast_bin_path }} ${{ env.chisel_bin_path }} ${{ env.forge_bin_path }} + ${{ steps.artifacts.outputs.file_name }} + + - name: Archive SBOM attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: ${{ steps.artifacts.outputs.file_name }} + sbom-path: ${{ steps.artifacts.outputs.foundry_sbom }} + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign archive with cosign (keyless) + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} + shell: bash + run: | + set -euo pipefail + cosign sign-blob \ + --yes \ + --bundle "$FOUNDRY_SIGNATURE" \ + "$FILE_NAME" - name: Record attestation URL env: @@ -321,11 +378,20 @@ jobs: TAG_NAME: ${{ needs.prepare.outputs.tag_name }} FILE_NAME: ${{ steps.artifacts.outputs.file_name }} FOUNDRY_ATTESTATION: ${{ steps.artifacts.outputs.foundry_attestation }} + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} FOUNDRY_MAN: ${{ steps.man.outputs.foundry_man }} shell: bash run: | set -euo pipefail - files=("$FILE_NAME" "$FOUNDRY_ATTESTATION") + files=( + "$FILE_NAME" + "$FOUNDRY_ATTESTATION" + "$FOUNDRY_SBOM" + "$FOUNDRY_CHECKSUM" + "$FOUNDRY_SIGNATURE" + ) if [[ -n "${FOUNDRY_MAN:-}" ]]; then files+=("$FOUNDRY_MAN") fi diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index d6244f826887e..9caa254f05c10 100644 --- a/.github/workflows/test-flaky.yml +++ b/.github/workflows/test-flaky.yml @@ -33,7 +33,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index 141e3a049a73b..6763f1f80bde3 100644 --- a/.github/workflows/test-isolate.yml +++ b/.github/workflows/test-isolate.yml @@ -37,7 +37,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaa46c8e79f7c..daa4822b6e395 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,14 +73,14 @@ jobs: toolchain: stable target: ${{ matrix.target }} - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest # External tests dependencies - name: Setup Node.js if: contains(matrix.name, 'external') - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 - name: Install Bun diff --git a/Cargo.lock b/Cargo.lock index 781b2c7f02b50..9db38f1967f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,21 +77,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a547705d5c1b42575a0542bae2ba45bc62a6154be86611afaef1c0ab5c38598e" +checksum = "d8010fc7e9e8643ef4e758cdccf3eef26734594aedf88a9d5ed35e51837d42ef" dependencies = [ "alloy-consensus", "alloy-contract", "alloy-core", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-network", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -116,14 +116,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8c24c95e90c1608c2d91cff1b451d796474168d3310ccc8b7cd12502ca8169" +checksum = "e3d64da86c616b5092ea64eea648f311bbd58630a0b384c42d699175d6f9122b" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "alloy-tx-macros", "auto_impl", @@ -143,23 +143,23 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d211ad0ef468a70a7a829e49683ff59ad25f02b4ab3764344c4c2663329a52c" +checksum = "8fd98696ca3617d3a9ba1a6f2011880cbfd5618228dab6400c9f8bca457859a8" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-contract" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59d55233ac14aa7fa6bcdcad45ba305e90c556065e0947cd9f243c4469e7c2d" +checksum = "de3df0aadc569a8b277808a7d0ad0e421180654ea36a3c59e9ed2bb968c9a1cd" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -238,12 +238,12 @@ dependencies = [ [[package]] name = "alloy-eip5792" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250ba1168b8a049185a68c4dfa7f2a6a4046bd26fcc8c68632caeb216a5e12dc" +checksum = "1ceb16e7fe5a95825305f218ccd356665f848831f94ce2bbf55339bf5d21e88a" dependencies = [ "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] @@ -265,13 +265,14 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +checksum = "ec6ae911a2fc304a7cb80a79fb7bed6d1474aed4e7c203df1f8ff538f64fc78d" dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "once_cell", "serde", ] @@ -300,9 +301,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae69eaa5096b47ffe97e6a5d6bde7e7fa2dec106af22a9315621d11039c3de3c" +checksum = "64c0456f5f7a4497e9342d20f528e30f5288ddfa0d6a012bd5044afee46cd8a0" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -310,7 +311,7 @@ dependencies = [ "alloy-eip7928", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "auto_impl", "borsh", "c-kzg", @@ -325,9 +326,9 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a8c1330ad33c95b5958573bca9a1ad0b419a51d76bb4c521556fbba8539b8d" +checksum = "d5638cbbffb318d440fdb009de019090d8d117dae40de9d10cdb29891ea59eb9" dependencies = [ "alloy-contract", "alloy-primitives", @@ -339,12 +340,12 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.33.2" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc4b83cb672156663e6094d098beb509965b7fe684bb3d6e44bb9ca2e9ae714" +checksum = "c1ceeea6dcbbcd4e546b27700763a6f6c3b3fee30054209884f521078b6fda4f" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-hardforks", "alloy-primitives", "alloy-rpc-types-engine", @@ -359,13 +360,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39789db0b3f3bbef0e6549c87bc6842b73886ebabee1405b6941685b1cc34083" +checksum = "a71ff8b55d2b8aa05259f474cae7dea0e4991724dc18936b81cb23ec492a0c2a" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "borsh", "serde", @@ -400,9 +401,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662b525af73e86b2167dae923261c8edf440ba7e1426b30a8b993177bc214c02" +checksum = "19e352478b756bad5d7203148e4b461861282ea2ded3da406ba24868b52cd098" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -415,19 +416,19 @@ dependencies = [ [[package]] name = "alloy-network" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c657c2d9751d3c7d94990554b231e5372c3c2e4bad842806280b6151a0d6a05d" +checksum = "ed08ae169869e08370ed121612e0d3dadac33d1a256e9f2465926b23f0bd7d95" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-any", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-sol-types", "async-trait", @@ -441,24 +442,24 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e7c4bb0ebbd6d7406d2808968f43c0d5186c69c5e58cedcbee7380f4cd1fcf" +checksum = "02e6c7ad28afe348a9a9c5624b67ee5b3607b8de98d5816b3056ecdfa6fa2697" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-op-evm" version = "0.31.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -472,7 +473,7 @@ dependencies = [ [[package]] name = "alloy-op-hardforks" version = "0.4.7" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -513,13 +514,13 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fea0fc2628cdbc851aaa333124f9d8ab9f567ab8d4c20202819db13aa1a534" +checksum = "93a7c17472b55482d4734154c2f5ed13f72e03f6752cebb927f6a2d8b52e646c" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network", "alloy-network-primitives", @@ -547,7 +548,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "thiserror 2.0.18", @@ -559,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc7b42e514613c717887dc77bb58d35e845557ebd63a18c3f92a77094e4891f" +checksum = "a8d86958b02bca85103d64fa60d7b364a8b017c6e40f2b02c3f50ca22964a738" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -603,9 +604,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ee7b51752c68fb95f21705e402700750e692b1d21ccc294ac48fadc8655d53" +checksum = "5beb5c2fe6b960c8e8b038e69fd502a90a2e930afa4770efb748b163b0767729" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -616,7 +617,7 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "tokio", @@ -629,9 +630,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa76988f54105ad4398828e8aaf1a39b3f07f91fb79091529056689514ee8c2" +checksum = "4ee1257a278f6d293e05c5162c5940a1561b1aa85ded0028b464c81de37ebfa5" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -640,44 +641,44 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-rpc-types-txpool", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-anvil" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d276bea4e92e4991269d31b9abd3e722eed2565b82036478a4416adb8dd4992" +checksum = "df32156f085e74eac942b6103744be49b817c302341aaa8cb0c1c88dc29228d9" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1a9a3bda9be7f6515316eb792710532411878bbfc88934973f4b371376b00d" +checksum = "6a234bfbdf7a76c3d13808f729af5321852de3dedcaa6fc6d5f54787aaf54c6a" dependencies = [ "alloy-consensus-any", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] [[package]] name = "alloy-rpc-types-beacon" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5d68ddca890854fb78291cbde06115473ded00b2337d0f815e92c0c1f8003" +checksum = "296450f5e76bece0116c939b9437b0421a5da9c5d40031bf4cf9b38d3d94e475" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "derive_more", @@ -689,9 +690,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea21739e232c221779741eba7e7b9bc19ad8ff777b72736647ae519f5c9f6f33" +checksum = "0ab075ac1c25bcf697f133b7cd92e2fb26afe213e872ef79fdf77f0d7bcb3793" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -702,15 +703,15 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f05338cfb4ee5508ff76f01c88142cab8a4579db74b7d9432936c26e4f11374" +checksum = "73b12366c96f4013e1aeebc96c6b56e5f33f07853c42ea2f485045c0c157a4a1" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "ethereum_ssz", "ethereum_ssz_derive", @@ -722,17 +723,17 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda4ece0050154ab278241aeffade58916b04f38254832e8cb6e4671c6e72ed2" +checksum = "56a282daf869eeb7383d3d5c2deb35b0b3fb45ecb329513af4090fc61245ee18" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "itertools 0.14.0", "serde", @@ -743,13 +744,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5905ac3663b0859d67b82d912acce20887d20682a0cadde79c8a763b133a515" +checksum = "6184b5d14152b68b0bb8beb621339d94f0b761a37958bb365fbf7c00922125c2" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", "thiserror 2.0.18", @@ -757,13 +758,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fbf71892d4df9cae8d35dc96f15d522384bb93806205465e2c8c012b7f0a34" +checksum = "f00b631c361e7c7baaf4f1f5a9877730f3507fed2acb9d4b34841b8184b2ec28" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] @@ -780,9 +781,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beaa5c581a67e2743d95b4849eb9cfeb90866429cdaa6d8f6b75eb988b2d0cd9" +checksum = "a0eada2558e921b39dfcead33c487364df9b31374f5733c1c9d2c891c4529933" dependencies = [ "alloy-primitives", "serde", @@ -791,9 +792,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5da9ae50f9b48d7b4e2e5cde87175257be7e5e56909a7794720597c1d9806f6" +checksum = "41eb29f7a8adcd8941fbb8e134022a133e6f8dfd345f2e3b7109599f8a7dca08" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -808,9 +809,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a57d1e72b1f9b11e5e71ebdab0569cb02277a462bbea6793fcaebfcd794ae9" +checksum = "1258987fbc82716b5153ec7bb95a8a295e7640871b8f03d8ec7c4000dc80c215" dependencies = [ "alloy-consensus", "alloy-network", @@ -827,9 +828,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b27f20b5298b76a5a3b7cdbe6bdb184ab1ebd6e120e00dad748867673f5c90" +checksum = "7ffc2a49bca5b73c6964711b57452f6c36a6bcb7f845ab7e9ad05b5a828d0161" dependencies = [ "alloy-consensus", "alloy-network", @@ -845,9 +846,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c7acc40ffbfd37d4113eb619863099f3235d78d044006a1eecb94d8b0b2f1a" +checksum = "94e11ddaddfb98c1ddce737dc440225565b0ae0987ac9ad5e59a85db5904878c" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -865,9 +866,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b794002d57fd2f71b4c87298a41ca24dfc0f2cf6630d95106a477e451747ba" +checksum = "bef839e7ce9b59aa60fa9a175e97986c6145c888d643b0f1fb0a3e7b8e56a2e2" dependencies = [ "alloy-consensus", "alloy-network", @@ -885,9 +886,9 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a09a865ae9e1f05478429ef0d935b16467f35c6e0b02cb10f23f66a3b33fc3" +checksum = "44eb341d0013784da6a39e5bbdc11b95d6744993b12a1c3fd55df795a850dd42" dependencies = [ "alloy-consensus", "alloy-network", @@ -902,9 +903,9 @@ dependencies = [ [[package]] name = "alloy-signer-turnkey" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bb8218544ab635281f1be180a1cfd9b5d549db686faa7e85b3b2c10969819e" +checksum = "82ff16b4166fb90bbe79bd1e49244824fb3cadc6b8cd11e9c8a002c1f8c07492" dependencies = [ "alloy-consensus", "alloy-network", @@ -991,9 +992,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19dec9bfb59647254afdecbb5ddcddd7ba02edcd48ffa40510bddfbed0be1634" +checksum = "3ac7a80c0bac3e44559d53d002e34c461dc2f23262b42cafec019bc70551abbe" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -1014,14 +1015,14 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2035f3c4d6bee20624da2dcf765d469b292398e48d766ffade61b0fcf8b4d45d" +checksum = "eed3ed3300a998f88639ed619fdbbd88bd82865e00c6a8ecb796c99eb12358f6" dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "tower", "tracing", @@ -1030,9 +1031,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfad7aa9206fcb831ae401b6a1c893a402b8eed74f9c8ffbb7a7323afb0d9a4c" +checksum = "1075d9d30fd4d71e50000fd4afb19ed2664ceab20c2a29f3889a6e988329e02d" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -1050,9 +1051,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aa8ff49386df3e008b73c7fb0a5479410e8493fdb86a8b916877a16e8aead9" +checksum = "0e3bff84b2b2a46eb34cc522dc3f889a2867c70be90a377421429b662b3ec4ce" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1085,9 +1086,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520337f3d3d063a7fe20f47aaa62d695e3dc0372b34f601560dee24e76988b9" +checksum = "99fce0350197dcd4ba4e9a7dd43915d908c0eb0e7352755791709a705e1c76b6" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1223,13 +1224,13 @@ dependencies = [ [[package]] name = "anvil" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-network", @@ -1241,7 +1242,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-beacon", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -1276,7 +1277,7 @@ dependencies = [ "parking_lot", "rand 0.8.6", "rand 0.9.4", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -1297,17 +1298,17 @@ dependencies = [ [[package]] name = "anvil-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-eip5792", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "foundry-common", "foundry-evm", @@ -1321,7 +1322,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "serde", "serde_json", @@ -1329,7 +1330,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "1.6.0" +version = "1.7.1" dependencies = [ "anvil-rpc", "async-trait", @@ -1669,20 +1670,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ascii-canvas" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" -dependencies = [ - "term", -] - [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -1955,9 +1947,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.101.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -2172,9 +2164,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.14" +version = "1.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -2361,9 +2353,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -2579,7 +2571,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -2745,13 +2737,13 @@ dependencies = [ [[package]] name = "cast" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-evm", "alloy-hardforks", @@ -2763,7 +2755,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-beacon", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -2822,9 +2814,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -2832,12 +2824,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -2876,7 +2862,7 @@ dependencies = [ [[package]] name = "chisel" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -2890,10 +2876,9 @@ dependencies = [ "foundry-compilers", "foundry-config", "foundry-evm", - "foundry-solang-parser", "foundry-test-utils", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "rexpect", "rustyline", "semver 1.0.28", @@ -2997,9 +2982,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" dependencies = [ "clap", ] @@ -3042,7 +3027,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3195,7 +3180,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3364,9 +3349,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -3375,9 +3360,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -3425,9 +3410,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -3538,9 +3523,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" @@ -3821,9 +3806,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -3973,9 +3958,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", @@ -3999,7 +3984,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4187,15 +4172,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -4304,7 +4280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4502,15 +4478,15 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment2" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4380ce44915a6227efbb61e3885bc1c8e99fb9820f5db612abfac2c5cfc46871" +checksum = "87d63dee16df12076c7770919713c0b92f4e1c85eac828dc2ade0b6c998f016b" dependencies = [ "atomic", "parking_lot", "serde", "tempfile", - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.11+spec-1.1.0", "uncased", "version_check", ] @@ -4607,7 +4583,7 @@ checksum = "932dcfbd51320af5f27f1ba02d2e567dec332cac7d2c221ba45d8e767264c4dc" [[package]] name = "forge" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4661,7 +4637,7 @@ dependencies = [ "rand 0.9.4", "rayon", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4689,7 +4665,7 @@ dependencies = [ [[package]] name = "forge-doc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "derive_more", @@ -4711,7 +4687,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "foundry-common", "foundry-config", @@ -4725,7 +4701,7 @@ dependencies = [ [[package]] name = "forge-lint" -version = "1.6.0" +version = "1.7.1" dependencies = [ "eyre", "foundry-common", @@ -4739,12 +4715,12 @@ dependencies = [ [[package]] name = "forge-script" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-json-abi", "alloy-network", @@ -4787,7 +4763,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-network", "alloy-primitives", @@ -4803,7 +4779,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -4818,7 +4794,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4841,7 +4817,7 @@ dependencies = [ "futures", "itertools 0.14.0", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4864,7 +4840,7 @@ dependencies = [ [[package]] name = "foundry-bench" -version = "1.6.0" +version = "1.7.1" dependencies = [ "chrono", "clap", @@ -4889,7 +4865,7 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "foundry-compilers", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -4899,7 +4875,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4952,7 +4928,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4963,11 +4939,11 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-json-abi", "alloy-network", @@ -5007,6 +4983,7 @@ dependencies = [ "tempo-primitives", "tikv-jemallocator", "tokio", + "toml", "tracing", "tracing-subscriber 0.3.23", "tracing-tracy", @@ -5015,7 +4992,7 @@ dependencies = [ [[package]] name = "foundry-cli-markdown" -version = "1.6.0" +version = "1.7.1" dependencies = [ "clap", "pretty_assertions", @@ -5023,12 +5000,12 @@ dependencies = [ [[package]] name = "foundry-common" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-abi", "alloy-json-rpc", "alloy-network", @@ -5040,6 +5017,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-engine", "alloy-signer", + "alloy-signer-local", "alloy-sol-types", "alloy-transport", "alloy-transport-ipc", @@ -5047,6 +5025,7 @@ dependencies = [ "anstream 0.6.21", "anstyle", "axum", + "base64 0.22.1", "chrono", "ciborium", "clap", @@ -5064,18 +5043,20 @@ dependencies = [ "futures", "itertools 0.14.0", "jiff", + "k256", "mpp", "num-format", "op-alloy-network", "op-alloy-rpc-types", "path-slash", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "rustls", "semver 1.0.28", "serde", "serde_json", + "sha2 0.10.9", "solar-compiler", "tempfile", "tempo-alloy", @@ -5094,14 +5075,14 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", "alloy-primitives", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "chrono", "comfy-table", "eyre", @@ -5217,7 +5198,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5257,7 +5238,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "crossterm", @@ -5275,7 +5256,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5312,7 +5293,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -5324,7 +5305,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -5339,7 +5320,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "anvil", "auto_impl", @@ -5376,7 +5357,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "eyre", @@ -5392,7 +5373,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5417,7 +5398,7 @@ dependencies = [ [[package]] name = "foundry-evm-hardforks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -5432,10 +5413,10 @@ dependencies = [ [[package]] name = "foundry-evm-networks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -5448,11 +5429,11 @@ dependencies = [ [[package]] name = "foundry-evm-sancov" -version = "1.6.0" +version = "1.7.1" [[package]] name = "foundry-evm-traces" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5470,7 +5451,7 @@ dependencies = [ "itertools 0.14.0", "memchr", "rayon", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -5509,7 +5490,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -5520,7 +5501,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "1.6.0" +version = "1.7.1" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -5530,7 +5511,7 @@ dependencies = [ [[package]] name = "foundry-primitives" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-evm", @@ -5541,7 +5522,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "derive_more", "op-alloy-consensus", @@ -5555,23 +5536,9 @@ dependencies = [ "tempo-revm", ] -[[package]] -name = "foundry-solang-parser" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9645e75b89f977423690f3b4bfd8d84825e5fdabd7803cbce6d4a2c4d54972b4" -dependencies = [ - "itertools 0.14.0", - "lalrpop", - "lalrpop-util", - "phf 0.11.3", - "thiserror 2.0.18", - "unicode-xid", -] - [[package]] name = "foundry-test-utils" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5586,7 +5553,7 @@ dependencies = [ "parking_lot", "rand 0.9.4", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "snapbox", "svm-rs", @@ -5814,7 +5781,7 @@ dependencies = [ "once_cell", "prost 0.14.3", "prost-types 0.14.3", - "reqwest 0.13.2", + "reqwest 0.13.3", "secret-vault-value", "serde", "serde_json", @@ -6188,9 +6155,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -6582,9 +6549,9 @@ dependencies = [ [[package]] name = "interprocess" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be5e5c847dbdb44564bd85294740d031f4f8aeb3464e5375ef7141f7538db69" +checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b" dependencies = [ "doctest-file", "futures-core", @@ -6592,7 +6559,7 @@ dependencies = [ "recvmsg", "tokio", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6641,7 +6608,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6694,9 +6661,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -6707,31 +6674,15 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -6741,7 +6692,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -6762,15 +6713,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -6802,9 +6744,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -6841,6 +6783,7 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", "getrandom 0.2.17", "js-sys", @@ -6923,45 +6866,14 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] -[[package]] -name = "lalrpop" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.14.0", - "lalrpop-util", - "petgraph", - "regex", - "regex-syntax", - "sha3", - "string_cache 0.8.9", - "term", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" -dependencies = [ - "regex-automata", - "rustversion", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -6982,9 +6894,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -6994,12 +6906,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" dependencies = [ "cc", - "libc", ] [[package]] @@ -7299,12 +7210,12 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] @@ -7331,9 +7242,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" dependencies = [ "libmimalloc-sys", ] @@ -7588,7 +7499,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7809,7 +7720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b685c8311c9171d1bd2895222965d25616b2de2cb5819dd3504ed9250df9fecd" dependencies = [ "ahash", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "parking_lot", "stable_deref_trait", ] @@ -7817,7 +7728,7 @@ dependencies = [ [[package]] name = "op-alloy" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "op-alloy-consensus", "op-alloy-network", @@ -7829,15 +7740,15 @@ dependencies = [ [[package]] name = "op-alloy-consensus" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "derive_more", "reth-codecs", @@ -7856,7 +7767,7 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", "alloy-network", @@ -7869,7 +7780,7 @@ dependencies = [ [[package]] name = "op-alloy-provider" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-network", "alloy-primitives", @@ -7883,15 +7794,15 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "op-alloy-consensus", "reth-rpc-traits", @@ -7903,15 +7814,14 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-engine", - "alloy-serde 2.0.1", - "derive_more", + "alloy-serde 2.0.4", "ethereum_ssz", "ethereum_ssz_derive", "op-alloy-consensus", @@ -7924,7 +7834,7 @@ dependencies = [ [[package]] name = "op-revm" version = "19.0.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "auto_impl", "revm", @@ -8142,16 +8052,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.14.0", -] - [[package]] name = "pharos" version = "0.5.3" @@ -8168,7 +8068,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -8178,7 +8077,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", + "phf_macros", "phf_shared 0.13.1", "serde", ] @@ -8223,19 +8122,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "phf_macros" version = "0.13.1" @@ -8571,7 +8457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8740,7 +8626,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -9091,9 +8977,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -9135,12 +9021,12 @@ dependencies = [ [[package]] name = "reth-chainspec" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-primitives", @@ -9155,11 +9041,12 @@ dependencies = [ [[package]] name = "reth-codecs" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce542a96bf888f31854803e80b3340bc233927743aa580838014e8a88fe0d66" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-trie", @@ -9173,8 +9060,9 @@ dependencies = [ [[package]] name = "reth-codecs-derive" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634c90f1cc0f9887680ca785b0b21aa961070b9465917bf65afaec56a6d005bb" dependencies = [ "proc-macro2", "quote", @@ -9183,8 +9071,8 @@ dependencies = [ [[package]] name = "reth-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9196,11 +9084,11 @@ dependencies = [ [[package]] name = "reth-consensus-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9209,8 +9097,8 @@ dependencies = [ [[package]] name = "reth-db-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9233,10 +9121,10 @@ dependencies = [ [[package]] name = "reth-db-models" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "bytes", "modular-bitfield", @@ -9247,11 +9135,11 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9263,8 +9151,8 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -9276,11 +9164,11 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-eth", "reth-codecs", @@ -9290,11 +9178,11 @@ dependencies = [ [[package]] name = "reth-evm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "auto_impl", @@ -9312,11 +9200,11 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rpc-types-engine", @@ -9332,8 +9220,8 @@ dependencies = [ [[package]] name = "reth-execution-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-evm", "alloy-primitives", @@ -9345,11 +9233,11 @@ dependencies = [ [[package]] name = "reth-execution-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rlp", @@ -9364,8 +9252,8 @@ dependencies = [ [[package]] name = "reth-network-peers" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9377,11 +9265,12 @@ dependencies = [ [[package]] name = "reth-primitives-traits" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee12e304adbacbb32248c9806ebafbe1e2811fbfefe53c5e5b710a8438b7ec0" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-rlp", @@ -9405,8 +9294,8 @@ dependencies = [ [[package]] name = "reth-prune-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9420,8 +9309,8 @@ dependencies = [ [[package]] name = "reth-revm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9433,8 +9322,8 @@ dependencies = [ [[package]] name = "reth-rpc-convert" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9453,9 +9342,9 @@ dependencies = [ [[package]] name = "reth-rpc-traits" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b766da61ec7c46596386b4bc88d9b57d1939d3da2bc9e927567a8a23650e5ce9" +checksum = "860fe223501a76ff14aa3bf164f739f31008c2a2905ac85708bfd88f042e6151" dependencies = [ "alloy-consensus", "alloy-network", @@ -9468,8 +9357,8 @@ dependencies = [ [[package]] name = "reth-stages-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "bytes", @@ -9481,8 +9370,8 @@ dependencies = [ [[package]] name = "reth-static-file-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9495,11 +9384,11 @@ dependencies = [ [[package]] name = "reth-storage-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "auto_impl", @@ -9518,10 +9407,10 @@ dependencies = [ [[package]] name = "reth-storage-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "derive_more", @@ -9536,14 +9425,14 @@ dependencies = [ [[package]] name = "reth-trie-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "arrayvec", "bytes", @@ -9559,8 +9448,9 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12fafa33d2f420a9d39249a3e0357b1928d09429f30758b85280409092873b2" dependencies = [ "zstd", ] @@ -9843,9 +9733,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -9853,20 +9743,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.4.0" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rtoolbox" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327b72899159dfae8060c51a1f6aebe955245bcd9cc4997eed0f623caea022e4" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", "windows-sys 0.59.0", @@ -9874,9 +9764,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" dependencies = [ "alloy-rlp", "arbitrary", @@ -10001,14 +9891,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -10034,9 +9924,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -10044,13 +9934,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -10060,7 +9950,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10478,9 +10368,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", @@ -10497,9 +10387,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -10560,14 +10450,14 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -10701,9 +10591,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -10842,7 +10732,7 @@ dependencies = [ "derive_more", "dunce", "inturn", - "itertools 0.12.1", + "itertools 0.14.0", "itoa", "normalize-path", "once_map", @@ -10877,7 +10767,7 @@ dependencies = [ "alloy-primitives", "bitflags 2.11.1", "bumpalo", - "itertools 0.12.1", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", @@ -11011,18 +10901,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -11181,7 +11059,7 @@ checksum = "4572dd9845e37ca0293acb5fe591a7f61b51f1b7b62d3dc6fb8e99e2664f3755" dependencies = [ "const-hex", "dirs", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -11301,22 +11179,22 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tempo-alloy" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-contract", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer-local", "alloy-sol-types", "alloy-transport", @@ -11334,9 +11212,9 @@ dependencies = [ [[package]] name = "tempo-chainspec" version = "1.5.3" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-hardforks", @@ -11353,7 +11231,7 @@ dependencies = [ [[package]] name = "tempo-consensus" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11371,7 +11249,7 @@ dependencies = [ [[package]] name = "tempo-contracts" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-contract", "alloy-primitives", @@ -11382,7 +11260,7 @@ dependencies = [ [[package]] name = "tempo-evm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11409,7 +11287,7 @@ dependencies = [ [[package]] name = "tempo-precompiles" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "alloy-evm", @@ -11429,7 +11307,7 @@ dependencies = [ [[package]] name = "tempo-precompiles-macros" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "proc-macro2", @@ -11440,15 +11318,15 @@ dependencies = [ [[package]] name = "tempo-primitives" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "aws-lc-rs", "base64 0.22.1", "derive_more", @@ -11471,7 +11349,7 @@ dependencies = [ [[package]] name = "tempo-revm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11502,15 +11380,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "term" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "terminal_size" version = "0.4.4" @@ -11518,7 +11387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11552,9 +11421,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" [[package]] name = "thiserror" @@ -11695,9 +11564,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -11864,9 +11733,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap 2.14.0", + "serde_core", + "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -11875,7 +11746,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -12220,9 +12091,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -12506,9 +12377,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174a690eb3293a5666442b0738d080df9ea6b9e03782bbe78875c89ff914a77c" +checksum = "2d7cb4a83971db3f6ae36f0aa41eaf5985d2e2b469581fa755c132f9c2a1ec89" dependencies = [ "anyhow", "bon", @@ -12519,9 +12390,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f628f4acc90a5c1a8136495eaf5f9ef94e03c174d6fb2e6de691bc58fc721ee" +checksum = "bba14c9676943b2899cea2ed7ea194b89b3d13564a3c93a61882a978b123a41c" dependencies = [ "anyhow", "bon", @@ -12533,9 +12404,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390d0442b660baedd7a6f60d2af01bd8967e0d7fe69cd15e13c82c811d82b709" +checksum = "fb684e6d170ef15a9b3c20561779a50ba8c806f8acdaff47c0a2c5c4c6cadd43" dependencies = [ "anyhow", "bon", @@ -12600,11 +12471,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -12613,14 +12484,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -12631,9 +12502,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -12641,9 +12512,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12651,9 +12522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -12664,9 +12535,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -12764,7 +12635,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -12804,9 +12675,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -12824,13 +12695,13 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", - "string_cache 0.9.0", + "string_cache", "string_cache_codegen", ] @@ -12841,7 +12712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni 0.22.4", + "jni", "log", "ndk-context", "objc2", @@ -12914,7 +12785,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13035,15 +12906,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -13080,21 +12942,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -13137,12 +12984,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -13155,12 +12996,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -13173,12 +13008,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -13203,12 +13032,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -13221,12 +13044,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -13239,12 +13056,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -13257,12 +13068,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -13286,9 +13091,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -13302,6 +13107,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index c412dad16366c..4db027dd400cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.6.0" +version = "1.7.1" edition = "2024" rust-version = "1.89" authors = ["Foundry Contributors"] @@ -174,9 +174,6 @@ foundry-compilers.opt-level = 3 serde_json.opt-level = 3 serde.opt-level = 3 -foundry-solang-parser.opt-level = 3 -lalrpop-util.opt-level = 3 - solar-compiler.opt-level = 3 solar-ast.opt-level = 3 solar-data-structures.opt-level = 3 @@ -307,40 +304,40 @@ crossterm.opt-level = "s" alloy-json-abi.opt-level = "s" [workspace.dependencies] -anvil = { path = "crates/anvil" } -cast = { path = "crates/cast" } -chisel = { path = "crates/chisel" } -forge = { path = "crates/forge" } - -forge-doc = { path = "crates/doc" } -forge-fmt = { path = "crates/fmt" } -forge-lint = { path = "crates/lint" } -forge-verify = { path = "crates/verify" } -forge-script = { path = "crates/script" } -forge-sol-macro-gen = { path = "crates/sol-macro-gen" } -forge-script-sequence = { path = "crates/script-sequence" } -foundry-cheatcodes = { path = "crates/cheatcodes" } +anvil = { path = "crates/anvil", default-features = false } +cast = { path = "crates/cast", default-features = false } +chisel = { path = "crates/chisel", default-features = false } +forge = { path = "crates/forge", default-features = false } + +forge-doc = { path = "crates/doc", default-features = false } +forge-fmt = { path = "crates/fmt", default-features = false } +forge-lint = { path = "crates/lint", default-features = false } +forge-verify = { path = "crates/verify", default-features = false } +forge-script = { path = "crates/script", default-features = false } +forge-sol-macro-gen = { path = "crates/sol-macro-gen", default-features = false } +forge-script-sequence = { path = "crates/script-sequence", default-features = false } +foundry-cheatcodes = { path = "crates/cheatcodes", default-features = false } foundry-cheatcodes-spec = { path = "crates/cheatcodes/spec" } -foundry-cli = { path = "crates/cli" } +foundry-cli = { path = "crates/cli", default-features = false } foundry-cli-markdown = { path = "crates/cli-markdown" } -foundry-common = { path = "crates/common" } -foundry-common-fmt = { path = "crates/common/fmt" } +foundry-common = { path = "crates/common", default-features = false } +foundry-common-fmt = { path = "crates/common/fmt", default-features = false } foundry-config = { path = "crates/config" } -foundry-debugger = { path = "crates/debugger" } -foundry-evm = { path = "crates/evm/evm" } +foundry-debugger = { path = "crates/debugger", default-features = false } +foundry-evm = { path = "crates/evm/evm", default-features = false } foundry-evm-abi = { path = "crates/evm/abi" } -foundry-evm-core = { path = "crates/evm/core" } -foundry-evm-coverage = { path = "crates/evm/coverage" } -foundry-evm-hardforks = { path = "crates/evm/hardforks" } -foundry-evm-networks = { path = "crates/evm/networks" } -foundry-evm-fuzz = { path = "crates/evm/fuzz" } +foundry-evm-core = { path = "crates/evm/core", default-features = false } +foundry-evm-coverage = { path = "crates/evm/coverage", default-features = false } +foundry-evm-hardforks = { path = "crates/evm/hardforks", default-features = false } +foundry-evm-networks = { path = "crates/evm/networks", default-features = false } +foundry-evm-fuzz = { path = "crates/evm/fuzz", default-features = false } foundry-evm-sancov = { path = "crates/evm/sancov" } -foundry-evm-traces = { path = "crates/evm/traces" } +foundry-evm-traces = { path = "crates/evm/traces", default-features = false } foundry-macros = { path = "crates/macros" } -foundry-test-utils = { path = "crates/test-utils" } +foundry-test-utils = { path = "crates/test-utils", default-features = false } foundry-wallets = { version = "0.1.0", default-features = false } foundry-linking = { path = "crates/linking" } -foundry-primitives = { path = "crates/primitives" } +foundry-primitives = { path = "crates/primitives", default-features = false } # solc & compilation utilities foundry-block-explorers = { version = "0.23.0", default-features = false } @@ -349,7 +346,6 @@ foundry-compilers = { version = "0.20.0", default-features = false, features = [ "svm-solc", ] } foundry-fork-db = { version = "0.26.0", features = ["zstd"] } -solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar = { package = "solar-compiler", version = "=0.1.8", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ "rustls", @@ -410,7 +406,7 @@ op-alloy-rpc-types = "0.24.0" op-alloy-flz = "0.13.1" ## alloy-evm -alloy-evm = "0.33.2" +alloy-evm = "0.34.0" alloy-op-evm = "0.31.0" # revm @@ -515,17 +511,17 @@ mpp = { git = "https://github.com/tempoxyz/mpp-rs", rev = "554d20112eb014bd223d5 "reqwest-rustls-tls", "ws", ] } -tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false, features = [ +tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false, features = [ "serde", ] } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false, features = [ +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false, features = [ "serde", ] } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } ## Pinned dependencies. Enabled for the workspace in crates/test-utils. @@ -589,27 +585,21 @@ rexpect = { git = "https://github.com/rust-cli/rexpect", rev = "2ed0b1898d7edaf6 ## alloy-evm # alloy-evm = { git = "https://github.com/paradigmxyz/evm.git", rev = "04d8e4a" } -## reth-core -reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } - ## op-revm / op-alloy / alloy-op-evm -op-revm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +op-revm = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } ## foundry-fork-db # foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "2f90eb86d4549fa15a8cc2d99bfc1039bc083977" } ## tempo — unify crates.io versions (pulled by mpp) with git rev -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } # solar solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } diff --git a/README.md b/README.md index c9f0a45c57b0a..90cd1d8a7865e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ foundryup See the [installation guide](https://getfoundry.sh/getting-started/installation) for more details. +To verify a downloaded release archive or container image, see [Verifying Releases](./SECURITY.md#verifying-releases). + ## Getting Started Initialize a new project, build and test: diff --git a/SECURITY.md b/SECURITY.md index d84327cc18e91..6296066db5e73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,3 +3,112 @@ ## Reporting a Vulnerability Contact [security@tempo.xyz](mailto:security@tempo.xyz). + +## Verifying Releases + +Every official Foundry release ships with multiple, independent integrity +artifacts. All signing is keyless via [Sigstore](https://www.sigstore.dev/) — +no Foundry-managed key material is involved, and every signature is recorded +in the public [Rekor](https://docs.sigstore.dev/logging/overview/) transparency +log. The signing identity is the GitHub Actions OIDC token of this repository's +`release.yml` / `docker-publish.yml` workflows. + +### Per-release artifacts + +For each `foundry___.{tar.gz,zip}` archive on the +[releases page](https://github.com/foundry-rs/foundry/releases), the same +release also publishes: + +| Suffix | Purpose | +| --- | --- | +| `.sha256` | SHA-256 checksum of the archive (`sha256sum` format) | +| `.sigstore.json` | Cosign keyless signature bundle (cert + signature + Rekor proof) over the archive | +| `.spdx.json` | SPDX 2.3 SBOM of the source workspace used for the build | +| `.attestation.txt` | URL of the GitHub artifact-attestation summary | + +In addition, GitHub stores SLSA build-provenance and SBOM attestations against +the archive's digest; these are queryable via `gh attestation` without +downloading anything else. + +### Verifying an archive + +Pick whichever toolchain you have available — they verify the same signatures. + +#### Option 1: GitHub CLI (simplest) + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry +``` + +This computes the file's digest, fetches the matching attestation from GitHub, +and verifies the Sigstore signature plus the SLSA provenance predicate. Add +`--signer-workflow foundry-rs/foundry/.github/workflows/release.yml` to also +require the workflow identity. + +To verify the SBOM attestation specifically: + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry \ + --predicate-type 'https://spdx.dev/Document/v2.3' +``` + +#### Option 2: Cosign (offline-friendly) + +Download the archive and its `.sigstore.json` bundle from the release page, +then: + +```bash +cosign verify-blob \ + --bundle foundry_v1.4.0_linux_amd64.sigstore.json \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/release\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + foundry_v1.4.0_linux_amd64.tar.gz +``` + +For nightly builds the certificate identity points at `refs/heads/master` +instead of a tag; the regex above matches both. + +#### Option 3: Plain checksum (integrity only) + +```bash +sha256sum -c foundry_v1.4.0_linux_amd64.sha256 # GNU coreutils +shasum -a 256 -c foundry_v1.4.0_linux_amd64.sha256 # macOS +``` + +This proves the bytes match what was uploaded, but says nothing about who +uploaded them. Combine with one of the verifications above for end-to-end +trust. + +### Verifying the Docker image + +Container signatures and attestations are pushed as OCI referrers to GHCR, so +no separate files need to be downloaded. + +```bash +# Cosign keyless signature on the image +cosign verify ghcr.io/foundry-rs/foundry:v1.4.0 \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' + +# SLSA build-provenance attestation +gh attestation verify oci://ghcr.io/foundry-rs/foundry:v1.4.0 \ + --repo foundry-rs/foundry + +# Inspect the buildx-attached SBOM and provenance +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .SBOM }}' +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .Provenance }}' +``` + +To pin to an immutable digest (recommended for reproducible deployments): + +```bash +docker pull ghcr.io/foundry-rs/foundry:v1.4.0 +DIGEST=$(docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 --format '{{ .Manifest.Digest }}') +cosign verify "ghcr.io/foundry-rs/foundry@${DIGEST}" \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' +``` diff --git a/benches/LATEST.md b/benches/LATEST.md index 238a691229389..cb75f8d68780b 100644 --- a/benches/LATEST.md +++ b/benches/LATEST.md @@ -1,74 +1,108 @@ -# Foundry Benchmark Results +# 📊 Foundry Benchmark Results -**Date**: 2026-04-24 23:10:24 +**Generated at**: 2026-05-02 21:53:46 UTC -## Repositories Tested +## Forge Test + +### Repositories Tested 1. [ithacaxyz/account](https://github.com/ithacaxyz/account) -2. [Vectorized/solady](https://github.com/Vectorized/solady) -3. [Uniswap/v4-core](https://github.com/Uniswap/v4-core) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) 4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) -5. [aave/aave-v4](https://github.com/aave/aave-v4) - -## Foundry Versions +### Foundry Versions - **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) -- **nightly**: forge Version: 1.6.0-nightly (a249f5c 2026-04-24) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) -## Forge Test - -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| vectorized-solady | 1.46 s | 1.38 s | -| aave-aave-v4 | 4m 14.2s | 3m 29.1s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.78 s | 0.965 s | +| vectorized-solady | 0.995 s | 0.645 s | +| uniswap-v4-core | 5.97 s | 1.51 s | +| sparkdotfi-spark-psm | 19.98 s | 10.20 s | ## Forge Fuzz Test -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | -------- | -| ithacaxyz-account | 2.81 s | 1.59 s | -| vectorized-solady | 1.40 s | 1.34 s | -| Uniswap-v4-core | 3.01 s | 2.87 s | -| sparkdotfi-spark-psm | 2.04 s | 1.87 s | -| aave-aave-v4 | 3m 46.0s | 3m 17.3s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.54 s | 0.923 s | +| vectorized-solady | 0.929 s | 0.617 s | +| uniswap-v4-core | 6.44 s | 1.40 s | +| sparkdotfi-spark-psm | 2.25 s | 2.03 s | ## Forge Test (Isolated) -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| Uniswap-v4-core | 3.50 s | 3.48 s. | -| aave-aave-v4 | 4m 14.0s | 3m 53.4s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 3.05 s | 1.02 s | +| vectorized-solady | 0.871 s | 0.741 s | +| uniswap-v4-core | 6.81 s | 1.68 s | +| sparkdotfi-spark-psm | 21.96 s | 11.26 s | + +## Forge Build -## Forge Build (No Cache) +### Repositories Tested -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| ithacaxyz-account | 26.06 s | 26.61 s | -| vectorized-solady | 14.20 s | 14.26 s | -| Uniswap-v4-core | 2m 1.3s | 2m 5.0s | -| sparkdotfi-spark-psm | 15.16 s | 15.30 s | -| aave-aave-v4 | 3m 37.0s | 3m 35.1s | +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +### No Cache + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 34.58 s | 33.29 s | +| vectorized-solady | 14.40 s | 14.41 s | +| uniswap-v4-core | 2m 17.6s | 2m 17.7s | +| sparkdotfi-spark-psm | 12.62 s | 12.61 s | -## Forge Build (With Cache) +### With Cache -| Repository | v1.5.1 | nightly | -| -------------------- | ------- | ------- | -| ithacaxyz-account | 0.167 s | 0.201 s | -| vectorized-solady | 0.099 s | 0.098 s | -| Uniswap-v4-core | 0.139 s | 0.140 s | -| sparkdotfi-spark-psm | 0.168 s | 0.173 s | -| aave-aave-v4 | 0.370 s | 0.357 s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 0.083 s | 0.089 s | +| vectorized-solady | 0.062 s | 0.064 s | +| uniswap-v4-core | 0.071 s | 0.074 s | +| sparkdotfi-spark-psm | 0.066 s | 0.068 s | ## Forge Coverage -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | ---------- | -| Uniswap-v4-core | 1m 13.9s | 1m 10.3s | -| sparkdotfi-spark-psm | 2m 54.7s | 2m 50.0s | -| aave-aave-v4 | 11m 20.8s | 10m 58.7s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [uniswap/v4-core](https://github.com/uniswap/v4-core) +3. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 29.35 s | 18.69 s | +| uniswap-v4-core | 1m 26.8s | 1m 4.1s | +| sparkdotfi-spark-psm | 2m 1.6s | 1m 28.4s | ## System Information -- **OS**: macos -- **CPU**: 12 -- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) \ No newline at end of file + +- **OS**: linux +- **CPU**: 32 +- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) diff --git a/benches/src/main.rs b/benches/src/main.rs index 60e815cecb0ec..8d7134b1c25bc 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -39,9 +39,15 @@ struct Cli { #[clap(long, default_value = ".")] output_dir: PathBuf, - /// Name of the output file (default: LATEST.md) - #[clap(long, default_value = "LATEST.md")] - output_file: String, + /// Name of the output file. Defaults to LATEST.md unless --json-output is set + /// without this flag, in which case no Markdown is written. + #[clap(long)] + output_file: Option, + + /// Filename for a flat JSON summary (benchmark/repo -> mean_seconds). + /// Resolved relative to --output-dir. Used by the nightly regression comparison script. + #[clap(long)] + json_output: Option, /// Run only specific benchmarks (comma-separated: /// forge_test,forge_build_no_cache,forge_build_with_cache,forge_fuzz_test,forge_coverage) @@ -216,12 +222,28 @@ fn main() -> Result<()> { } } - // Generate markdown report - sh_println!("📝 Generating report..."); - let markdown = results.generate_markdown(&versions, &repos); - let output_path = cli.output_dir.join(cli.output_file); - fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; - sh_println!("✅ Report written to: {}", output_path.display()); + // Write Markdown report unless --json-output is set without an explicit --output-file. + let md_filename = match cli.output_file { + Some(f) => Some(f), + None if cli.json_output.is_none() => Some("LATEST.md".to_string()), + None => None, + }; + if let Some(filename) = md_filename { + sh_println!("📝 Generating report..."); + let markdown = results.generate_markdown(&versions, &repos); + let output_path = cli.output_dir.join(filename); + fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; + sh_println!("✅ Report written to: {}", output_path.display()); + } + + if let Some(json_filename) = cli.json_output { + let summary = results.generate_json_summary(&versions); + let json = + serde_json::to_string_pretty(&summary).wrap_err("Failed to serialize JSON summary")?; + let json_path = cli.output_dir.join(json_filename); + fs::write(&json_path, json).wrap_err("Failed to write JSON summary")?; + sh_println!("✅ JSON summary written to: {}", json_path.display()); + } Ok(()) } diff --git a/benches/src/results.rs b/benches/src/results.rs index 447e8ed2766b4..e7d57250fc9a1 100644 --- a/benches/src/results.rs +++ b/benches/src/results.rs @@ -66,6 +66,25 @@ impl BenchmarkResults { self.version_details.insert(version.to_string(), details); } + /// Generate a flat JSON summary mapping `"benchmark/repo" -> mean_seconds`. + /// + /// Used by the nightly regression comparison script. + pub fn generate_json_summary(&self, versions: &[String]) -> HashMap { + let mut summary = HashMap::new(); + for (benchmark_name, version_data) in &self.data { + for version in versions { + if let Some(repo_data) = version_data.get(version) { + for (repo_name, result) in repo_data { + let key = format!("{benchmark_name}/{repo_name}"); + let rounded = (result.mean * 10_000.0).round() / 10_000.0; + summary.insert(key, rounded); + } + } + } + } + summary + } + pub fn generate_markdown(&self, versions: &[String], repos: &[RepoConfig]) -> String { let mut output = String::new(); diff --git a/benchmark.sh b/benchmark.sh index 2faffa93dfa1d..ac6159099069b 100755 --- a/benchmark.sh +++ b/benchmark.sh @@ -1,36 +1,52 @@ #!/bin/bash -versions="v1.3.6,v1.4.0-rc1" +versions="v1.5.1,v1.7.0" # Repositories -export ITHACA_ACCOUNT="ithacaxyz/account:v0.3.2" -export SOLADY_REPO="Vectorized/solady:v0.1.22" -export UNISWAP_V4_CORE="Uniswap/v4-core:59d3ecf" -export SPARK_PSM="sparkdotfi/spark-psm:v1.0.0" +ITHACA_ACCOUNT="ithacaxyz/account:v0.5.7" +SOLADY_REPO="vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test'" +AAVE_V4="aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" +UNISWAP_V4_CORE="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc TickMathTestTest" +SPARK_PSM="sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting" -# Benches -echo "===========FORGE TEST AND BUILD BENCHMARKS===========" +SOLADY_ISOLATE="vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest'" +ITHACA_ISOLATE="ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_test,forge_fuzz_test,forge_build_no_cache,forge_build_with_cache \ - --output-dir ./benches/results \ - --output-file TEST_BUILD.md +SOLADY_BUILD="vectorized/solady:v0.1.26" +UNISWAP_BUILD="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75" +SPARK_PSM_BUILD="sparkdotfi/spark-psm:v1.0.0" -echo "===========FORGE COVERAGE BENCHMARKS===========" +# Benches +echo "===========FORGE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_coverage \ - --output-dir ./benches/results \ - --output-file COVERAGE.md +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_REPO,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ + --benchmarks forge_test,forge_fuzz_test \ + --output-dir ./benches \ + --output-file forge_test_bench.md echo "===========FORGE ISOLATE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ISOLATE,$SOLADY_ISOLATE,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ --benchmarks forge_isolate_test \ - --output-dir ./benches/results \ - --output-file ISOLATE_TEST.md + --output-dir ./benches \ + --output-file forge_isolate_test_bench.md + +echo "===========FORGE BUILD BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_BUILD,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --output-dir ./benches \ + --output-file forge_build_bench.md + +echo "===========FORGE COVERAGE BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_coverage \ + --output-dir ./benches \ + --output-file forge_coverage_bench.md echo "===========BENCHMARKS COMPLETED===========" diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index b664266450d07..d6404b21e2e8e 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -20,15 +20,15 @@ required-features = ["cli"] [dependencies] # foundry internal -anvil-core = { path = "core" } +anvil-core = { path = "core", default-features = false } anvil-rpc = { path = "rpc" } anvil-server = { path = "server" } -foundry-cli.workspace = true +foundry-cli = { workspace = true, optional = true } foundry-common.workspace = true foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-networks.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } tempo-chainspec.workspace = true tempo-primitives.workspace = true tempo-precompiles.workspace = true @@ -37,7 +37,7 @@ tempo-revm.workspace = true # alloy alloy-evm = { workspace = true, features = ["call-util"] } -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = ["serde"] } alloy-consensus = { workspace = true, features = ["k256", "kzg"] } alloy-contract = { workspace = true, features = ["pubsub"] } @@ -63,7 +63,8 @@ alloy-transport.workspace = true alloy-chains.workspace = true alloy-genesis.workspace = true alloy-trie.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde"] } +op-alloy-consensus = { workspace = true, features = ["serde"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } # revm revm = { workspace = true, features = [ @@ -73,7 +74,7 @@ revm = { workspace = true, features = [ "c-kzg", ] } revm-inspectors.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } # axum related axum.workspace = true @@ -120,17 +121,28 @@ reqwest.workspace = true foundry-test-utils.workspace = true tokio = { workspace = true, features = ["full"] } -op-alloy-rpc-types.workspace = true tempo-alloy.workspace = true [features] -default = ["cli", "jemalloc", "asm-keccak"] +default = ["cli", "jemalloc", "asm-keccak", "optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "anvil-core/optimism", + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli?/optimism", +] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] -jemalloc = ["foundry-cli/jemalloc"] -mimalloc = ["foundry-cli/mimalloc"] -tracy-allocator = ["foundry-cli/tracy-allocator"] +jemalloc = ["foundry-cli?/jemalloc"] +mimalloc = ["foundry-cli?/mimalloc"] +tracy-allocator = ["foundry-cli?/tracy-allocator"] cli = ["tokio/full", "cmd"] cmd = [ + "dep:foundry-cli", "clap", "clap_complete", "dep:fdlimit", diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index cf4b952ecfaa3..8456413a78b1f 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [dependencies] foundry-common.workspace = true foundry-evm.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } revm = { workspace = true, default-features = false, features = [ "std", "serde", @@ -39,3 +39,11 @@ bytes.workspace = true # misc rand.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index fb75529b82908..4fd2aabb82a8b 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -12,7 +12,9 @@ use clap::Parser; use core::fmt; use foundry_common::shell; use foundry_config::{Chain, Config, FigmentProviders}; -use foundry_evm::hardfork::{EthereumHardfork, OpHardfork}; +#[cfg(feature = "optimism")] +use foundry_evm::hardfork::OpHardfork; +use foundry_evm::hardfork::{EthereumHardfork, FoundryHardfork}; use foundry_evm_networks::NetworkConfigs; use foundry_primitives::FoundryReceiptEnvelope; use futures::FutureExt; @@ -240,15 +242,7 @@ impl NodeArgs { } let hardfork = match &self.hardfork { - Some(hf) => { - if self.evm.networks.is_optimism() { - Some(OpHardfork::from_str(hf)?.into()) - } else if self.evm.networks.is_tempo() { - Some(TempoHardfork::from_str(hf)?.into()) - } else { - Some(EthereumHardfork::from_str(hf)?.into()) - } - } + Some(hf) => Some(parse_hardfork(hf, &self.evm.networks)?), None => None, }; @@ -849,6 +843,19 @@ impl FromStr for ForkUrl { } } +/// Parses a hardfork string against the active network configuration. +fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result { + #[cfg(feature = "optimism")] + if networks.is_optimism() { + return Ok(OpHardfork::from_str(hf)?.into()); + } + if networks.is_tempo() { + Ok(TempoHardfork::from_str(hf)?.into()) + } else { + Ok(EthereumHardfork::from_str(hf)?.into()) + } +} + /// Clap's value parser for genesis. Loads a genesis.json file. fn read_genesis_file(path: &str) -> Result { foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string()) diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 23cd6e61bc076..7c91590c071fa 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -37,7 +37,7 @@ use foundry_config::Config; use foundry_evm::{ backend::{BlockchainDb, BlockchainDbMeta, SharedBackend}, constants::DEFAULT_CREATE2_DEPLOYER, - hardfork::{FoundryHardfork, OpHardfork}, + hardfork::FoundryHardfork, utils::{ apply_chain_and_block_specific_env_changes, block_env_from_header, get_blob_base_fee_update_fraction, @@ -577,8 +577,9 @@ impl NodeConfig { if let Some(hardfork) = self.hardfork { return hardfork; } + #[cfg(feature = "optimism")] if self.networks.is_optimism() { - return OpHardfork::default().into(); + return foundry_evm::hardforks::OpHardfork::default().into(); } if self.networks.is_tempo() { return TempoHardfork::default().into(); @@ -1079,6 +1080,7 @@ impl NodeConfig { } /// Enable Optimism network features. + #[cfg(feature = "optimism")] #[must_use] pub fn with_optimism(mut self) -> Self { self.networks = NetworkConfigs::with_optimism(); diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 78cce938318c1..4900c18eebd82 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -1882,6 +1882,7 @@ impl EthApi { fn sign_request(&self, from: &Address, typed_tx: FoundryTypedTx) -> Result { match typed_tx { + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => return Ok(build_impersonated(typed_tx)), _ => { for signer in self.signers.iter() { @@ -2210,9 +2211,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; - debug_assert!(requires != provides); + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -2288,11 +2293,10 @@ impl EthApi { let priority = self.transaction_priority(&pending_transaction.transaction); // Tempo txs use a 2D nonce system — no sequential ordering by account nonce. - let (requires, provides) = if let FoundryTxEnvelope::Tempo(aa_tx) = - pending_transaction.transaction.as_ref() - && !aa_tx.tx().nonce_key.is_zero() + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) { - (vec![], vec![pending_transaction.hash().to_vec()]) + (requires, provides) } else { let on_chain_nonce = self.backend.current_nonce(from).await?; let nonce = pending_transaction.transaction.nonce(); @@ -3192,8 +3196,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -3549,6 +3558,7 @@ impl EthApi { requires: Vec, provides: Vec, ) -> Result { + debug_assert!(requires != provides); let from = *pending_transaction.sender(); let priority = self.transaction_priority(&pending_transaction.transaction); let pool_transaction = @@ -3565,7 +3575,9 @@ impl EthApi { FoundryTxEnvelope::Eip1559(_) => self.backend.ensure_eip1559_active(), FoundryTxEnvelope::Eip4844(_) => self.backend.ensure_eip4844_active(), FoundryTxEnvelope::Eip7702(_) => self.backend.ensure_eip7702_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => Err(BlockchainError::InvalidTransactionRequest( "not implemented for post-exec tx".to_string(), )), @@ -3634,6 +3646,20 @@ fn required_marker(provided_nonce: u64, on_chain_nonce: u64, from: Address) -> V if on_chain_nonce <= prev_nonce { vec![to_marker(prev_nonce, from)] } else { Vec::new() } } +fn tempo_parallel_nonce_markers( + pending_transaction: &PendingTransaction, +) -> Option<(Vec, Vec)> { + // Tempo txs with non-zero nonce_key use a 2D nonce system and should not + // be sequenced by account nonce markers. + if let FoundryTxEnvelope::Tempo(aa_tx) = pending_transaction.transaction.as_ref() + && !aa_tx.tx().nonce_key.is_zero() + { + Some((vec![], vec![pending_transaction.hash().to_vec()])) + } else { + None + } +} + fn convert_transact_out(out: &Option) -> Bytes { match out { None => Default::default(), diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 614f409c3eb65..c41937055fdd4 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -67,9 +67,11 @@ impl ReceiptBuilder for FoundryReceiptBuilder { FoundryTxType::Eip1559 => FoundryReceiptEnvelope::Eip1559(receipt), FoundryTxType::Eip4844 => FoundryReceiptEnvelope::Eip4844(receipt), FoundryTxType::Eip7702 => FoundryReceiptEnvelope::Eip7702(receipt), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { unreachable!("deposit receipts are built in commit_transaction") } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt), FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt), } @@ -85,7 +87,7 @@ pub struct AnvilTxResult { pub sender: Address, } -impl TxResult for AnvilTxResult { +impl TxResult for AnvilTxResult { type HaltReason = H; fn result(&self) -> &ResultAndState { @@ -217,12 +219,10 @@ where }) } - fn commit_transaction( - &mut self, - output: Self::Result, - ) -> Result { + fn commit_transaction(&mut self, output: Self::Result) -> GasOutput { let AnvilTxResult { inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type }, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] sender, } = output; @@ -237,6 +237,7 @@ where self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used); } + #[cfg(feature = "optimism")] let receipt = if tx_type == FoundryTxType::Deposit { let deposit_nonce = state.get(&sender).map(|acc| acc.info.nonce); let receipt = alloy_consensus::Receipt { @@ -262,11 +263,19 @@ where cumulative_gas_used: self.gas_used, }) }; + #[cfg(not(feature = "optimism"))] + let receipt = self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx_type, + evm: &self.evm, + result, + state: &state, + cumulative_gas_used: self.gas_used, + }); self.receipts.push(receipt); self.evm.db_mut().commit(state); - Ok(GasOutput::new(gas_used)) + GasOutput::new(gas_used) } fn finish( @@ -429,7 +438,7 @@ where let exec_result = result.result().result.clone(); let gas_used = result.result().result.tx_gas_used(); - executor.commit_transaction(result).expect("commit failed"); + executor.commit_transaction(result); let traces = executor.evm_mut().inspector_mut().finish_transaction(inspector_config); diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index f404031445611..1c8dfffab9d95 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -57,6 +57,7 @@ use alloy_network::{ AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, Network, NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, UnknownTypedTransaction, }; +#[cfg(feature = "optimism")] use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; use alloy_primitives::{ Address, B256, Bloom, Bytes, TxHash, TxKind, U64, U256, hex, keccak256, logs_bloom, @@ -108,20 +109,60 @@ use foundry_evm::{ }, }; use foundry_evm_networks::NetworkConfigs; +#[cfg(feature = "optimism")] +use foundry_primitives::get_deposit_tx_parts; use foundry_primitives::{ FoundryNetwork, FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope, - FoundryTxReceipt, get_deposit_tx_parts, + FoundryTxReceipt, }; use futures::channel::mpsc::{UnboundedSender, unbounded}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait}; -use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +#[cfg(feature = "optimism")] +use op_revm::{OpSpecId, OpTransaction, transaction::deposit::DepositTransactionParts}; + +/// Side-channel container for OP-specific deposit info produced by +/// [`Backend::build_call_env`] and consumed by the OP transact path. +/// +/// When the `optimism` feature is enabled, this is an alias for +/// `op_revm::DepositTransactionParts`. When disabled, it is a zero-sized +/// stand-in so the eth/tempo dispatch chain still type-checks. +#[cfg(feature = "optimism")] +type OpCallDepositInfo = DepositTransactionParts; +#[cfg(not(feature = "optimism"))] +#[derive(Default, Clone, Debug)] +struct OpCallDepositInfo; + +/// Marker trait that abstracts over the per-network inspector trait bounds +/// required by the in-memory backend. The OP bound is only included when the +/// `optimism` feature is enabled. +#[cfg(feature = "optimism")] +pub trait BackendInspector: + Inspector> + Inspector> + Inspector> +{ +} +#[cfg(feature = "optimism")] +impl BackendInspector for T where + T: Inspector> + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +pub trait BackendInspector: + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +impl BackendInspector for T where + T: Inspector> + Inspector> +{ +} use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ DatabaseCommit, Inspector, context::{Block as RevmBlock, BlockEnv, Cfg, TxEnv}, context_interface::{ block::BlobExcessGasAndPrice, - result::{EVMError, ExecutionResult, HaltReason, Output, ResultAndState}, + result::{ExecutionResult, HaltReason, Output, ResultAndState}, }, database::{CacheDB, DbAccount, WrapDatabaseRef}, interpreter::InstructionResult, @@ -157,6 +198,8 @@ pub mod cache; pub mod fork_db; pub mod in_memory_db; pub mod inspector; +#[cfg(feature = "optimism")] +pub mod optimism; pub mod state; pub mod storage; @@ -419,6 +462,11 @@ impl Backend { self.genesis.timestamp } + /// Returns the configured genesis block number. + pub const fn genesis_number(&self) -> u64 { + self.genesis.number + } + /// Returns balance of the given account. pub async fn current_balance(&self, address: Address) -> DatabaseResult { Ok(self.get_account(address).await?.balance) @@ -490,12 +538,21 @@ impl Backend { } /// Returns true if op-stack deposits are active - pub fn is_optimism(&self) -> bool { + #[cfg(feature = "optimism")] + pub const fn is_optimism(&self) -> bool { self.networks.is_optimism() } + /// Returns true if op-stack deposits are active. + /// + /// Always `false` when built without the `optimism` feature. + #[cfg(not(feature = "optimism"))] + pub const fn is_optimism(&self) -> bool { + false + } + /// Returns true if Tempo network mode is active - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { self.networks.is_tempo() } @@ -589,7 +646,8 @@ impl Backend { } /// Returns an error if op-stack deposits are not active - pub fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { + #[cfg(feature = "optimism")] + pub const fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { if self.is_optimism() { return Ok(()); } @@ -597,7 +655,7 @@ impl Backend { } /// Returns an error if Tempo transactions are not active - pub fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { + pub const fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { if self.is_tempo() { return Ok(()); } @@ -1139,58 +1197,53 @@ impl Backend { db: &'db DB, evm_env: &EvmEnv, inspector: &mut I, - tx_env: OpTransaction, + tx_env: TxEnv, + op_deposit: OpCallDepositInfo, ) -> Result, BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_env = EvmEnv::new( - evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), - evm_env.block_env.clone(), - ); - let mut evm = OpEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - op_env, - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { - EVMError::Database(db) => EVMError::Database(db), - EVMError::Header(h) => EVMError::Header(h), - EVMError::Custom(s) => EVMError::Custom(s), - EVMError::CustomAny(err) => EVMError::CustomAny(err), - EVMError::Transaction(t) => EVMError::Transaction(t), - })?; - Ok(ResultAndState { - result: result.result.map_haltreason(|h| match h { - OpHaltReason::Base(eth) => eth, - _ => HaltReason::PrecompileError, - }), - state: result.state, - }) - } else if self.is_tempo() { - self.transact_tempo_with_inspector_ref( - db, - evm_env, - inspector, - TempoTxEnv::from(tx_env.base), - ) + let op_tx = OpTransaction { base: tx_env, deposit: op_deposit, ..Default::default() }; + return self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx); + } + // `op_deposit` only matters on the OP path; eth/tempo ignore it. + let _ = op_deposit; + if self.is_tempo() { + self.transact_tempo_with_inspector_ref(db, evm_env, inspector, TempoTxEnv::from(tx_env)) } else { - let mut evm = EthEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - evm_env.clone(), - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - Ok(evm.transact(tx_env.base)?) + self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env) } } + /// Eth path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an Ethereum EVM, injects precompiles, and transacts with a + /// plain [`TxEnv`]. + fn transact_eth_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: TxEnv, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let mut evm = EthEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + evm_env.clone(), + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + Ok(evm.transact(tx_env)?) + } + /// Builds the appropriate tx env from a [`FoundryTxEnvelope`], executes via the correct /// EVM backend (Op/Tempo/Eth), and returns both the result and the base [`TxEnv`]. fn transact_envelope_with_inspector_ref<'db, I, DB>( @@ -1203,9 +1256,7 @@ impl Backend { ) -> Result<(ResultAndState, TxEnv), BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { if tx.is_tempo() { @@ -1213,14 +1264,21 @@ impl Backend { FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); let base = tx_env.inner.clone(); let result = self.transact_tempo_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) - } else { - let tx_env: OpTransaction = + return Ok((result, base)); + } + #[cfg(feature = "optimism")] + if self.is_optimism() { + let op_tx: OpTransaction = FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); - let base = tx_env.base.clone(); - let result = self.transact_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) + let base = op_tx.base.clone(); + let result = self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx)?; + return Ok((result, base)); } + let tx_env: TxEnv = + FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); + let base = tx_env.clone(); + let result = self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env)?; + Ok((result, base)) } /// Creates a Tempo EVM, injects precompiles, and transacts with a native [`TempoTxEnv`]. @@ -1298,6 +1356,7 @@ impl Backend { }}; } + #[cfg(feature = "optimism")] if self.is_optimism() { let op_env = EvmEnv::new( evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), @@ -1305,8 +1364,10 @@ impl Backend { ); let mut evm = OpEvmFactory::::default().create_evm_with_inspector(db, op_env, inspector); - run!(evm) - } else if self.is_tempo() { + return run!(evm); + } + + if self.is_tempo() { let hardfork = TempoHardfork::from(self.hardfork); let tempo_env = EvmEnv::new( evm_env @@ -1338,7 +1399,7 @@ impl Backend { request: WithOtherFields, fee_details: FeeDetails, block_env: BlockEnv, - ) -> (EvmEnv, OpTransaction) { + ) -> (EvmEnv, TxEnv, OpCallDepositInfo) { let tx_type = request.minimal_tx_type() as u8; let WithOtherFields:: { @@ -1391,7 +1452,7 @@ impl Backend { let caller = from.unwrap_or_default(); let to = to.as_ref().and_then(TxKind::to); let blob_hashes = blob_versioned_hashes.unwrap_or_default(); - let mut base = TxEnv { + let mut tx_env = TxEnv { caller, gas_limit, gas_price, @@ -1413,11 +1474,10 @@ impl Backend { blob_hashes, ..Default::default() }; - base.set_signed_authorization(authorization_list.unwrap_or_default()); - let mut tx_env = OpTransaction { base, ..Default::default() }; + tx_env.set_signed_authorization(authorization_list.unwrap_or_default()); if let Some(nonce) = nonce { - tx_env.base.nonce = nonce; + tx_env.nonce = nonce; } if evm_env.block_env.basefee == 0 { @@ -1427,13 +1487,22 @@ impl Backend { } // Deposit transaction? (only valid when op-stack deposits are active) - if self.ensure_op_deposits_active().is_ok() + #[cfg(feature = "optimism")] + let op_deposit = if self.ensure_op_deposits_active().is_ok() && let Ok(deposit) = get_deposit_tx_parts(&other) { - tx_env.deposit = deposit; - } + deposit + } else { + OpCallDepositInfo::default() + }; + #[cfg(not(feature = "optimism"))] + let op_deposit = { + // `other` carries OP-only deposit fields; consumed only when feature is enabled. + let _ = &other; + OpCallDepositInfo::default() + }; - (evm_env, tx_env) + (evm_env, tx_env, op_deposit) } pub fn call_with_state( @@ -1467,13 +1536,13 @@ impl Backend { (fee_token, nonce_key, valid_before, valid_after) }); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state } = if let Some((fee_token, nonce_key, valid_before, valid_after)) = tempo_overrides { use tempo_primitives::transaction::Call; - let base = tx_env.base; + let base = tx_env; let mut tempo_tx = TempoTxEnv::from(base.clone()); tempo_tx.fee_token = fee_token; @@ -1495,7 +1564,13 @@ impl Backend { } self.transact_tempo_with_inspector_ref(state, &evm_env, &mut inspector, tempo_tx)? } else { - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)? + self.transact_with_inspector_ref( + state, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )? }; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -1518,9 +1593,9 @@ impl Backend { let mut inspector = AccessListInspector::new(request.access_list.clone().unwrap_or_default()); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)?; + self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env, op_deposit)?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); let access_list = inspector.access_list(); Ok((exit_reason, out, gas_used, access_list)) @@ -2912,7 +2987,7 @@ where TracingInspectorConfig::from_geth_call_config(&call_config), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let ResultAndState { result, state: _ } = self .transact_with_inspector_ref( @@ -2920,6 +2995,7 @@ where &evm_env, &mut inspector, tx_env, + op_deposit, )?; inspector.print_logs(); @@ -2945,13 +3021,14 @@ where ), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env, + op_deposit, )?; Ok(inspector @@ -2973,22 +3050,22 @@ where } #[cfg(feature = "js-tracer")] GethDebugTracerType::JsTracer(code) => { - use alloy_evm::IntoTxEnv; let config = tracer_config.into_json(); let mut inspector = revm_inspectors::tracing::js::JsInspector::new(code, config) .map_err(|err| BlockchainError::Message(err.to_string()))?; - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block.clone()); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env.clone(), + op_deposit, )?; let res = inspector - .json_result(result, &OpTx(tx_env).into_tx_env(), &block, &cache_db) + .json_result(result, &tx_env, &block, &cache_db) .map_err(|err| BlockchainError::Message(err.to_string()))?; Ok(GethTrace::JS(res)) @@ -3001,9 +3078,14 @@ where .build_inspector() .with_tracing_config(TracingInspectorConfig::from_geth_config(&config)); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block); - let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); + let ResultAndState { result, state: _ } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -3187,10 +3269,7 @@ where f: F, ) -> Result where - for<'a> I: Inspector>>>> - + Inspector>>>> - + Inspector>>>> - + 'a, + for<'a> I: BackendInspector>>> + 'a, for<'a> F: FnOnce(ResultAndState, CacheDB>, I, TxEnv, EvmEnv) -> T, { @@ -3965,7 +4044,7 @@ impl Backend { )? .or_zero_fees(); - let (mut evm_env, tx_env) = self.build_call_env( + let (mut evm_env, tx_env, op_deposit) = self.build_call_env( WithOtherFields::new(request.clone()), fee_details, block_env.clone(), @@ -3986,8 +4065,13 @@ impl Backend { inspector = inspector.with_transfers(); } trace!(target: "backend", env=?evm_env, spec=?evm_env.spec_id(),"simulate evm env"); - let ResultAndState { result, state } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let ResultAndState { result, state } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; trace!(target: "backend", ?result, ?request, "simulate call"); inspector.print_logs(); @@ -4359,7 +4443,10 @@ where } // Nonce validation — skip for deposits (L1→L2) and Tempo txs (2D nonce system) + #[cfg(feature = "optimism")] let is_deposit_tx = pending.transaction.as_ref().is_deposit(); + #[cfg(not(feature = "optimism"))] + let is_deposit_tx = false; let is_tempo_tx = pending.transaction.as_ref().is_tempo(); let nonce = tx.nonce(); if nonce < account.nonce && !is_deposit_tx && !is_tempo_tx { @@ -4475,6 +4562,7 @@ where ); let value = tx.value(); match tx.as_ref() { + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(deposit_tx) => { // Deposit transactions // https://specs.optimism.io/protocol/deposits.html#execution @@ -4538,6 +4626,7 @@ pub fn transaction_build( info: Option, base_fee: Option, ) -> AnyRpcTransaction { + #[cfg(feature = "optimism")] if let FoundryTxEnvelope::Deposit(deposit_tx) = eth_transaction.as_ref() { let dep_tx = deposit_tx; diff --git a/crates/anvil/src/eth/backend/mem/optimism.rs b/crates/anvil/src/eth/backend/mem/optimism.rs new file mode 100644 index 0000000000000..e9a94cc254fb7 --- /dev/null +++ b/crates/anvil/src/eth/backend/mem/optimism.rs @@ -0,0 +1,61 @@ +//! Optimism-specific transact helpers for the in-memory backend. + +use super::Backend; +use crate::eth::error::BlockchainError; +use alloy_evm::{Database, Evm, EvmEnv, EvmFactory}; +use alloy_network::Network; +use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; +use foundry_evm::backend::DatabaseError; +use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +use revm::{ + DatabaseRef, Inspector, + context::{ + TxEnv, + result::{EVMError, HaltReason, ResultAndState}, + }, + database_interface::WrapDatabaseRef, +}; + +impl Backend { + /// Optimism path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an OP EVM, injects precompiles, transacts, and maps the + /// OP-specific halt reason back to the shared [`HaltReason`]. + pub(super) fn transact_op_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: OpTransaction, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let op_env = EvmEnv::new( + evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), + evm_env.block_env.clone(), + ); + let mut evm = OpEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + op_env, + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { + EVMError::Database(db) => EVMError::Database(db), + EVMError::Header(h) => EVMError::Header(h), + EVMError::Custom(s) => EVMError::Custom(s), + EVMError::CustomAny(err) => EVMError::CustomAny(err), + EVMError::Transaction(t) => EVMError::Transaction(t), + })?; + Ok(ResultAndState { + result: result.result.map_haltreason(|h| match h { + OpHaltReason::Base(eth) => eth, + _ => HaltReason::PrecompileError, + }), + state: result.state, + }) + } +} diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error/mod.rs similarity index 91% rename from crates/anvil/src/eth/error.rs rename to crates/anvil/src/eth/error/mod.rs index 3b2ada43d7731..482df681b184a 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error/mod.rs @@ -12,7 +12,6 @@ use anvil_rpc::{ response::ResponseResult, }; use foundry_evm::{backend::DatabaseError, decode::RevertDecoder}; -use op_revm::OpTransactionError; use revm::{ context_interface::result::{EVMError, InvalidHeader, InvalidTransaction}, interpreter::InstructionResult, @@ -21,6 +20,9 @@ use serde::Serialize; use tempo_revm::TempoInvalidTransaction; use tokio::time::Duration; +#[cfg(feature = "optimism")] +mod optimism; + pub(crate) type Result = std::result::Result; #[derive(Debug, thiserror::Error)] @@ -163,51 +165,6 @@ where } } -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => match err { - OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), - OpTransactionError::DepositSystemTxPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::HaltedDepositPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), - }, - EVMError::Header(err) => match err { - InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, - InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, - }, - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => { - let op_err: OpTransactionError = err.0; - EVMError::::Transaction(op_err).into() - } - EVMError::Header(err) => EVMError::::Header(err).into(), - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - impl From> for BlockchainError where T: Into, @@ -451,16 +408,6 @@ impl From for InvalidTransactionError { } } -impl From for InvalidTransactionError { - fn from(value: OpTransactionError) -> Self { - match value { - OpTransactionError::Base(err) => err.into(), - OpTransactionError::DepositSystemTxPostRegolith - | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, - OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, - } - } -} /// Helper trait to easily convert results to rpc results pub(crate) trait ToRpcResponseResult { fn to_rpc_result(self) -> ResponseResult; @@ -577,9 +524,13 @@ impl ToRpcResponseResult for Result { err => RpcError::internal_error_with(format!("Fork Error: {err:?}")), } } - err @ BlockchainError::EvmError(_) => { - RpcError::internal_error_with(err.to_string()) - } + err @ BlockchainError::EvmError(_) => RpcError { + // VM halts are execution failures, not JSON-RPC server faults. REVERT has a + // dedicated code/data path above; other halts, such as invalid opcode, do not. + code: ErrorCode::TransactionRejected, + message: err.to_string().into(), + data: None, + }, err @ BlockchainError::EvmOverrideError(_) => { RpcError::invalid_params(err.to_string()) } diff --git a/crates/anvil/src/eth/error/optimism.rs b/crates/anvil/src/eth/error/optimism.rs new file mode 100644 index 0000000000000..1207fde30a72a --- /dev/null +++ b/crates/anvil/src/eth/error/optimism.rs @@ -0,0 +1,62 @@ +//! Optimism-specific error conversions for [`BlockchainError`] and +//! [`InvalidTransactionError`]. + +use super::{BlockchainError, InvalidTransactionError}; +use op_revm::OpTransactionError; +use revm::context_interface::result::{EVMError, InvalidHeader}; + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => match err { + OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), + OpTransactionError::DepositSystemTxPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::HaltedDepositPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), + }, + EVMError::Header(err) => match err { + InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, + InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, + }, + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => { + let op_err: OpTransactionError = err.0; + EVMError::::Transaction(op_err).into() + } + EVMError::Header(err) => EVMError::::Header(err).into(), + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From for InvalidTransactionError { + fn from(value: OpTransactionError) -> Self { + match value { + OpTransactionError::Base(err) => err.into(), + OpTransactionError::DepositSystemTxPostRegolith + | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, + OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, + } + } +} diff --git a/crates/anvil/src/eth/otterscan/api.rs b/crates/anvil/src/eth/otterscan/api.rs index 4da86933020ae..2ccf65ba721f5 100644 --- a/crates/anvil/src/eth/otterscan/api.rs +++ b/crates/anvil/src/eth/otterscan/api.rs @@ -155,9 +155,12 @@ impl EthApi { let best = self.backend.best_number(); // we go from given block (defaulting to best) down to first block - // considering only post-fork + // considering only post-fork (or post-genesis in non-fork mode) let from = if block_number == 0 { best } else { block_number - 1 }; - let to = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + let to = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let first_page = from >= best; let mut last_page = false; @@ -198,8 +201,11 @@ impl EthApi { node_info!("ots_searchTransactionsAfter"); let best = self.backend.best_number(); - // we go from the first post-fork block, up to the tip - let first_block = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + // we go from the first post-fork (or post-genesis) block, up to the tip + let first_block = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let from = if block_number == 0 { first_block } else { block_number + 1 }; let to = best; @@ -248,7 +254,10 @@ impl EthApi { ) -> Result> { node_info!("ots_getTransactionBySenderAndNonce"); - let from = self.get_fork().map(|f| f.block_number() + 1).unwrap_or_default(); + let from = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let to = self.backend.best_number(); for n in (from..=to).rev() { diff --git a/crates/anvil/src/eth/pool/transactions.rs b/crates/anvil/src/eth/pool/transactions.rs index df65822e1eab3..5280987483dd7 100644 --- a/crates/anvil/src/eth/pool/transactions.rs +++ b/crates/anvil/src/eth/pool/transactions.rs @@ -123,10 +123,10 @@ impl PoolTransaction { impl fmt::Debug for PoolTransaction { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "Transaction {{ ")?; - write!(fmt, "hash: {:?}, ", &self.pending_transaction.hash())?; + write!(fmt, "hash: {:?}, ", self.pending_transaction.hash())?; write!(fmt, "requires: [{}], ", hex_fmt_many(self.requires.iter()))?; write!(fmt, "provides: [{}], ", hex_fmt_many(self.provides.iter()))?; - write!(fmt, "raw tx: {:?}", &self.pending_transaction)?; + write!(fmt, "raw tx: {:?}", self.pending_transaction)?; write!(fmt, "}}")?; Ok(()) } diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index 3fdf6192c4537..d1736c3093056 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -1,5 +1,7 @@ use crate::eth::error::BlockchainError; -use alloy_consensus::{Sealed, SignableTransaction}; +#[cfg(feature = "optimism")] +use alloy_consensus::Sealed; +use alloy_consensus::SignableTransaction; use alloy_dyn_abi::TypedData; use alloy_network::{Network, TxSignerSync}; use alloy_primitives::{Address, B256, Signature, map::AddressHashMap}; @@ -130,9 +132,11 @@ impl Signer for DevSigner { let sig = signer.sign_transaction_sync(&mut t)?; FoundryTxEnvelope::Eip4844(t.into_signed(sig)) } + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => { unreachable!("op deposit txs should not be signed") } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be signed") } @@ -156,7 +160,9 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope { FoundryTypedTx::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)), FoundryTypedTx::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)), FoundryTypedTx::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be impersonated") } diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm/mod.rs similarity index 64% rename from crates/anvil/src/evm.rs rename to crates/anvil/src/evm/mod.rs index d1b40ba56ebbd..85e43d371b097 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm/mod.rs @@ -2,6 +2,9 @@ use alloy_evm::precompiles::DynPrecompile; use alloy_primitives::Address; use std::fmt::Debug; +#[cfg(feature = "optimism")] +mod optimism; + /// Object-safe trait that enables injecting extra precompiles when using /// `anvil` as a library. pub trait PrecompileFactory: Send + Sync + Unpin + Debug { @@ -15,14 +18,12 @@ mod tests { use crate::PrecompileFactory; use alloy_evm::{ - EthEvm, Evm, EvmEnv, EvmFactory, + EthEvm, Evm, eth::EthEvmContext, precompiles::{DynPrecompile, PrecompilesMap}, }; - use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; - use alloy_primitives::{Address, Bytes, TxKind, U256, address}; + use alloy_primitives::{Address, Bytes, TxKind, address}; use itertools::Itertools; - use op_revm::{OpSpecId, OpTransaction}; use revm::{ Journal, context::{BlockEnv, CfgEnv, Evm as RevmEvm, JournalTr, LocalContext, TxEnv}, @@ -35,20 +36,19 @@ mod tests { }; // A precompile activated in the `Prague` spec (BLS12-381 G2 map). - const ETH_PRAGUE_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000011"); + pub(super) const ETH_PRAGUE_PRECOMPILE: Address = + address!("0x0000000000000000000000000000000000000011"); // A precompile activated in the `Osaka` spec (EIP-7951). const ETH_OSAKA_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A precompile activated in the `Isthmus` spec. - const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A custom precompile address and payload for testing. - const PRECOMPILE_ADDR: Address = address!("0x0000000000000000000000000000000000000071"); - const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; + pub(super) const PRECOMPILE_ADDR: Address = + address!("0x0000000000000000000000000000000000000071"); + pub(super) const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; #[derive(Debug)] - struct CustomPrecompileFactory; + pub(super) struct CustomPrecompileFactory; impl PrecompileFactory for CustomPrecompileFactory { fn precompiles(&self) -> Vec<(Address, DynPrecompile)> { @@ -109,34 +109,6 @@ mod tests { (tx_env, eth_evm) } - /// Creates a new OP EVM instance. - fn create_op_evm( - _spec: SpecId, - op_spec: OpSpecId, - ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { - let tx = OpTx(OpTransaction:: { - base: TxEnv { - kind: TxKind::Call(PRECOMPILE_ADDR), - data: PAYLOAD.into(), - ..Default::default() - }, - ..Default::default() - }); - - let mut evm = OpEvmFactory::::default().create_evm_with_inspector( - EmptyDB::default(), - EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), - NoOpInspector, - ); - - if op_spec == OpSpecId::ISTHMUS { - evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); - evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); - } - - (tx, evm) - } - #[test] fn build_eth_evm_with_extra_precompiles_osaka_spec() { let (tx_env, mut evm) = create_eth_evm(SpecId::OSAKA); @@ -187,38 +159,4 @@ mod tests { assert!(result.result.is_success()); assert_eq!(result.result.output(), Some(&PAYLOAD.into())); } - - #[test] - fn build_op_evm_with_extra_precompiles_isthmus_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); - - assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } - - #[test] - fn build_op_evm_with_extra_precompiles_bedrock_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); - - assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } } diff --git a/crates/anvil/src/evm/optimism.rs b/crates/anvil/src/evm/optimism.rs new file mode 100644 index 0000000000000..526375fec31ea --- /dev/null +++ b/crates/anvil/src/evm/optimism.rs @@ -0,0 +1,87 @@ +//! Optimism-specific EVM helpers. + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use super::super::tests::{ + CustomPrecompileFactory, ETH_PRAGUE_PRECOMPILE, PAYLOAD, PRECOMPILE_ADDR, + }; + use crate::PrecompileFactory; + use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; + use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; + use alloy_primitives::{Address, TxKind, U256, address}; + use itertools::Itertools; + use op_revm::{OpSpecId, OpTransaction}; + use revm::{ + context::{BlockEnv, CfgEnv, TxEnv}, + database::{EmptyDB, EmptyDBTyped}, + inspector::NoOpInspector, + primitives::hardfork::SpecId, + }; + + // A precompile activated in the `Isthmus` spec. + const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); + + /// Creates a new OP EVM instance. + fn create_op_evm( + _spec: SpecId, + op_spec: OpSpecId, + ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { + let tx = OpTx(OpTransaction:: { + base: TxEnv { + kind: TxKind::Call(PRECOMPILE_ADDR), + data: PAYLOAD.into(), + ..Default::default() + }, + ..Default::default() + }); + + let mut evm = OpEvmFactory::::default().create_evm_with_inspector( + EmptyDB::default(), + EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), + NoOpInspector, + ); + + if op_spec == OpSpecId::ISTHMUS { + evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); + evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); + } + + (tx, evm) + } + + #[test] + fn build_op_evm_with_extra_precompiles_isthmus_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); + + assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } + + #[test] + fn build_op_evm_with_extra_precompiles_bedrock_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); + + assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } +} diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index 26e587e8b5123..a661e9c765b26 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::{ error::{NodeError, NodeResult}, eth::{ diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index c4879e36d5240..216476c69eeb8 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -11,6 +11,7 @@ mod gas; mod genesis; mod ipc; mod logs; +#[cfg(feature = "optimism")] mod optimism; mod otterscan; mod proof; diff --git a/crates/anvil/tests/it/revert.rs b/crates/anvil/tests/it/revert.rs index ab85fc89abf80..a15454fa5593e 100644 --- a/crates/anvil/tests/it/revert.rs +++ b/crates/anvil/tests/it/revert.rs @@ -28,6 +28,38 @@ async fn test_deploy_reverting() { assert!(!receipt.inner.inner.status()); } +#[tokio::test(flavor = "multi_thread")] +async fn test_invalid_opcode_rpc_error_code() { + let (_api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + let sender = handle.dev_accounts().next().unwrap(); + + // Deploy a contract whose runtime bytecode is the invalid opcode 0xfe. + let code = bytes!("60fe60005360016000f3"); + let tx = TransactionRequest::default().from(sender).with_deploy_code(code); + let receipt = provider + .send_transaction(WithOtherFields::new(tx)) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + let contract = receipt.contract_address.unwrap(); + + for (method, params) in [ + ("eth_call", serde_json::json!([{ "from": sender, "to": contract }, "latest"])), + ("eth_estimateGas", serde_json::json!([{ "from": sender, "to": contract }])), + ] { + let error = rpc_error(&handle.http_endpoint(), method, params).await; + assert_eq!(error["code"], serde_json::json!(-32003), "{error}"); + assert!(error.get("data").is_none(), "{error}"); + + let message = error["message"].as_str().unwrap(); + assert!(message.contains("EVM error InvalidFEOpcode"), "{error}"); + assert!(!message.contains("execution reverted"), "{error}"); + } +} + #[tokio::test(flavor = "multi_thread")] async fn test_revert_messages() { sol!( @@ -124,3 +156,21 @@ async fn test_solc_revert_custom_errors() { let s = err.to_string(); assert!(s.contains("execution reverted"), "{s:?}"); } + +async fn rpc_error(endpoint: &str, method: &str, params: serde_json::Value) -> serde_json::Value { + let response = reqwest::Client::new() + .post(endpoint) + .json(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + })) + .send() + .await + .unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + + let body = response.json::().await.unwrap(); + body.get("error").cloned().unwrap_or_else(|| panic!("expected JSON-RPC error, got {body}")) +} diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index dc5be77a244da..03be26a631a11 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -61,9 +61,9 @@ tempo-contracts.workspace = true tempo-primitives.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-flz.workspace = true -op-alloy-network.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-flz = { workspace = true, optional = true } +op-alloy-network = { workspace = true, optional = true } chrono.workspace = true eyre.workspace = true @@ -100,7 +100,7 @@ anvil.workspace = true foundry-test-utils.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -109,3 +109,12 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "dep:op-alloy-flz", + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "foundry-common/optimism", + "foundry-evm-networks/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 6dc2ed6acf430..23fda25c40faa 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -29,6 +29,7 @@ use foundry_common::{ shell, stdin, }; use foundry_evm_networks::NetworkVariant; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; use std::time::Instant; use tempo_alloy::TempoNetwork; @@ -351,6 +352,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let output = if raw || fields.contains(&"raw".into()) { match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -569,6 +571,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let is_raw = raw || field.as_ref().is_some_and(|f| f == "raw"); let output = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -791,6 +794,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::DecodeTransaction { tx, network } => { let tx = stdin::unwrap_line(tx)?; let decoded_tx = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { SimpleCast::decode_raw_transaction::(&tx)? } @@ -809,6 +813,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::Erc20Token { command } => command.run().await?, CastSubcommand::Tip20Token { command } => command.run().await?, CastSubcommand::Keychain { command } => command.run().await?, + CastSubcommand::Tempo { command } => command.run().await?, + CastSubcommand::VirtualAddress { command } => command.run().await?, + #[cfg(feature = "optimism")] CastSubcommand::DAEstimate(cmd) => { cmd.run().await?; } diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index ae5c9668f522f..e25798086d512 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy_consensus::SignableTransaction; use alloy_eips::eip2718::Encodable2718; -use alloy_network::{EthereumWallet, NetworkTransactionBuilder}; +use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder}; use alloy_primitives::Address; use alloy_provider::Provider; use alloy_signer::Signer; @@ -17,7 +17,7 @@ use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use tempo_alloy::TempoNetwork; @@ -53,7 +53,7 @@ pub struct BatchMakeTxArgs { impl BatchMakeTxArgs { pub async fn run(self) -> Result<()> { - let Self { calls, tx, eth, raw_unsigned, ethsign } = self; + let Self { calls, mut tx, eth, raw_unsigned, ethsign } = self; let has_nonce = tx.nonce.is_some(); if calls.is_empty() { @@ -63,6 +63,10 @@ impl BatchMakeTxArgs { let config = eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + // Resolve signer to detect keychain mode let (signer, tempo_access_key) = eth.wallet.maybe_signer().await?; @@ -92,14 +96,14 @@ impl BatchMakeTxArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as raw unsigned. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Set calls on the transaction builder.tx.calls = tempo_calls; @@ -117,6 +121,7 @@ impl BatchMakeTxArgs { let from = eth.wallet.from.unwrap_or(Address::ZERO); let (tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -125,6 +130,7 @@ impl BatchMakeTxArgs { if ethsign { let (tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; return Ok(()); @@ -137,7 +143,9 @@ impl BatchMakeTxArgs { }; let signed_tx = if let Some(ref access_key) = tempo_access_key { - let (tx, _) = tx_builder.build(access_key.wallet_address).await?; + let (tx, _) = + tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = tx .sign_with_access_key( &provider, @@ -151,6 +159,7 @@ impl BatchMakeTxArgs { } else { tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?; let (tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let envelope = tx.build(&EthereumWallet::new(signer)).await?; alloy_primitives::hex::encode(envelope.encoded_2718()) }; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index ec7254b08e2f5..33128ae897898 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -9,14 +9,14 @@ use crate::{ cmd::send::{cast_send, cast_send_with_access_key}, tx::{self, CastTxBuilder, SendTxOpts}, }; -use alloy_network::EthereumWallet; +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::TransactionOpts, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::provider::ProviderBuilder; use std::time::Duration; @@ -50,7 +50,7 @@ pub struct BatchSendArgs { impl BatchSendArgs { pub async fn run(self) -> Result<()> { - let Self { calls, send_tx, tx, unlocked } = self; + let Self { calls, send_tx, mut tx, unlocked } = self; if calls.is_empty() { return Err(eyre!("No calls specified. Use --call to specify at least one call.")); @@ -59,6 +59,10 @@ impl BatchSendArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -93,14 +97,14 @@ impl BatchSendArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as unlocked. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Access the inner tx and set calls builder.tx.calls = tempo_calls; @@ -116,6 +120,7 @@ impl BatchSendArgs { if unlocked { let (tx, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; cast_send( provider, tx, @@ -132,7 +137,12 @@ impl BatchSendArgs { }; if let Some(ref access_key) = tempo_access_key { - let (tx_request, _) = builder.build(access_key.wallet_address).await?; + let (tx_request, _) = + builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; cast_send_with_access_key( &provider, tx_request, @@ -146,6 +156,10 @@ impl BatchSendArgs { } else { tx::validate_from_address(send_tx.eth.wallet.from, Signer::address(&signer))?; let (tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() .wallet(wallet) diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 3637f166a53df..63ea17f707e03 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -33,10 +33,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork}, }, executors::TracingExecutor, opts::EvmOpts, @@ -222,18 +224,19 @@ impl CallArgs { return self.run_curl().await; } if self.tx.tempo.is_tempo() { - self.run_with_network::().await - } else { - let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); - let mut evm_opts = figment.extract::()?; - evm_opts.infer_network_from_fork().await; + return self.run_with_network::().await; + } - if evm_opts.networks.is_optimism() { - self.run_with_network::().await - } else { - self.run_with_network::().await - } + let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); + let mut evm_opts = figment.extract::()?; + evm_opts.infer_network_from_fork().await; + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_network::().await; } + + self.run_with_network::().await } pub async fn run_with_network(self) -> Result<()> diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index 8b7d80786dfad..897a01c39202a 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -1,9 +1,12 @@ use alloy_ens::NameOrAddress; -use alloy_network::EthereumWallet; +use std::time::Duration; + +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_primitives::{Address, U256, hex, keccak256}; -use alloy_provider::ProviderBuilder as AlloyProviderBuilder; +use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use alloy_sol_types::SolCall; +use alloy_transport::TransportError; use chrono::DateTime; use clap::Parser; use eyre::Result; @@ -12,11 +15,16 @@ use foundry_cli::{ utils::LoadConfig, }; use foundry_common::{ + FoundryTransactionBuilder, provider::ProviderBuilder, - shell, - tempo::{self, KeyType, KeysFile, WalletType, read_tempo_keys_file, tempo_keys_path}, + sh_warn, shell, + tempo::{ + self, KeyType, KeysFile, TEMPO_BROWSER_GAS_BUFFER, WalletType, read_tempo_keys_file, + tempo_keys_path, + }, }; use foundry_evm::hardfork::TempoHardfork; +use serde::Deserialize; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_contracts::precompiles::{ ACCOUNT_KEYCHAIN_ADDRESS, IAccountKeychain, @@ -24,13 +32,16 @@ use tempo_contracts::precompiles::{ CallScope, KeyInfo, KeyRestrictions, LegacyTokenLimit, SelectorRule, SignatureType, TokenLimit, }, + ITIP20, PATH_USD_ADDRESS, account_keychain::{authorizeKeyCall, legacyAuthorizeKeyCall}, }; use yansi::Paint; +use foundry_cli::utils::{maybe_print_resolved_lane, resolve_lane}; + use crate::{ - cmd::send::{cast_send, cast_send_with_access_key}, - tx::{CastTxBuilder, SendTxOpts}, + cmd::send::cast_send, + tx::{CastTxBuilder, CastTxSender, SendTxOpts}, }; /// Tempo keychain management commands. @@ -62,6 +73,19 @@ pub enum KeychainSubcommand { rpc: RpcOpts, }, + /// Inspect an access key policy using the local key registry and on-chain state. + Inspect { + /// The key address to inspect. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + #[command(flatten)] + rpc: RpcOpts, + }, + /// Authorize a new key on-chain via the AccountKeychain precompile. #[command(visible_alias = "auth")] Authorize { @@ -183,8 +207,92 @@ pub enum KeychainSubcommand { #[command(flatten)] send_tx: SendTxOpts, }, + + /// Read or edit TIP-1011 access-key permissions. + Policy { + #[command(subcommand)] + command: KeychainPolicySubcommand, + }, +} + +/// Higher-level access-key policy editing commands. +#[derive(Debug, Parser)] +pub enum KeychainPolicySubcommand { + /// Add or widen an allowed call rule for a target contract. + AddCall { + /// The key address to update. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + /// Target contract address. + #[arg(long)] + target: Address, + + /// Function selector, full signature, or known TIP-20 shorthand. + #[arg(long, value_parser = parse_selector_arg)] + selector: SelectorArg, + + /// Optional recipient/spender restrictions for selector calls. + #[arg(long, value_delimiter = ',')] + recipients: Vec
, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Update a token spending limit amount for a key. + SetLimit { + /// The key address to update. + key_address: Address, + + /// Token address, numeric TIP-20 token id, or PathUSD. + #[arg(long, value_parser = parse_policy_token)] + token: Address, + + /// New raw token-denominated limit. + #[arg(long)] + amount: U256, + + /// Limit period such as 7d, 24h, or 3600s. + /// + /// The current AccountKeychain update entrypoint cannot change periods, so non-zero + /// values are rejected. + #[arg(long, value_parser = parse_period)] + period: Option, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Remove all allowed-call rules for a target contract. + RemoveTarget { + /// The key address to update. + key_address: Address, + + /// Target contract address to remove. + #[arg(long)] + target: Address, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, } +#[derive(Debug, Clone, Copy)] +pub struct SelectorArg([u8; 4]); + fn parse_signature_type(s: &str) -> Result { match s.to_lowercase().as_str() { "secp256k1" => Ok(SignatureType::Secp256k1), @@ -203,6 +311,15 @@ const fn signature_type_name(t: &SignatureType) -> &'static str { } } +const fn signature_type_label(t: &SignatureType) -> &'static str { + match t { + SignatureType::Secp256k1 => "Secp256k1", + SignatureType::P256 => "P256", + SignatureType::WebAuthn => "WebAuthn", + _ => "unknown", + } +} + const fn key_type_name(t: &KeyType) -> &'static str { match t { KeyType::Secp256k1 => "secp256k1", @@ -211,6 +328,14 @@ const fn key_type_name(t: &KeyType) -> &'static str { } } +const fn key_type_label(t: &KeyType) -> &'static str { + match t { + KeyType::Secp256k1 => "Secp256k1", + KeyType::P256 => "P256", + KeyType::WebAuthn => "WebAuthn", + } +} + const fn wallet_type_name(t: &WalletType) -> &'static str { match t { WalletType::Local => "local", @@ -332,6 +457,48 @@ fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> { } } +fn parse_selector_arg(s: &str) -> Result { + parse_selector_bytes(s).map(SelectorArg) +} + +fn parse_policy_token(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "pathusd" | "path_usd" | "path-usd" | "usd" => Ok(PATH_USD_ADDRESS), + _ => foundry_cli::utils::parse_fee_token_address(s).map_err(|e| e.to_string()), + } +} + +fn parse_period(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err("period cannot be empty".to_string()); + } + + let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()); + if split == 0 { + return Err(format!( + "invalid period '{s}': expected a number followed by s, m, h, d, or w" + )); + } + + let value: u64 = + s[..split].parse().map_err(|e| format!("invalid period value '{}': {e}", &s[..split]))?; + let multiplier = match &s[split..].to_ascii_lowercase()[..] { + "" | "s" => 1, + "m" => 60, + "h" => 60 * 60, + "d" => 24 * 60 * 60, + "w" => 7 * 24 * 60 * 60, + unit => { + return Err(format!( + "invalid period unit '{unit}' in '{s}' (expected s, m, h, d, or w)" + )); + } + }; + + value.checked_mul(multiplier).ok_or_else(|| format!("period '{s}' is too large")) +} + /// Represents a single scope entry in JSON format for `--scopes`. #[derive(serde::Deserialize)] struct JsonCallScope { @@ -402,6 +569,9 @@ impl KeychainSubcommand { Self::Check { wallet_address, key_address, rpc } => { run_check(wallet_address, key_address, rpc).await } + Self::Inspect { key_address, root_account, rpc } => { + run_inspect(key_address, root_account, rpc).await + } Self::Authorize { key_address, key_type, @@ -443,6 +613,40 @@ impl KeychainSubcommand { Self::RemoveScope { key_address, target, tx, send_tx } => { run_remove_scope(key_address, target, tx, send_tx).await } + Self::Policy { command } => command.run().await, + } + } +} + +impl KeychainPolicySubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::AddCall { + key_address, + root_account, + target, + selector, + recipients, + tx, + send_tx, + } => { + run_policy_add_call( + key_address, + root_account, + target, + selector.0, + recipients, + tx, + send_tx, + ) + .await + } + Self::SetLimit { key_address, token, amount, period, tx, send_tx } => { + run_policy_set_limit(key_address, token, amount, period, tx, send_tx).await + } + Self::RemoveTarget { key_address, target, tx, send_tx } => { + run_remove_scope(key_address, target, tx, send_tx).await + } } } } @@ -500,6 +704,143 @@ fn run_show(wallet_address: Address) -> Result<()> { Ok(()) } +#[derive(Debug, Clone)] +struct LocalLimitMetadata { + token: Address, + amount: String, +} + +#[derive(Debug, Clone)] +struct KeyMetadata { + root_account: Address, + key_type: Option, + limits: Vec, +} + +#[derive(Debug, Clone)] +struct InspectedLimit { + token: Address, + configured_amount: Option, + remaining: U256, + period_end: Option, +} + +#[derive(Debug, Clone)] +enum AllowedCallsView { + Unsupported, + Unrestricted, + Scoped(Vec), +} + +/// `cast keychain inspect ` — inspect on-chain key policy. +async fn run_inspect( + key_address: Address, + root_account: Option
, + rpc: RpcOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let info: KeyInfo = provider.get_keychain_key(metadata.root_account, key_address).await?; + let provisioned = info.keyId != Address::ZERO; + let is_t3 = is_tempo_hardfork_active(&provider, TempoHardfork::T3).await?; + + let mut limits = Vec::new(); + if info.enforceLimits { + for local_limit in &metadata.limits { + let (remaining, period_end) = if is_t3 { + let limit = provider + .get_keychain_remaining_limit_with_period( + metadata.root_account, + key_address, + local_limit.token, + ) + .await?; + (limit.remaining, Some(limit.periodEnd)) + } else { + let remaining = provider + .account_keychain() + .getRemainingLimit(metadata.root_account, key_address, local_limit.token) + .call() + .await?; + (remaining, None) + }; + + limits.push(InspectedLimit { + token: local_limit.token, + configured_amount: Some(local_limit.amount.clone()), + remaining, + period_end, + }); + } + } + + let allowed_calls = if is_t3 { + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + if allowed.isScoped { + AllowedCallsView::Scoped(allowed.scopes) + } else { + AllowedCallsView::Unrestricted + } + } else { + AllowedCallsView::Unsupported + }; + + if shell::is_json() { + let key_type = if provisioned { + signature_type_name(&info.signatureType).to_string() + } else { + metadata + .key_type + .map(|key_type| key_type_name(&key_type).to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }; + let json = serde_json::json!({ + "root_account": metadata.root_account.to_string(), + "key_id": key_address.to_string(), + "provisioned": provisioned, + "type": key_type, + "expiry": provisioned.then_some(info.expiry), + "expiry_human": provisioned.then(|| format_expiry_for_inspect(info.expiry)), + "enforce_limits": info.enforceLimits, + "is_revoked": info.isRevoked, + "limits": limits.iter().map(inspected_limit_to_json).collect::>(), + "allowed_calls": allowed_calls_to_json(&allowed_calls), + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + return Ok(()); + } + + let key_type = if provisioned { + signature_type_label(&info.signatureType) + } else { + metadata.key_type.map(|key_type| key_type_label(&key_type)).unwrap_or("unknown") + }; + + sh_println!("Root account: {}", metadata.root_account)?; + sh_println!("Key id: {key_address}")?; + sh_println!("Type: {key_type}")?; + + if info.isRevoked { + sh_println!("Status: revoked")?; + } else if !provisioned { + sh_println!("Status: not provisioned")?; + } else { + sh_println!("Status: active")?; + sh_println!("Expiry: {}", format_expiry_for_inspect(info.expiry))?; + } + + print_inspected_limits(info.enforceLimits, &limits)?; + print_allowed_calls(&allowed_calls)?; + + Ok(()) +} + /// `cast keychain check` / `cast keychain info` — query on-chain key status. async fn run_check(wallet_address: Address, key_address: Address, rpc: RpcOpts) -> Result<()> { let config = rpc.load_config()?; @@ -584,7 +925,7 @@ async fn run_authorize( let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let calldata = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let calldata = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { // T3+ authorizeKey(address,SignatureType,KeyRestrictions) let restrictions = KeyRestrictions { expiry, @@ -634,7 +975,7 @@ async fn run_remaining_limit( let config = rpc.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let remaining: U256 = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let remaining: U256 = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { provider.get_keychain_remaining_limit(wallet_address, key_address, token).await? } else { // Pre-T3: use the legacy getRemainingLimit(address,address,address) @@ -646,7 +987,7 @@ async fn run_remaining_limit( }; if shell::is_json() { - sh_println!("{}", serde_json::to_string(&remaining.to_string())?)?; + sh_println!("{}", serde_json::json!({ "remaining": remaining.to_string() }))?; } else { sh_println!("{remaining}")?; } @@ -695,6 +1036,88 @@ async fn run_remove_scope( send_keychain_tx(calldata, tx_opts, &send_tx).await } +/// `cast keychain policy add-call` — merge a selector rule into a target scope. +async fn run_policy_add_call( + key_address: Address, + root_account: Option
, + target: Address, + selector: [u8; 4], + recipients: Vec
, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = send_tx.eth.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + if !is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { + eyre::bail!("allowed-call policy editing requires the Tempo T3 hardfork"); + } + + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + + let new_rule = SelectorRule { selector: selector.into(), recipients }; + let existing_target = allowed + .isScoped + .then(|| allowed.scopes.into_iter().find(|scope| scope.target == target)) + .flatten(); + + let (target_scope, changed) = match existing_target { + Some(mut scope) => { + if scope.selectorRules.is_empty() { + sh_warn!( + "Allowed calls for {} already allow any selector; leaving wildcard scope unchanged", + address_label_with_address(target) + )?; + } + let changed = add_selector_rule_to_scope(&mut scope, new_rule); + (scope, changed) + } + None => (CallScope { target, selectorRules: vec![new_rule] }, true), + }; + + if !changed { + if shell::is_json() { + sh_println!( + "{}", + serde_json::json!({ "status": "already_present", "target": target.to_string() }) + )?; + } else { + sh_println!("Allowed call already present for {}", address_label_with_address(target))?; + } + return Ok(()); + } + + let calldata = + IAccountKeychain::setAllowedCallsCall { keyId: key_address, scopes: vec![target_scope] } + .abi_encode(); + send_keychain_tx(calldata, tx_opts, &send_tx).await +} + +/// `cast keychain policy set-limit` — update a spending limit amount. +async fn run_policy_set_limit( + key_address: Address, + token: Address, + amount: U256, + period: Option, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + if period.is_some_and(|period| period != 0) { + eyre::bail!( + "--period is not supported by the current AccountKeychain updateSpendingLimit \ + precompile; periods can only be set when authorizing a key" + ); + } + + // updateSpendingLimit authorizes against msg.sender; the root account is not part of calldata. + run_update_limit(key_address, token, amount, tx_opts, send_tx).await +} + /// Shared helper to send a keychain precompile transaction. async fn send_keychain_tx( calldata: Vec, @@ -702,16 +1125,22 @@ async fn send_keychain_tx( send_tx: &SendTxOpts, ) -> Result<()> { let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; + let tempo_sponsor = + if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? }; let config = send_tx.eth.load_config()?; let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); let provider = ProviderBuilder::::from_config(&config)?.build()?; - // Inject key_id for correct gas estimation with keychain signature overhead. - if let Some(ref ak) = tempo_access_key { - tx_opts.tempo.key_id = Some(ak.key_address); + if let Some(interval) = send_tx.poll_interval { + provider.client().set_poll_interval(Duration::from_secs(interval)); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx_opts.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx_opts.tempo, &config.root)?; + let builder = CastTxBuilder::new(&provider, tx_opts, &config) .await? .with_to(Some(NameOrAddress::Address(ACCOUNT_KEYCHAIN_ADDRESS))) @@ -719,27 +1148,70 @@ async fn send_keychain_tx( .with_code_sig_and_args(None, Some(hex::encode_prefixed(&calldata)), vec![]) .await?; - if let Some(ref ak) = tempo_access_key { - let signer = - signer.as_ref().ok_or_else(|| eyre::eyre!("signer required for access key"))?; - let (tx, _) = builder.build(ak.wallet_address).await?; - cast_send_with_access_key( - &provider, - tx, - signer, - ak, - send_tx.cast_async, - send_tx.confirmations, - timeout, - ) - .await?; + // Keychain management calls are authorized by the root account. Access keys can use their + // permissions, but cannot mutate their own key policy. + let browser = send_tx.browser.run::().await?; + + if print_sponsor_hash { + let from = if let Some(ref browser) = browser { + browser.address() + } else { + signer + .as_ref() + .ok_or_else(|| { + eyre::eyre!( + "--tempo.print-sponsor-hash requires a root account signer, such as \ + --browser, --private-key, or --keystore" + ) + })? + .address() + }; + + let (tx, _) = builder.build(from).await?; + let hash = tx + .compute_sponsor_hash(from) + .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?; + if shell::is_json() { + sh_println!("{}", serde_json::json!({ "sponsor_hash": format!("{hash:?}") }))?; + } else { + sh_println!("{hash:?}")?; + } + return Ok(()); + } + + if let Some(browser) = browser { + let chain = builder.chain(); + let (mut tx, _) = builder.build(browser.address()).await?; + if chain.is_tempo() + && let Some(gas) = tx.gas_limit() + { + tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); + } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } + + let tx_hash = browser.send_transaction_via_browser(tx).await?; + CastTxSender::new(&provider) + .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout) + .await?; + } else if tempo_access_key.is_some() { + eyre::bail!( + "keychain policy changes must be signed by the root account; the selected `--from` \ + resolved to a Tempo access key. Use `--browser` for passkey roots, or pass a root \ + account signer with `--private-key`, `--keystore`, Ledger, Trezor, AWS, GCP, or Turnkey." + ); } else { let signer = match signer { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (mut tx, _) = builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() @@ -753,6 +1225,361 @@ async fn send_keychain_tx( Ok(()) } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AnvilNodeInfo { + hard_fork: Option, + network: Option, +} + +async fn is_tempo_hardfork_active

(provider: &P, hardfork: TempoHardfork) -> Result +where + P: Provider, +{ + match provider.is_hardfork_active(hardfork).await { + Ok(active) => Ok(active), + Err(err) if is_rpc_method_not_found(&err) => { + match anvil_tempo_hardfork_active(provider, hardfork).await { + Ok(Some(active)) => Ok(active), + _ => Err(err.into()), + } + } + Err(err) => Err(err.into()), + } +} + +async fn anvil_tempo_hardfork_active

( + provider: &P, + hardfork: TempoHardfork, +) -> Result, TransportError> +where + P: Provider, +{ + let info = provider.raw_request::<_, AnvilNodeInfo>("anvil_nodeInfo".into(), ()).await?; + Ok(active_from_anvil_node_info(&info, hardfork)) +} + +fn active_from_anvil_node_info(info: &AnvilNodeInfo, hardfork: TempoHardfork) -> Option { + (info.network.as_deref() == Some("tempo")).then(|| { + info.hard_fork + .as_deref() + .and_then(|active_hardfork| active_hardfork.parse::().ok()) + .is_some_and(|active_hardfork| active_hardfork >= hardfork) + }) +} + +fn is_rpc_method_not_found(err: &TransportError) -> bool { + err.as_error_resp().is_some_and(|payload| payload.code == -32601) +} + +fn resolve_key_metadata( + key_address: Address, + root_account: Option

, +) -> Result { + let keys_file = read_tempo_keys_file(); + + if let Some(root_account) = root_account { + if let Some(keys_file) = keys_file.as_ref() + && let Some(entry) = keys_file.keys.iter().find(|entry| { + entry.wallet_address == root_account + && key_entry_effective_key(entry) == key_address + }) + { + return Ok(key_metadata_from_entry(entry)); + } + + return Ok(KeyMetadata { root_account, key_type: None, limits: Vec::new() }); + } + + let Some(keys_file) = keys_file.as_ref() else { + eyre::bail!( + "key {key_address} was not found because the local keys file could not be read at {}; pass --root-account", + tempo_keys_path_display() + ); + }; + + let matches: Vec<_> = keys_file + .keys + .iter() + .filter(|entry| key_entry_effective_key(entry) == key_address) + .collect(); + + if matches.is_empty() { + eyre::bail!( + "key {key_address} was not found in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let root_account = matches[0].wallet_address; + if matches.iter().any(|entry| entry.wallet_address != root_account) { + eyre::bail!( + "key {key_address} matches multiple root accounts in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let entry = + matches.iter().copied().find(|entry| !entry.limits.is_empty()).unwrap_or(matches[0]); + Ok(key_metadata_from_entry(entry)) +} + +fn key_entry_effective_key(entry: &tempo::KeyEntry) -> Address { + entry.key_address.unwrap_or(entry.wallet_address) +} + +fn key_metadata_from_entry(entry: &tempo::KeyEntry) -> KeyMetadata { + KeyMetadata { + root_account: entry.wallet_address, + key_type: Some(entry.key_type), + limits: entry + .limits + .iter() + .map(|limit| LocalLimitMetadata { token: limit.currency, amount: limit.limit.clone() }) + .collect(), + } +} + +fn tempo_keys_path_display() -> String { + tempo_keys_path() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "(unknown)".to_string()) +} + +fn add_selector_rule_to_scope(scope: &mut CallScope, rule: SelectorRule) -> bool { + if scope.selectorRules.is_empty() { + return false; + } + + let Some(existing_rule) = + scope.selectorRules.iter_mut().find(|existing| existing.selector == rule.selector) + else { + scope.selectorRules.push(rule); + return true; + }; + + if existing_rule.recipients.is_empty() { + return false; + } + + if rule.recipients.is_empty() { + existing_rule.recipients = Vec::new(); + return true; + } + + let mut changed = false; + for recipient in rule.recipients { + if !existing_rule.recipients.contains(&recipient) { + existing_rule.recipients.push(recipient); + changed = true; + } + } + changed +} + +fn inspected_limit_to_json(limit: &InspectedLimit) -> serde_json::Value { + serde_json::json!({ + "token": limit.token.to_string(), + "token_label": address_label(limit.token), + "configured_amount": limit.configured_amount.as_deref(), + "remaining": limit.remaining.to_string(), + "period_end": limit.period_end, + "period_end_human": limit.period_end.and_then(|period_end| { + (period_end != 0).then(|| format_period_end(period_end)) + }), + }) +} + +fn allowed_calls_to_json(allowed_calls: &AllowedCallsView) -> serde_json::Value { + match allowed_calls { + AllowedCallsView::Unsupported => serde_json::json!({ + "mode": "unsupported", + "scopes": [], + }), + AllowedCallsView::Unrestricted => serde_json::json!({ + "mode": "any", + "scopes": [], + }), + AllowedCallsView::Scoped(scopes) => serde_json::json!({ + "mode": if scopes.is_empty() { "none" } else { "scoped" }, + "scopes": scopes.iter().map(call_scope_to_json).collect::>(), + }), + } +} + +fn call_scope_to_json(scope: &CallScope) -> serde_json::Value { + serde_json::json!({ + "target": scope.target.to_string(), + "target_label": address_label(scope.target), + "selectors": scope.selectorRules.iter().map(selector_rule_to_json).collect::>(), + }) +} + +fn selector_rule_to_json(rule: &SelectorRule) -> serde_json::Value { + serde_json::json!({ + "selector": selector_hex(&rule.selector.0), + "signature": selector_signature(&rule.selector.0), + "recipients": rule.recipients.iter().map(ToString::to_string).collect::>(), + }) +} + +fn print_inspected_limits(enforce_limits: bool, limits: &[InspectedLimit]) -> Result<()> { + if !enforce_limits { + sh_println!("Limits: none")?; + return Ok(()); + } + + sh_println!("Limits:")?; + if limits.is_empty() { + sh_println!(" enforced, but no local limit metadata was found")?; + return Ok(()); + } + + for limit in limits { + let configured = limit.configured_amount.as_deref().unwrap_or("unknown"); + let period = limit + .period_end + .and_then(|period_end| { + (period_end != 0).then(|| format!(" ({})", format_period_end(period_end))) + }) + .unwrap_or_default(); + sh_println!( + " {}: {} / {} remaining{}", + address_label(limit.token), + limit.remaining, + configured, + period + )?; + } + + Ok(()) +} + +fn print_allowed_calls(allowed_calls: &AllowedCallsView) -> Result<()> { + match allowed_calls { + AllowedCallsView::Unsupported => sh_println!("Allowed calls: unsupported before T3")?, + AllowedCallsView::Unrestricted => sh_println!("Allowed calls: any")?, + AllowedCallsView::Scoped(scopes) if scopes.is_empty() => { + sh_println!("Allowed calls: none")?; + } + AllowedCallsView::Scoped(scopes) => { + sh_println!("Allowed calls:")?; + for scope in scopes { + sh_println!(" {}:", address_label_with_address(scope.target))?; + if scope.selectorRules.is_empty() { + sh_println!(" any selector")?; + continue; + } + + for rule in &scope.selectorRules { + sh_println!( + " {} -> {}", + format_selector(&rule.selector.0), + format_recipients(&rule.recipients) + )?; + } + } + } + } + + Ok(()) +} + +fn address_label(address: Address) -> String { + if address == PATH_USD_ADDRESS { "PathUSD".to_string() } else { address.to_string() } +} + +fn address_label_with_address(address: Address) -> String { + if address == PATH_USD_ADDRESS { format!("PathUSD ({address})") } else { address.to_string() } +} + +fn format_selector(selector: &[u8; 4]) -> String { + selector_signature(selector).map(str::to_string).unwrap_or_else(|| selector_hex(selector)) +} + +fn selector_signature(selector: &[u8; 4]) -> Option<&'static str> { + if selector == &ITIP20::transferCall::SELECTOR { + Some("transfer(address,uint256)") + } else if selector == &ITIP20::approveCall::SELECTOR { + Some("approve(address,uint256)") + } else if selector == &ITIP20::transferFromCall::SELECTOR { + Some("transferFrom(address,address,uint256)") + } else if selector == &ITIP20::transferWithMemoCall::SELECTOR { + Some("transferWithMemo(address,uint256,bytes32)") + } else if selector == &ITIP20::transferFromWithMemoCall::SELECTOR { + Some("transferFromWithMemo(address,address,uint256,bytes32)") + } else if selector == &ITIP20::mintCall::SELECTOR { + Some("mint(address,uint256)") + } else if selector == &ITIP20::burnCall::SELECTOR { + Some("burn(uint256)") + } else { + None + } +} + +fn selector_hex(selector: &[u8; 4]) -> String { + hex::encode_prefixed(selector) +} + +fn format_recipients(recipients: &[Address]) -> String { + if recipients.is_empty() { + return "any recipient".to_string(); + } + + let recipients = recipients.iter().map(ToString::to_string).collect::>().join(", "); + format!("recipients [{recipients}]") +} + +fn format_expiry_for_inspect(expiry: u64) -> String { + if expiry == u64::MAX { + return "never".to_string(); + } + + format!("{} ({})", format_timestamp_iso(expiry), format_relative_timestamp(expiry)) +} + +fn format_period_end(period_end: u64) -> String { + format!("period resets {}", format_relative_timestamp(period_end)) +} + +fn format_timestamp_iso(timestamp: u64) -> String { + DateTime::from_timestamp(timestamp as i64, 0) + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| timestamp.to_string()) +} + +fn format_relative_timestamp(timestamp: u64) -> String { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if timestamp == now { + "now".to_string() + } else if timestamp > now { + format!("in {}", format_duration_words(timestamp - now)) + } else { + format!("{} ago", format_duration_words(now - timestamp)) + } +} + +fn format_duration_words(seconds: u64) -> String { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + const DAY: u64 = 24 * HOUR; + + if seconds >= DAY { + let days = seconds / DAY; + if days == 1 { "1 day".to_string() } else { format!("{days} days") } + } else if seconds >= HOUR { + format!("{}h", seconds / HOUR) + } else if seconds >= MINUTE { + format!("{}m", seconds / MINUTE) + } else { + format!("{seconds}s") + } +} + fn format_expiry(expiry: u64) -> String { if expiry == u64::MAX { return "never".to_string(); @@ -842,6 +1669,7 @@ fn key_entry_to_json(entry: &tempo::KeyEntry) -> serde_json::Value { #[cfg(test)] mod tests { use super::*; + use alloy_json_rpc::ErrorPayload; use std::str::FromStr; #[test] @@ -967,4 +1795,144 @@ mod tests { let json = r#"[{"target":"0x20c0000000000000000000000000000000000001","selectors":[{"selector":"transfer","recipients":[],"bogus":true}]}]"#; assert!(parse_scopes_json(json).is_err()); } + + #[test] + fn test_parse_policy_token_path_usd() { + assert_eq!(parse_policy_token("PathUSD").unwrap(), PATH_USD_ADDRESS); + assert_eq!(parse_policy_token("path-usd").unwrap(), PATH_USD_ADDRESS); + } + + #[test] + fn test_parse_period_units() { + assert_eq!(parse_period("0").unwrap(), 0); + assert_eq!(parse_period("30s").unwrap(), 30); + assert_eq!(parse_period("5m").unwrap(), 300); + assert_eq!(parse_period("2h").unwrap(), 7200); + assert_eq!(parse_period("7d").unwrap(), 604800); + assert_eq!(parse_period("2w").unwrap(), 1209600); + assert!(parse_period("1mo").is_err()); + } + + #[test] + fn test_add_selector_rule_merges_recipients() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let second = Address::from_str("0x2222222222222222222222222222222222222222").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![second], + }, + ); + + assert!(changed); + assert_eq!(scope.selectorRules.len(), 1); + assert_eq!(scope.selectorRules[0].recipients, vec![first, second]); + } + + #[test] + fn test_add_selector_rule_empty_recipients_widens_to_any() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(changed); + assert!(scope.selectorRules[0].recipients.is_empty()); + } + + #[test] + fn test_add_selector_rule_target_wildcard_is_unchanged() { + let mut scope = CallScope { target: PATH_USD_ADDRESS, selectorRules: vec![] }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(!changed); + assert!(scope.selectorRules.is_empty()); + } + + #[test] + fn test_policy_set_limit_parses() { + let key = "0x1111111111111111111111111111111111111111"; + + let command = KeychainSubcommand::try_parse_from([ + "keychain", + "policy", + "set-limit", + key, + "--token", + "PathUSD", + "--amount", + "123", + ]) + .unwrap(); + + match command { + KeychainSubcommand::Policy { + command: + KeychainPolicySubcommand::SetLimit { key_address, token, amount, period, .. }, + } => { + assert_eq!(key_address, Address::from_str(key).unwrap()); + assert_eq!(token, PATH_USD_ADDRESS); + assert_eq!(amount, U256::from(123)); + assert_eq!(period, None); + } + other => panic!("unexpected command: {other:?}"), + } + } + + #[test] + fn test_active_from_anvil_node_info_requires_tempo_network() { + let tempo_t3 = + AnvilNodeInfo { network: Some("tempo".to_string()), hard_fork: Some("T3".to_string()) }; + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T2), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T3), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T4), Some(false)); + + let ethereum_t3 = AnvilNodeInfo { + network: Some("ethereum".to_string()), + hard_fork: Some("T3".to_string()), + }; + assert_eq!(active_from_anvil_node_info(ðereum_t3, TempoHardfork::T3), None); + } + + #[test] + fn test_rpc_method_not_found_detection() { + let method_missing: TransportError = + TransportError::ErrorResp(ErrorPayload::method_not_found()); + assert!(is_rpc_method_not_found(&method_missing)); + + let internal_error: TransportError = + TransportError::ErrorResp(ErrorPayload::internal_error()); + assert!(!is_rpc_method_not_found(&internal_error)); + + let transport_error = alloy_transport::TransportErrorKind::backend_gone(); + assert!(!is_rpc_method_not_found(&transport_error)); + } } diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index 8aaf6e97a1827..67178cd093d7b 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -2,7 +2,9 @@ use crate::tx::{self, CastTxBuilder}; use alloy_consensus::{SignableTransaction, Signed}; use alloy_eips::Encodable2718; use alloy_ens::NameOrAddress; -use alloy_network::{Ethereum, EthereumWallet, Network, NetworkTransactionBuilder}; +use alloy_network::{ + Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, TransactionBuilder, +}; use alloy_primitives::{Address, hex}; use alloy_provider::Provider; use alloy_signer::{Signature, Signer}; @@ -10,7 +12,7 @@ use clap::Parser; use eyre::Result; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::LoadConfig, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use std::{path::PathBuf, str::FromStr}; @@ -94,9 +96,13 @@ impl MakeTxArgs { N::UnsignedTx: SignableTransaction, N::TransactionRequest: FoundryTransactionBuilder, { - let Self { to, mut sig, mut args, command, tx, path, eth, raw_unsigned, ethsign } = self; + let Self { to, mut sig, mut args, command, mut tx, path, eth, raw_unsigned, ethsign } = + self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -117,6 +123,11 @@ impl MakeTxArgs { let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + // Must happen before `tx.clone()` so the cloned tx carries the resolved nonce_key. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config) .await? .with_to(to) @@ -139,6 +150,10 @@ impl MakeTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + if raw_unsigned { // Build unsigned raw tx // Check if nonce is provided when --from is not specified @@ -148,11 +163,20 @@ impl MakeTxArgs { "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce" ); } + if tempo_sponsor.is_some() && eth.wallet.from.is_none() { + eyre::bail!( + "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender" + ); + } // Use zero address as placeholder for unsigned transactions let from = eth.wallet.from.unwrap_or(Address::ZERO); - let (tx, _) = tx_builder.build(from).await?; + let (mut tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -162,7 +186,11 @@ impl MakeTxArgs { if ethsign { // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has // unlocked accounts. - let (tx, _) = tx_builder.build(config.sender).await?; + let (mut tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, config.sender).await?; + } let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; @@ -176,7 +204,11 @@ impl MakeTxArgs { tx::validate_from_address(eth.wallet.from, from)?; - let (tx, _) = tx_builder.build(&signer).await?; + let (mut tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let tx = tx.build(&EthereumWallet::new(signer)).await?; diff --git a/crates/cast/src/cmd/mod.rs b/crates/cast/src/cmd/mod.rs index 6a9d11f5dc61d..0b1b26615694a 100644 --- a/crates/cast/src/cmd/mod.rs +++ b/crates/cast/src/cmd/mod.rs @@ -15,6 +15,7 @@ pub mod call; pub mod constructor_args; pub mod create2; pub mod creation_code; +#[cfg(feature = "optimism")] pub mod da_estimate; pub mod erc20; pub mod estimate; @@ -28,7 +29,9 @@ pub mod rpc; pub mod run; pub mod send; pub mod storage; +pub mod tempo; pub mod tip20; pub mod trace; pub mod txpool; +pub mod vaddr; pub mod wallet; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 84699d6cd6956..7e52a9e265f25 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -29,10 +29,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock as _, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, }, executors::{EvmError, Executor, TracingExecutor}, hardforks::FoundryHardfork, @@ -123,12 +125,15 @@ impl RunArgs { evm_opts.infer_network_from_fork().await; if evm_opts.networks.is_tempo() { - self.run_with_evm::().await - } else if evm_opts.networks.is_optimism() { - self.run_with_evm::().await - } else { - self.run_with_evm::().await + return self.run_with_evm::().await; + } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_evm::().await; } + + self.run_with_evm::().await } async fn run_with_evm(self) -> Result<()> { diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 2d6e248cc7a73..421aae1f2153e 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -8,7 +8,10 @@ use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::{Signature, Signer}; use clap::Parser; use eyre::{Result, eyre}; -use foundry_cli::{opts::TransactionOpts, utils::LoadConfig}; +use foundry_cli::{ + opts::TransactionOpts, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, +}; use foundry_common::{ FoundryTransactionBuilder, fmt::{UIfmt, UIfmtReceiptExt}, @@ -119,7 +122,9 @@ impl SendTxArgs { self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; - let sponsor_signature = tx.tempo.sponsor_signature; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -183,6 +188,8 @@ impl SendTxArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -202,13 +209,19 @@ impl SendTxArgs { // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit. if print_sponsor_hash { - // Use the pre-resolved signer to derive the actual sender address, since the - // sponsor hash commits to the sender. - let signer = pre_resolved_signer.as_ref().ok_or_else(|| { - eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") - })?; - let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (tx, from) = if let Some(ref ak) = access_key { + let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?; + (tx, ak.wallet_address) + } else { + // Use the pre-resolved signer to derive the actual sender address, since the + // sponsor hash commits to the sender. + let signer = pre_resolved_signer.as_ref().ok_or_else(|| { + eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") + })?; + let from = signer.address(); + let (tx, _) = builder.build(from).await?; + (tx, from) + }; let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?; @@ -216,6 +229,10 @@ impl SendTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); // Launch browser signer if `--browser` flag is set @@ -245,11 +262,18 @@ impl SendTxArgs { } } - let (tx, _) = builder.build(config.sender).await?; + let (mut tx_request, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, config.sender).await?; + } cast_send( provider, - tx, + tx_request, send_tx.cast_async, send_tx.sync, send_tx.confirmations, @@ -261,6 +285,10 @@ impl SendTxArgs { } else if let Some(browser) = browser { let chain = builder.chain(); let (mut tx_request, _) = builder.build(browser.address()).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which // costs more gas for signature verification on Tempo chains. Add a @@ -270,6 +298,9 @@ impl SendTxArgs { { tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, browser.address()).await?; + } let tx_hash = browser.send_transaction_via_browser(tx_request).await?; @@ -283,7 +314,14 @@ impl SendTxArgs { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; - let (tx_request, _) = builder.build(ak.wallet_address).await?; + let (mut tx_request, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, ak.wallet_address).await?; + } cast_send_with_access_key( &provider, tx_request, @@ -308,11 +346,13 @@ impl SendTxArgs { tx::validate_from_address(send_tx.eth.wallet.from, from)?; let (mut tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; - // Apply sponsor signature after gas estimation so the estimate is - // consistent with what `--tempo.print-sponsor-hash` computes. - if let Some(sig) = sponsor_signature { - tx_request.set_fee_payer_signature(sig); + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, from).await?; } let wallet = EthereumWallet::from(signer); diff --git a/crates/cast/src/cmd/tempo.rs b/crates/cast/src/cmd/tempo.rs new file mode 100644 index 0000000000000..6744e6b73c039 --- /dev/null +++ b/crates/cast/src/cmd/tempo.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use eyre::Result; +use foundry_common::tempo::{EnsureAccessKeyConfig, ensure_access_key}; + +/// Tempo wallet integration commands. +#[derive(Debug, Parser)] +pub enum TempoSubcommand { + /// Authorize a new access key against your Tempo wallet via wallet.tempo. + /// + /// Persists the key to `$TEMPO_HOME/wallet/keys.toml` (default + /// `~/.tempo/wallet/keys.toml`). Also runs automatically on a 402 from a + /// Tempo RPC when no local key is configured. + /// + /// Env: `TEMPO_HOME`, `TEMPO_CLI_AUTH_URL` (override auth service). + Login { + /// Chain ID to authorize the key for. Defaults to Tempo mainnet (4217). + #[arg(long, default_value_t = 4217)] + chain_id: u64, + + /// Print the authorization URL to stderr instead of opening a browser. + #[arg(long)] + no_browser: bool, + }, +} + +impl TempoSubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::Login { chain_id, no_browser } => { + let mut cfg = EnsureAccessKeyConfig::from_env(chain_id); + if no_browser { + cfg.no_browser = true; + } + let outcome = ensure_access_key(cfg).await?; + let _ = foundry_common::sh_println!( + "Authorized key {} for wallet {} on chain {}", + outcome.key_address, + outcome.wallet_address, + outcome.chain_id, + ); + Ok(()) + } + } + } +} diff --git a/crates/cast/src/cmd/tip20/mine.rs b/crates/cast/src/cmd/tip20/mine.rs index a5f9062482a01..0367450a19b06 100644 --- a/crates/cast/src/cmd/tip20/mine.rs +++ b/crates/cast/src/cmd/tip20/mine.rs @@ -20,11 +20,11 @@ use tempo_primitives::{MasterId, TempoAddressExt, UserTag}; const POW_BYTES: usize = 4; -pub(super) struct Output { - pub(super) salt: B256, - pub(super) registration_hash: B256, - pub(super) master_id: MasterId, - pub(super) zero_tag_virtual_address: Address, +pub(crate) struct Output { + pub(crate) salt: B256, + pub(crate) registration_hash: B256, + pub(crate) master_id: MasterId, + pub(crate) zero_tag_virtual_address: Address, } pub(super) fn run( @@ -127,7 +127,12 @@ pub(super) async fn register( Ok(()) } -fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Result { +pub(crate) fn mine( + master: Address, + salt: B256, + n_threads: usize, + pow_bytes: usize, +) -> Result { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); @@ -144,7 +149,7 @@ fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Resu .ok_or_else(|| eyre::eyre!("virtual master mining failed: all threads panicked")) } -fn derive(master: Address, salt: B256) -> Output { +pub(crate) fn derive(master: Address, salt: B256) -> Output { let registration_hash = registration_hash(master, salt); let master_id = MasterId::from_slice(®istration_hash[4..8]); let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO); @@ -152,14 +157,14 @@ fn derive(master: Address, salt: B256) -> Output { Output { salt, registration_hash, master_id, zero_tag_virtual_address } } -fn registration_hash(master: Address, salt: B256) -> B256 { +pub(crate) fn registration_hash(master: Address, salt: B256) -> B256 { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); packed[20..].copy_from_slice(salt.as_slice()); keccak256(packed) } -fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { +pub(crate) fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { registration_hash[..pow_bytes].iter().all(|byte| *byte == 0) } diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index e3c39b2c9bb18..edb4c3b7a57b3 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; mod create; pub(crate) use create::iso4217_warning_message; -mod mine; +pub(crate) mod mine; /// TIP-20 token operations (Tempo). #[derive(Debug, Parser, Clone)] diff --git a/crates/cast/src/cmd/vaddr/create.rs b/crates/cast/src/cmd/vaddr/create.rs new file mode 100644 index 0000000000000..0563b09601323 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/create.rs @@ -0,0 +1,181 @@ +use crate::{ + cmd::{ + erc20::build_provider_with_signer, + send::{cast_send, cast_send_with_access_key}, + tip20::mine, + }, + tx::{SendTxOpts, TxParams}, +}; +use alloy_primitives::{Address, B256}; +use alloy_signer::Signer; +use eyre::Result; +use foundry_cli::utils::{LoadConfig, get_chain}; +use foundry_common::{provider::ProviderBuilder, shell}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; +use serde_json::json; +use std::time::Instant; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; +use tempo_primitives::{TempoAddressExt, UserTag}; + +const POW_BYTES: usize = 4; + +#[allow(clippy::too_many_arguments)] +pub(super) async fn run( + owner: Address, + salt: Option, + tag: u64, + count: u32, + threads: Option, + seed: Option, + no_random: bool, + no_register: bool, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + if count == 0 { + // no virtual addresses to compute + return Ok(()); + } + + if !owner.is_valid_master() { + eyre::bail!( + "invalid owner address {owner}; see https://docs.tempo.xyz/protocol/tips/tip-1022" + ); + } + + let output = if let Some(salt) = salt { + let output = mine::derive(owner, salt); + if !mine::has_pow(&output.registration_hash, POW_BYTES) { + eyre::bail!( + "provided salt does not satisfy TIP-1022 proof of work: {}", + output.registration_hash + ); + } + output + } else { + let mut n_threads = threads.unwrap_or(0); + if n_threads == 0 { + n_threads = std::thread::available_parallelism().map_or(1, |n| n.get()); + } + + let mut start_salt = B256::ZERO; + if !no_random { + let mut rng = match seed { + Some(seed) => StdRng::from_seed(seed.0), + None => StdRng::from_os_rng(), + }; + rng.fill_bytes(&mut start_salt[..]); + } + + if !shell::is_json() { + sh_println!("Mining TIP-1022 salt for {owner} with {n_threads} threads...")?; + } + let timer = Instant::now(); + let output = mine::mine(owner, start_salt, n_threads, POW_BYTES)?; + if !shell::is_json() { + sh_println!("Found salt in {:?}", timer.elapsed())?; + } + output + }; + + const MAX_USER_TAG: u64 = 0x0000_FFFF_FFFF_FFFF; + let mut virtual_addresses = Vec::with_capacity(count as usize); + for i in 0..count { + let tag_value = tag + .checked_add(i as u64) + .filter(|&t| t <= MAX_USER_TAG) + .ok_or_else(|| eyre::eyre!("tag overflow: tag + count exceeds the 6-byte user tag range (max {MAX_USER_TAG:#x})"))?; + let raw = tag_value.to_be_bytes(); + let user_tag = UserTag::new(raw[2..].try_into().expect("slice is 6 bytes")); + let vaddr = Address::new_virtual(output.master_id, user_tag); + virtual_addresses.push((user_tag, vaddr)); + } + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "salt": format!("{}", output.salt), + "registration_hash": format!("{}", output.registration_hash), + "master_id": format!("{}", output.master_id), + "virtual_addresses": virtual_addresses.iter().map(|(tag, addr)| json!({ + "tag": format!("{tag}"), + "address": format!("{addr}"), + })).collect::>(), + }))? + )?; + } else { + sh_println!( + "Salt: {} +Registration hash: {} +Master ID: {}", + output.salt, + output.registration_hash, + output.master_id, + )?; + sh_println!("\nVirtual addresses:")?; + for (tag, vaddr) in &virtual_addresses { + sh_println!(" tag={tag} {vaddr}")?; + } + } + + if no_register { + return Ok(()); + } + + register(owner, output.salt, send_tx, tx_opts).await +} + +async fn register( + owner: Address, + salt: B256, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let signer = signer.ok_or_else(|| { + eyre::eyre!("cast vaddr create requires a signer (for example --private-key or --from)") + })?; + + let sender = + tempo_access_key.as_ref().map(|ak| ak.wallet_address).unwrap_or_else(|| signer.address()); + + if sender != owner { + eyre::bail!( + "signer mismatch: salt is for {owner}, but the configured signer would register as {sender}" + ); + } + + let config = send_tx.eth.load_config()?; + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider) + .registerVirtualMaster(salt) + .into_transaction_request(); + tx_opts.apply::(&mut tx, get_chain(config.chain, &provider).await?.is_legacy()); + + sh_println!("Submitting registerVirtualMaster({salt})...")?; + + if let Some(ref access_key) = tempo_access_key { + cast_send_with_access_key( + &provider, + tx, + &signer, + access_key, + send_tx.cast_async, + send_tx.confirmations, + timeout, + ) + .await?; + } else { + let provider = build_provider_with_signer::(&send_tx, signer)?; + cast_send(provider, tx, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout) + .await?; + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/mod.rs b/crates/cast/src/cmd/vaddr/mod.rs new file mode 100644 index 0000000000000..446d1e7ece5e2 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/mod.rs @@ -0,0 +1,131 @@ +use crate::tx::{SendTxOpts, TxParams}; +use alloy_primitives::{Address, B256}; +use clap::Parser; +use foundry_cli::opts::RpcOpts; + +mod create; +mod resolve; +mod watch; + +/// TIP-1022 virtual address registry operations (Tempo). +/// +/// Virtual addresses are deterministic 20-byte aliases (masterId || VIRTUAL_MAGIC || userTag) +/// that auto-forward TIP-20 deposits to a registered master wallet at the protocol level, +/// with no on-chain sweep transaction required. +/// +/// See: +#[derive(Debug, Parser, Clone)] +pub enum VaddrSubcommand { + /// Mine a TIP-1022 proof-of-work salt, register as a virtual address master, and print + /// derived virtual addresses for the given owner. + #[command(visible_alias = "c")] + Create { + /// The master (owner) address that will control all virtual addresses under this + /// registration. Must not be the zero address, a virtual address, or a TIP-20 token. + #[arg(long, value_name = "ADDRESS")] + owner: Address, + + /// Use this salt directly instead of mining one. Must satisfy the 32-bit PoW requirement. + #[arg(long, conflicts_with_all = ["seed", "no_random"], value_name = "HEX")] + salt: Option, + + /// Starting user tag for the derived virtual address output (hex-encoded 6 bytes). + #[arg(long, default_value = "0", value_name = "U64")] + tag: u64, + + /// Number of virtual addresses to derive and print. + #[arg(long, default_value = "1", value_name = "N")] + count: u32, + + /// Number of threads to use for mining. Defaults to number of logical cores. + #[arg(long, short = 'j', visible_alias = "jobs")] + threads: Option, + + /// Seed for the random number generator used to initialize the salt search. + #[arg(long, value_name = "HEX")] + seed: Option, + + /// Start salt search from zero instead of a random value. + #[arg(long, conflicts_with = "seed")] + no_random: bool, + + /// Mine and print the salt and derived virtual addresses without submitting the + /// registerVirtualMaster transaction. + #[arg(long)] + no_register: bool, + + #[command(flatten)] + send_tx: Box, + + #[command(flatten)] + tx: Box, + }, + + /// Resolve a virtual address to its registered master and decode its components. + #[command(visible_alias = "r")] + Resolve { + /// The virtual address to resolve. + #[arg(value_name = "ADDRESS")] + addr: Address, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Watch (tail) incoming TIP-20 transfers to a virtual address. + #[command(visible_alias = "w")] + Watch { + /// The virtual address to monitor. + #[arg(value_name = "ADDRESS")] + addr: Address, + + /// Filter on a specific TIP-20 token address. Watches all tokens if omitted. + #[arg(long, value_name = "ADDRESS")] + token: Option
, + + /// Block number to start from. Defaults to the current latest block. + #[arg(long, value_name = "BLOCK")] + from_block: Option, + + #[command(flatten)] + rpc: RpcOpts, + }, +} + +impl VaddrSubcommand { + pub async fn run(self) -> eyre::Result<()> { + match self { + Self::Create { + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + send_tx, + tx, + } => { + create::run( + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + *send_tx, + *tx, + ) + .await? + } + Self::Resolve { addr, rpc } => resolve::run(addr, rpc).await?, + Self::Watch { addr, token, from_block, rpc } => { + watch::run(addr, token, from_block, rpc).await? + } + } + Ok(()) + } +} diff --git a/crates/cast/src/cmd/vaddr/resolve.rs b/crates/cast/src/cmd/vaddr/resolve.rs new file mode 100644 index 0000000000000..96936f4fe4713 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/resolve.rs @@ -0,0 +1,52 @@ +use alloy_primitives::{Address, hex}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; + +pub(super) async fn run(addr: Address, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let registry = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider); + + let decode_builder = registry.decodeVirtualAddress(addr); + let resolve_builder = registry.resolveVirtualAddress(addr); + let (decoded, master) = tokio::try_join!(decode_builder.call(), resolve_builder.call())?; + + if !decoded.isVirtual { + sh_println!("{addr} is not a virtual address")?; + return Ok(()); + } + + let master_id = decoded.masterId; + let user_tag = decoded.userTag; + let master: Address = master; + + if shell::is_json() { + let master_address = if master.is_zero() { None } else { Some(format!("{master}")) }; + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "address": format!("{addr}"), + "master_id": format!("0x{}", hex::encode(master_id)), + "user_tag": format!("0x{}", hex::encode(user_tag)), + "master_address": master_address, + }))? + )?; + } else { + sh_println!("Virtual address: {addr}")?; + sh_println!("Master ID: 0x{}", hex::encode(master_id))?; + sh_println!("User tag: 0x{}", hex::encode(user_tag))?; + if master.is_zero() { + sh_println!("Master address: (unregistered)")?; + } else { + sh_println!("Master address: {master}")?; + } + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/watch.rs b/crates/cast/src/cmd/vaddr/watch.rs new file mode 100644 index 0000000000000..dc159d5e37c8e --- /dev/null +++ b/crates/cast/src/cmd/vaddr/watch.rs @@ -0,0 +1,108 @@ +use alloy_primitives::{Address, B256, keccak256}; +use alloy_provider::Provider; +use alloy_rpc_types::{BlockNumberOrTag, Filter}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use std::sync::LazyLock; +use tempo_alloy::TempoNetwork; +use tempo_primitives::TempoAddressExt; + +static TRANSFER_TOPIC: LazyLock = + LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)")); + +pub(super) async fn run( + addr: Address, + token: Option
, + from_block: Option, + rpc: RpcOpts, +) -> Result<()> { + if !addr.is_virtual() { + eyre::bail!("{addr} is not a virtual address"); + } + + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + // Transfer(address indexed from, address indexed to, uint256 value) + // topic[0] = event sig, topic[1] = from, topic[2] = to + let to_topic: B256 = { + let mut buf = [0u8; 32]; + buf[12..].copy_from_slice(addr.as_slice()); + buf.into() + }; + + let start = from_block.map(BlockNumberOrTag::Number).unwrap_or(BlockNumberOrTag::Latest); + + let mut filter = + Filter::new().event_signature(*TRANSFER_TOPIC).topic2(to_topic).from_block(start); + + if let Some(tok) = token { + filter = filter.address(tok); + } + + if !shell::is_json() { + sh_println!("Watching transfers to {addr}... (Ctrl-C to stop)")?; + } + + // Fetch logs from the requested start block (historical when from_block is set) + let logs = provider.get_logs(&filter).await?; + for log in &logs { + print_transfer_log(log)?; + } + + // Poll for new logs + let mut last_block = provider.get_block_number().await?; + loop { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let current = provider.get_block_number().await?; + if current > last_block { + let poll_filter = filter.clone().from_block(last_block + 1).to_block(current); + let new_logs = provider.get_logs(&poll_filter).await?; + for log in &new_logs { + print_transfer_log(log)?; + } + last_block = current; + } + } +} + +fn print_transfer_log(log: &alloy_rpc_types::Log) -> Result<()> { + let block = log.block_number.unwrap_or(0); + let tx = log.transaction_hash.unwrap_or_default(); + let token = log.address(); + + // Decode topics: topic[1]=from, topic[2]=to + let from = log.topics().get(1).map(|t| { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&t[12..]); + Address::from(addr) + }); + + // Decode amount from data + let amount = if log.data().data.len() >= 32 { + alloy_primitives::U256::from_be_slice(&log.data().data[..32]) + } else { + alloy_primitives::U256::ZERO + }; + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string(&json!({ + "block": block, + "tx": format!("{tx}"), + "token": format!("{token}"), + "from": from.map(|a| format!("{a}")).unwrap_or_default(), + "amount": amount.to_string(), + }))? + )?; + } else { + sh_println!( + "block={block} tx={tx} token={token} from={} amount={amount}", + from.map(|a| a.to_string()).unwrap_or_default(), + )?; + } + Ok(()) +} diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 8e0dd8dd3ed8c..b2378d8bfdc58 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -779,8 +779,7 @@ flag to set your key via: )?; let address = wallet.address(); let success_message = format!( - "`{}` keystore was saved successfully. Address: {:?}", - &account_name, address, + "`{account_name}` keystore was saved successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } @@ -815,7 +814,7 @@ flag to set your key via: format!("Failed to remove keystore file at {}", keystore_path.display()) })?; - let success_message = format!("`{}` keystore was removed successfully.", &name); + let success_message = format!("`{name}` keystore was removed successfully."); sh_println!("{}", success_message.green())?; } Self::PrivateKey { @@ -886,8 +885,7 @@ flag to set your key via: let private_key = B256::from_slice(&wallet.credential().to_bytes()); - let success_message = - format!("{}'s private key is: {}", &account_name, private_key); + let success_message = format!("{account_name}'s private key is: {private_key}"); sh_println!("{}", success_message.green())?; } @@ -945,10 +943,9 @@ flag to set your key via: Some(&account_name), )?; + let address = wallet.address(); let success_message = format!( - "Password for keystore `{}` was changed successfully. Address: {:?}", - &account_name, - wallet.address(), + "Password for keystore `{account_name}` was changed successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index ce5572acebc13..2b1b03486bf04 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -40,6 +40,7 @@ use foundry_common::{ use foundry_config::Chain; use foundry_evm::core::bytecode::InstIter; use futures::{FutureExt, StreamExt, future::Either}; +#[cfg(feature = "optimism")] use op_alloy_consensus as _; use rayon::prelude::*; @@ -60,6 +61,7 @@ pub use foundry_evm::*; pub mod args; pub mod cmd; pub mod opts; +pub mod tempo; pub mod base; pub mod call_spec; @@ -246,7 +248,7 @@ impl + Clone + Unpin, N: Network> Cast { let mut s = vec![format!("gas used: {}", access_list.gas_used), "access list:".to_string()]; for al in access_list.access_list.0 { - s.push(format!("- address: {}", &al.address.to_checksum(None))); + s.push(format!("- address: {}", al.address.to_checksum(None))); if !al.storage_keys.is_empty() { s.push(" keys:".to_string()); for key in al.storage_keys { diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index effc081e8072a..763eb132ddb5c 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -1,11 +1,13 @@ +#[cfg(feature = "optimism")] +use crate::cmd::da_estimate::DAEstimateArgs; use crate::cmd::{ access_list::AccessListArgs, artifact::ArtifactArgs, b2e_payload::B2EPayloadArgs, batch_mktx::BatchMakeTxArgs, batch_send::BatchSendArgs, bind::BindArgs, call::CallArgs, constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs, - da_estimate::DAEstimateArgs, erc20::Erc20Subcommand, estimate::EstimateArgs, - find_block::FindBlockArgs, interface::InterfaceArgs, keychain::KeychainSubcommand, - logs::LogsArgs, mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, - storage::StorageArgs, tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, + erc20::Erc20Subcommand, estimate::EstimateArgs, find_block::FindBlockArgs, + interface::InterfaceArgs, keychain::KeychainSubcommand, logs::LogsArgs, mktx::MakeTxArgs, + rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, tempo::TempoSubcommand, + tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, vaddr::VaddrSubcommand, wallet::WalletSubcommands, }; use alloy_ens::NameOrAddress; @@ -1163,6 +1165,7 @@ pub enum CastSubcommand { command: TxPoolSubcommands, }, /// Estimates the data availability size of a given opstack block. + #[cfg(feature = "optimism")] #[command(name = "da-estimate")] DAEstimate(DAEstimateArgs), @@ -1186,6 +1189,20 @@ pub enum CastSubcommand { #[command(subcommand)] command: KeychainSubcommand, }, + + /// Tempo wallet integration (login, etc.). + Tempo { + #[command(subcommand)] + command: TempoSubcommand, + }, + + /// TIP-1022 virtual address registry operations (Tempo). + #[command(visible_alias = "vaddr")] + VirtualAddress { + #[command(subcommand)] + command: VaddrSubcommand, + }, + #[command(name = "trace")] Trace(TraceArgs), } diff --git a/crates/cast/src/tempo.rs b/crates/cast/src/tempo.rs new file mode 100644 index 0000000000000..737c33f5b70de --- /dev/null +++ b/crates/cast/src/tempo.rs @@ -0,0 +1,3 @@ +//! Tempo transaction helpers used by Cast-facing commands. + +pub use foundry_common::tempo::{TempoSponsor, TempoSponsorPreview, resolve_tempo_sponsor_signer}; diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 96f5fc5137575..b58136f4ae9de 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -20,7 +20,7 @@ use foundry_common::{ get_pretty_receipt_w_reason_attr, shell, }; use foundry_config::{Chain, Config}; -use foundry_wallets::{BrowserWalletOpts, WalletOpts, WalletSigner}; +use foundry_wallets::{BrowserWalletOpts, TempoAccessKeyConfig, WalletOpts, WalletSigner}; use itertools::Itertools; use serde_json::value::RawValue; use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration}; @@ -535,13 +535,29 @@ where sender: impl Into>, ) -> Result<(N::TransactionRequest, Option)> { let fill = self.fill; - self._build(sender, fill).await + self._build(sender, fill, None).await + } + + /// Builds a transaction that will be signed by a Tempo access key. + /// + /// The access-key id is set before gas estimation. If the access key needs on-chain + /// provisioning, its authorization is embedded before access-list/gas estimation and before + /// any sponsor digest can be computed. + pub async fn build_with_access_key( + mut self, + sender: impl Into>, + access_key: &TempoAccessKeyConfig, + ) -> Result<(N::TransactionRequest, Option)> { + self.tx.set_key_id(access_key.key_address); + let fill = self.fill; + self._build(sender, fill, Some(access_key)).await } async fn _build( mut self, sender: impl Into>, fill: bool, + access_key: Option<&TempoAccessKeyConfig>, ) -> Result<(N::TransactionRequest, Option)> { // prepare let sender = sender.into(); @@ -555,6 +571,16 @@ where // resolve let tx_nonce = self.resolve_nonce(sender.address(), fill).await?; self.resolve_auth(&sender, tx_nonce).await?; + if let Some(access_key) = access_key { + self.tx + .prepare_access_key_authorization( + &self.provider, + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.as_ref(), + ) + .await?; + } self.resolve_access_list().await?; // fill diff --git a/crates/cast/tests/cli/keychain.rs b/crates/cast/tests/cli/keychain.rs new file mode 100644 index 0000000000000..88e9e16983cc5 --- /dev/null +++ b/crates/cast/tests/cli/keychain.rs @@ -0,0 +1,76 @@ +//! CLI tests for `cast keychain` subcommands. + +use anvil::NodeConfig; +use foundry_test_utils::util::OutputExt; + +/// Anvil test accounts (standard mnemonic). +mod accounts { + pub const PK1: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + pub const ADDR1: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + pub const ADDR2: &str = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; + pub const TOKEN: &str = "0x20C000000000000000000000b9537d11c60E8b50"; // PathUSD +} + +// `cast keychain rl --json` must emit `{"remaining":""}`, not a bare string. +casttest!(keychain_rl_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "rl", + accounts::ADDR1, + accounts::ADDR2, + accounts::TOKEN, + "--rpc-url", + &rpc, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain rl --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + assert!( + parsed.get("remaining").is_some(), + "expected 'remaining' key in JSON output, got: {output}" + ); + // Must not be a bare string (old bug: `"0"`) + assert!(!parsed.is_string(), "JSON output must not be a bare string, got: {output}"); +}); + +// `cast keychain authorize --tempo.print-sponsor-hash --json` must emit +// `{"sponsor_hash":"0x..."}`, not a raw hex string. +casttest!(keychain_authorize_sponsor_hash_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "authorize", + accounts::ADDR2, // key to authorize + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + "--tempo.print-sponsor-hash", + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain authorize --tempo.print-sponsor-hash --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + let hash = parsed + .get("sponsor_hash") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("expected 'sponsor_hash' key in JSON output, got: {output}")); + assert!(hash.starts_with("0x"), "sponsor_hash should be 0x-prefixed, got: {hash}"); + assert_eq!(hash.len(), 66, "sponsor_hash should be 32-byte hex (66 chars), got: {hash}"); +}); diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index da33a34d849db..2f744efe4d4f0 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,6 +1,7 @@ //! Contains various tests for checking cast commands use alloy_chains::NamedChain; +use alloy_eips::Decodable2718; use alloy_hardforks::EthereumHardfork; use alloy_network::{TransactionBuilder, TransactionResponse}; use alloy_primitives::{B256, Bytes, U256, address, b256, hex}; @@ -20,11 +21,13 @@ use foundry_test_utils::{ }; use serde_json::json; use std::{fs, path::Path, str::FromStr}; +use tempo_primitives::TempoTxEnvelope; #[macro_use] extern crate foundry_test_utils; mod erc20; +mod keychain; mod selectors; casttest!(print_short_version, |_prj, cmd| { @@ -2055,6 +2058,55 @@ casttest!(mktx_ethsign, async |_prj, cmd| { ]]); }); +// tests that `cast mktx --tempo.lane ` resolves the lane against a `tempo.lanes.toml` file at +// the project root, sets the corresponding `nonce_key` on the produced Tempo AA transaction. +casttest!(mktx_tempo_lane_resolves_nonce_key, |prj, cmd| { + // Write a shared lanes file at the project root. + let lanes_path = prj.root().join("tempo.lanes.toml"); + fs::write(&lanes_path, "deploy = 1\nops = 2\npayments = 42\n").unwrap(); + + let output = cmd + .current_dir(prj.root()) + .args([ + "mktx", + "--tempo.lane", + "payments", + "--private-key", + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--chain", + "1", + "--nonce", + "0", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + ]) + .assert_success() + .get_output() + .clone(); + + // The resolved-lane breadcrumb is printed to stderr so it doesn't pollute stdout + // (which carries the raw signed transaction). + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("lane: payments (nonce_key=42, nonce=0)"), + "expected lane breadcrumb on stderr, got: {stderr}", + ); + + // Decode the produced signed Tempo AA transaction and verify it carries the + // resolved 2D nonce key. + let stdout = String::from_utf8_lossy(&output.stdout); + let raw_hex = stdout.trim().trim_start_matches("0x"); + let raw = hex::decode(raw_hex).expect("decode hex output"); + let envelope = TempoTxEnvelope::decode_2718(&mut raw.as_slice()).expect("decode tempo tx"); + assert!(envelope.is_aa(), "expected Tempo AA transaction, got: {envelope:?}"); + assert_eq!(envelope.nonce_key(), Some(U256::from(42_u64))); +}); + // tests that the raw encoded transaction is returned casttest!(tx_raw, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); @@ -4024,6 +4076,7 @@ Warning: Contract code is empty }); // +#[cfg(feature = "optimism")] casttest!(tx_raw_opstack_deposit, |_prj, cmd| { cmd.args([ "tx", @@ -5020,6 +5073,7 @@ casttest!(cast_decode_tx_network_flag_short_and_long_equivalent, |_prj, cmd| { // Test that `--network optimism` and `-n optimism` produce identical output for decode-tx. // Uses a known OP-stack deposit transaction (same tx as tx_raw_opstack_deposit test). +#[cfg(feature = "optimism")] casttest!(cast_decode_tx_network_optimism_short_and_long_equivalent, |_prj, cmd| { let tx = "0x7ef90207a0cbde10ec697aff886f95d2514bab434e455620627b9bb8ba33baaaa4d537d62794d45955f4de64f1840e5686e64278da901e263031944200000000000000000000000000000000000007872386f26fc10000872386f26fc1000083096c4980b901a4d764ad0b0001000000000000000000000000000000000000000000000000000000065132000000000000000000000000fd0bf71f60660e2f608ed56e1659c450eb1131200000000000000000000000004200000000000000000000000000000000000010000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a41635f5fd000000000000000000000000ca11bde05977b3631167028862be2a173976ca110000000000000000000000005703b26fe5a7be820db1bf34c901a79da1a46ba4000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -5076,3 +5130,68 @@ casttest!(run_evm_version_updates_gas_params, |_prj, cmd| { "expected Spurious Dragon gas (177241), got: {sd_output}" ); }); + +// Tests for `cast vaddr` JSON output +casttest!(vaddr_create_json_output, |_prj, cmd| { + // Use a pre-computed salt that satisfies the 4-byte PoW requirement for this owner. + // Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 + // Owner: 0x1234567890123456789012345678901234567890 + let out = cmd + .args([ + "--json", + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + "--count", + "2", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let v: serde_json::Value = serde_json::from_str(out.trim()).expect("valid JSON"); + assert_eq!(v["salt"], "0x0000000000000000000000000000000000000000000000003ee0a78d00000000"); + assert_eq!( + v["registration_hash"], + "0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396" + ); + assert_eq!(v["master_id"], "0x2f51c0c4"); + let addrs = v["virtual_addresses"].as_array().expect("array"); + assert_eq!(addrs.len(), 2); + assert_eq!(addrs[0]["tag"], "0x000000000000"); + assert_eq!( + addrs[0]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000000" + ); + assert_eq!(addrs[1]["tag"], "0x000000000001"); + assert_eq!( + addrs[1]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000001" + ); +}); + +casttest!(vaddr_create_plain_output, |_prj, cmd| { + cmd.args([ + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + ]) + .assert_success() + .stdout_eq(str![[r#" +Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 +Registration hash: 0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396 +Master ID: 0x2f51c0c4 + +Virtual addresses: + tag=0x000000000000 [..] + +"#]]); +}); diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 659fec7f1a333..0eab12331be04 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -68,3 +68,13 @@ tracing.workspace = true walkdir.workspace = true proptest.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", + "forge-script-sequence/optimism", +] diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 94974301df8ac..01de77b9c95fd 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -5447,7 +5447,7 @@ { "func": { "id": "expectEmit_0", - "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).", + "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external;", "visibility": "external", "mutability": "", @@ -5487,7 +5487,7 @@ { "func": { "id": "expectEmit_2", - "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.", + "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit() external;", "visibility": "external", "mutability": "", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 7c2e0741704b3..12cfd19017770 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1082,6 +1082,9 @@ interface Vm { /// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.). /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data (as specified by the booleans). + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; @@ -1093,6 +1096,9 @@ interface Vm { /// Prepare an expected log with all topic and data checks enabled. /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data. + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit() external; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index b22f76714dd0f..27545c1b6cd33 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -856,42 +856,6 @@ impl Cheatcodes { } } - // Handle mocked calls - if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { - let ctx = MockCallDataContext { - calldata: call.input.bytes(ecx), - value: call.transfer_value(), - }; - - if let Some(return_data_queue) = match mocks.get_mut(&ctx) { - Some(queue) => Some(queue), - None => mocks - .iter_mut() - .find(|(mock, _)| { - call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) - && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) - }) - .map(|(_, v)| v), - } && let Some(return_data) = if return_data_queue.len() == 1 { - // If the mocked calls stack has a single element in it, don't empty it - return_data_queue.front().map(|x| x.to_owned()) - } else { - // Else, we pop the front element - return_data_queue.pop_front() - } { - return Some(CallOutcome { - result: InterpreterResult { - result: return_data.ret_type, - output: return_data.data, - gas, - }, - memory_offset: call.return_memory_offset.clone(), - was_precompile_called: true, - precompile_call_logs: vec![], - }); - } - } - // Apply our prank if let Some(prank) = &self.get_prank(curr_depth) { // Apply delegate call, `call.caller`` will not equal `prank.prank_caller` @@ -932,6 +896,72 @@ impl Cheatcodes { } } + // Handle mocked calls + if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { + let ctx = MockCallDataContext { + calldata: call.input.bytes(ecx), + value: call.transfer_value(), + }; + + if let Some(return_data_queue) = match mocks.get_mut(&ctx) { + Some(queue) => Some(queue), + None => mocks + .iter_mut() + .find(|(mock, _)| { + call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) + && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) + }) + .map(|(_, v)| v), + } && let Some(return_data) = return_data_queue.front().map(|x| x.to_owned()) + { + if let Some(value) = call.transfer_value() { + let checkpoint = ecx.journal_mut().checkpoint(); + match ecx.journal_mut().transfer_loaded( + call.transfer_from(), + call.transfer_to(), + value, + ) { + None => { + if return_data.ret_type.is_ok() { + ecx.journal_mut().checkpoint_commit(); + } else { + ecx.journal_mut().checkpoint_revert(checkpoint); + } + } + Some(err) => { + ecx.journal_mut().checkpoint_revert(checkpoint); + return Some(CallOutcome { + result: InterpreterResult { + result: err.into(), + output: Bytes::new(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: false, + precompile_call_logs: vec![], + }); + } + } + } + + // If the mocked calls stack has a single element in it, don't empty it + if return_data_queue.len() > 1 { + return_data_queue.pop_front(); + } + + return Some(CallOutcome { + result: InterpreterResult { + result: return_data.ret_type, + output: return_data.data, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: true, + precompile_call_logs: vec![], + }); + } + } + // Apply EIP-2930 access list self.apply_accesslist(ecx); @@ -1497,6 +1527,21 @@ impl Inspector> for Cheatcode } } + // this will ensure we don't have false positives when trying to diagnose reverts in fork + // mode + let diag = self.fork_revert_diagnostic.take(); + + // If the call already reverted, preserve that primary failure and skip post-call + // expect* validation so it cannot overwrite the original revert. + if outcome.result.is_revert() { + // if there's a revert and a previous call was diagnosed as fork related revert then we + // can return a better error here + if let Some(err) = diag { + outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); + } + return; + } + // At the end of the call, // we need to check if we've found all the emits. // We know we've found all the expected emits in the right order @@ -1574,19 +1619,6 @@ impl Inspector> for Cheatcode self.expected_emits.clear() } - // this will ensure we don't have false positives when trying to diagnose reverts in fork - // mode - let diag = self.fork_revert_diagnostic.take(); - - // if there's a revert and a previous call was diagnosed as fork related revert then we can - // return a better error here - if outcome.result.is_revert() - && let Some(err) = diag - { - outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); - return; - } - // try to diagnose reverts in multi-fork mode where a call is made to an address that does // not exist if let TxKind::Call(test_contract) = ecx.tx().kind() { @@ -1867,10 +1899,23 @@ impl Inspector> for Cheatcode } // Handle expected reverts - if let Some(expected_revert) = &self.expected_revert + if let Some(expected_revert) = &mut self.expected_revert && curr_depth <= expected_revert.depth && matches!(expected_revert.kind, ExpectedRevertKind::Default) { + // Mirror the logic in `call_end`: when an expected reverter address is set + // and we don't yet have one (or we're matching multiple reverts), record the + // would-be deployed address as the reverter. revm guarantees `outcome.address` + // is `Some(_)` whenever the constructor actually ran (including the revert + // case); it is only `None` for pre-frame rejection (depth/balance/nonce), + // for which a reverter address is meaningless. + if outcome.result.is_revert() + && expected_revert.reverter.is_some() + && (expected_revert.reverted_by.is_none() || expected_revert.count > 1) + && let Some(addr) = outcome.address + { + expected_revert.reverted_by = Some(addr); + } let mut expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); return match revert_handlers::handle_expect_revert( false, diff --git a/crates/cheatcodes/src/test/assert.rs b/crates/cheatcodes/src/test/assert.rs index 608f20f0c4e32..12d625768a0c9 100644 --- a/crates/cheatcodes/src/test/assert.rs +++ b/crates/cheatcodes/src/test/assert.rs @@ -164,7 +164,7 @@ impl EqRelAssertionError { format_units_uint(&f.left, decimals), format_units_uint(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } @@ -179,7 +179,7 @@ impl EqRelAssertionError { format_units_int(&f.left, decimals), format_units_int(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } diff --git a/crates/cheatcodes/src/version.rs b/crates/cheatcodes/src/version.rs index fb722c2814baa..2b8f81518a621 100644 --- a/crates/cheatcodes/src/version.rs +++ b/crates/cheatcodes/src/version.rs @@ -20,7 +20,14 @@ impl Cheatcode for foundryVersionAtLeastCall { } fn foundry_version_cmp(version: &str) -> Result { - version_cmp(SEMVER_VERSION.split('-').next().unwrap(), version) + version_cmp(strip_semver_metadata(SEMVER_VERSION), version) +} + +/// Strips pre-release (e.g. `-nightly`, `-dev`) and build metadata +/// (e.g. `+..`) from a version string +/// so we compare on `MAJOR.MINOR.PATCH` only. +fn strip_semver_metadata(version: &str) -> &str { + version.split(['-', '+']).next().unwrap() } fn version_cmp(version_a: &str, version_b: &str) -> Result { @@ -42,3 +49,61 @@ fn parse_version(version: &str) -> Result { } Ok(version) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_build_metadata_only() { + // Tagged release: `1.7.1+..` + assert_eq!(strip_semver_metadata("1.7.1+abc1234567.1737036656.release"), "1.7.1"); + } + + #[test] + fn strips_pre_release_and_build_metadata() { + // Nightly: `1.7.1-nightly+..` + assert_eq!(strip_semver_metadata("1.7.1-nightly+abc1234567.1737036656.release"), "1.7.1"); + // Dev: `1.7.1-dev+..` + assert_eq!(strip_semver_metadata("1.7.1-dev+abc1234567.1737036656.debug"), "1.7.1"); + } + + #[test] + fn strips_plain_version() { + assert_eq!(strip_semver_metadata("1.7.1"), "1.7.1"); + } + + #[test] + fn version_cmp_orders_correctly() { + assert_eq!(version_cmp("1.7.1", "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp("1.7.1", "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "1.7.2").unwrap(), Ordering::Less); + assert_eq!(version_cmp("1.7.1", "0.0.1").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "99.0.0").unwrap(), Ordering::Less); + } + + #[test] + fn parse_version_rejects_pre_release_and_build_metadata() { + // User-supplied versions must be plain `MAJOR.MINOR.PATCH`. + assert!(parse_version("1.7.1-nightly").is_err()); + assert!(parse_version("1.7.1+abc").is_err()); + assert!(parse_version("not-a-version").is_err()); + assert!(parse_version("1.7.1").is_ok()); + } + + #[test] + fn cmp_works_against_full_semver_version_strings() { + // Simulate comparing each shape of `SEMVER_VERSION` against a user-supplied version. + for current in [ + "1.7.1+abc1234567.1737036656.release", + "1.7.1-nightly+abc1234567.1737036656.release", + "1.7.1-dev+abc1234567.1737036656.debug", + "1.7.1", + ] { + let stripped = strip_semver_metadata(current); + assert_eq!(version_cmp(stripped, "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp(stripped, "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp(stripped, "1.7.2").unwrap(), Ordering::Less); + } + } +} diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index a9396b4208886..bb673c9219e10 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -49,7 +49,6 @@ itertools.workspace = true semver.workspace = true serde_json.workspace = true serde.workspace = true -solang-parser.workspace = true time = { version = "0.3", features = ["formatting"] } yansi.workspace = true tracing.workspace = true @@ -64,8 +63,13 @@ foundry-test-utils.workspace = true rexpect = "0.6" [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] tracy-allocator = ["foundry-cli/tracy-allocator"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index da2c7f4caff02..2ec057b8167c0 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -2,10 +2,7 @@ //! //! This module contains the execution logic for the [SessionSource]. -use crate::{ - prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}, - source::IntermediateOutput, -}; +use crate::prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_json_abi::EventParam; use alloy_primitives::{Address, B256, U256, hex}; @@ -15,7 +12,17 @@ use foundry_evm::{ backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder, inspectors::CheatsConfig, traces::TraceMode, }; -use solang_parser::pt; +use solar::{ + ast::{BinOpKind, ElementaryType, FunctionKind, LitKind, StateMutability, StrKind, UnOpKind}, + interface::Symbol, + sema::{ + hir::{ + ContractId, Event, Expr, ExprKind, Function, ItemId, Res, StmtKind, Type as HirType, + TypeKind, Visibility, + }, + ty::{Gcx, Ty, TyKind}, + }, +}; use std::ops::ControlFlow; use yansi::Paint; @@ -86,8 +93,10 @@ impl SessionSource { if let Some(err) = err { let output = source_without_inspector.build()?; - let formatted_event = - output.enter(|output| output.get_event(input).map(format_event_definition)); + let formatted_event = output.enter(|output| { + let gcx = output.gcx(); + output.get_event(input).map(|eid| format_event_definition(gcx, gcx.hir.event(eid))) + }); if let Some(formatted_event) = formatted_event { return Ok((ControlFlow::Break(()), Some(formatted_event?))); } @@ -122,30 +131,37 @@ impl SessionSource { // which was wrapped in `abi.encode`. let generated_output = source.build()?; - // If the expression is a variable declaration within the REPL contract, use its type; - // otherwise, attempt to infer the type. - let contract_expr = generated_output - .intermediate - .repl_contract_expressions - .get(input) - .or_else(|| source.infer_inner_expr_type()); + // Inside the compiler closure, infer the DynSolType of the inspected expression and + // determine whether the REPL should continue. + let res_ty = generated_output.enter(|out| -> Option<(bool, DynSolType)> { + let gcx = out.gcx(); - // If the current action is a function call, we get its return type - // otherwise it returns None - let function_call_return_type = - Type::get_function_return_type(contract_expr, &generated_output.intermediate); + // Try direct lookup of `input` as a named variable in the REPL contract. + if let Some(direct_ty) = lookup_named_variable_type(gcx, input) { + return Some((false, direct_ty)); + } - let (contract_expr, ty) = if let Some(function_call_return_type) = function_call_return_type - { - (function_call_return_type.0, function_call_return_type.1) - } else { - match contract_expr.and_then(|e| { - Type::ethabi(e, Some(&generated_output.intermediate)).map(|ty| (e, ty)) - }) { - Some(res) => res, - // this type was denied for inspection, continue - None => return Ok((ControlFlow::Continue(()), None)), + // Otherwise, find the appended `bytes memory inspectoor = abi.encode();` + // and pull out the first call argument. + let block = out.run_func_body(); + let last = block.last()?; + let StmtKind::DeclSingle(vid) = last.kind else { return None }; + let var = gcx.hir.variable(vid); + let init = var.initializer?; + let ExprKind::Call(_callee, args, _) = &init.kind else { return None }; + let inner_expr = args.exprs().next()?; + + // If the call is `func()` returning a single value, prefer the function return type. + if let Some(ty) = get_function_return_type(gcx, inner_expr) { + return Some((should_continue(inner_expr), ty)); } + + let ty = expr_to_dyn(gcx, inner_expr, true)?; + Some((should_continue(inner_expr), ty)) + }); + + let Some((cont, ty)) = res_ty else { + return Ok((ControlFlow::Continue(()), None)); }; // the file compiled correctly, thus the last stack item must be the memory offset of @@ -162,42 +178,10 @@ impl SessionSource { eyre::bail!("Failed to inspect last expression: could not retrieve data from memory") }; let token = ty.abi_decode(data).wrap_err("Could not decode inspected values")?; - let c = if should_continue(contract_expr) { - ControlFlow::Continue(()) - } else { - ControlFlow::Break(()) - }; + let c = if cont { ControlFlow::Continue(()) } else { ControlFlow::Break(()) }; Ok((c, Some(format_token(token)))) } - /// Gracefully attempts to extract the type of the expression within the `abi.encode(...)` - /// call inserted by the inspect function. - /// - /// ### Takes - /// - /// A reference to a [SessionSource] - /// - /// ### Returns - /// - /// Optionally, a [Type] - fn infer_inner_expr_type(&self) -> Option<&pt::Expression> { - let out = self.build().ok()?; - let run = out.run_func_body().ok()?.last(); - match run { - Some(pt::Statement::VariableDefinition( - _, - _, - Some(pt::Expression::FunctionCall(_, _, args)), - )) => { - // We can safely unwrap the first expression because this function - // will only be called on a session source that has just had an - // `inspectoor` variable appended to it. - Some(args.first().unwrap()) - } - _ => None, - } - } - async fn build_runner(&mut self, final_pc: usize) -> Result { let (evm_env, tx_env, fork_block) = self.config.evm_opts.env().await?; @@ -241,6 +225,51 @@ impl SessionSource { } } +/// Looks up `name` as a named variable in the REPL contract (state variables or run() locals) +/// and returns its type as a [`DynSolType`]. +/// +/// Only top-level statements of `run()` are scanned. Variables declared inside nested blocks +/// (`if`, `for`, `while`, `unchecked`, etc.) are not visible here; the caller falls back to +/// the `inspectoor`-based path for those cases. +fn lookup_named_variable_type(gcx: Gcx<'_>, name: &str) -> Option { + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + + // State variables. + for vid in repl.variables() { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + + // Locals declared in run(). + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + for stmt in body.stmts { + match stmt.kind { + StmtKind::DeclSingle(vid) => { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + StmtKind::DeclMulti(vids, _) => { + for vid in vids.iter().flatten() { + let var = hir.variable(*vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item((*vid).into())); + } + } + } + _ => {} + } + } + None +} + /// Formats a value into an inspection message // TODO: Verbosity option fn format_token(token: DynSolValue) -> String { @@ -343,49 +372,37 @@ fn format_token(token: DynSolValue) -> String { } } -/// Formats a [pt::EventDefinition] into an inspection message -/// -/// ### Takes -/// -/// An borrowed [pt::EventDefinition] -/// -/// ### Returns -/// -/// A formatted [pt::EventDefinition] for use in inspection output. +/// Formats an [`Event`] into an inspection message. // TODO: Verbosity option -fn format_event_definition(event_definition: &pt::EventDefinition) -> Result { - let event_name = event_definition.name.as_ref().expect("Event has a name").to_string(); - let inputs = event_definition - .fields +fn format_event_definition(gcx: Gcx<'_>, event: &Event<'_>) -> Result { + let event_name = event.name.as_str().to_string(); + let inputs = event + .parameters .iter() - .map(|param| { - let name = param - .name - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "".to_string()); - let kind = Type::from_expression(¶m.ty) - .and_then(Type::into_builtin) + .map(|&pid| { + let var = gcx.hir.variable(pid); + let name = + var.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "".into()); + let kind = solar_ty_to_dyn(gcx, gcx.type_of_item(pid.into())) .ok_or_else(|| eyre::eyre!("Invalid type in event {event_name}"))?; Ok(EventParam { name, ty: kind.to_string(), components: vec![], - indexed: param.indexed, + indexed: var.indexed, internal_type: None, }) }) .collect::>>()?; - let event = - alloy_json_abi::Event { name: event_name, inputs, anonymous: event_definition.anonymous }; + let event = alloy_json_abi::Event { name: event_name, inputs, anonymous: event.anonymous }; Ok(format!( "Type: {}\n├ Name: {}\n├ Signature: {:?}\n└ Selector: {:?}", "event".red(), SolidityHelper::new().highlight(&format!( "{}({})", - &event.name, - &event + event.name, + event .inputs .iter() .map(|param| format!( @@ -395,7 +412,7 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result>() @@ -411,844 +428,724 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result), - - /// (type, length) - FixedArray(Box, usize), +/// Converts an [`Expr`] directly to a [`DynSolType`] for ABI inspection. +/// +/// `lookup` controls whether user-defined type names are resolved via the HIR. +fn expr_to_dyn(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + match &expr.kind { + // Elementary type expression: `uint256`, `address`, etc. + ExprKind::Type(ty) => hir_ty_to_dyn(gcx, ty), + + // `type(T)`: only meaningful as the lhs of a member access. + ExprKind::TypeCall(_) => None, + + // Literals. + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Address(_) => Some(DynSolType::Address), + LitKind::Bool(_) => Some(DynSolType::Bool), + LitKind::Str(kind, _, _) => match kind { + StrKind::Hex => Some(DynSolType::Bytes), + StrKind::Str | StrKind::Unicode => Some(DynSolType::String), + }, + LitKind::Number(_) | LitKind::Rational(_) => Some(DynSolType::Uint(256)), + LitKind::Err(_) => None, + }, + + // Resolved identifier: `foo`. + ExprKind::Ident(reses) => { + let res = reses.first()?; + match *res { + Res::Item(ItemId::Variable(vid)) => { + solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) + } + Res::Item(ItemId::Struct(sid)) => { + // Struct reference used as a constructor produces a tuple of field types. + Some(DynSolType::Tuple( + gcx.struct_field_types(sid) + .iter() + .filter_map(|&t| solar_ty_to_dyn(gcx, t)) + .collect(), + )) + } + // Other items and builtins: handled by enclosing Call/Member expressions. + _ => None, + } + } - /// (type, index) - ArrayIndex(Box, Option), + // Index/access: `arr[i]`, `MyType[]`, `MyType[N]`. + ExprKind::Index(base, idx) => { + let base_ty = expr_to_dyn(gcx, base, lookup)?; + let num = + idx.and_then(|e| parse_number_literal(e)).and_then(|n| usize::try_from(n).ok()); + match &base.kind { + // Type-level indexing builds an array type expression. + ExprKind::Type(_) | ExprKind::TypeCall(_) => { + if let Some(n) = num { + Some(DynSolType::FixedArray(Box::new(base_ty), n)) + } else { + Some(DynSolType::Array(Box::new(base_ty))) + } + } + // Runtime indexing returns the element type. + _ => match base_ty { + DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => Some(*inner), + DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_) => { + Some(DynSolType::FixedBytes(1)) + } + other => Some(other), + }, + } + } - /// (types) - Tuple(Vec>), + // Slice: same type as the base. + ExprKind::Slice(base, _, _) => expr_to_dyn(gcx, base, lookup), - /// (name, params, returns) - Function(Box, Vec>, Vec>), + // Array literal `[a, b, c]`. + ExprKind::Array(values) => values + .first() + .and_then(|e| expr_to_dyn(gcx, e, lookup)) + .map(|ty| DynSolType::FixedArray(Box::new(ty), values.len())), - /// (lhs, rhs) - Access(Box, String), + // Tuple expression `(a, b, c)`. + ExprKind::Tuple(items) => Some(DynSolType::Tuple( + items.iter().filter_map(|opt| opt.and_then(|e| expr_to_dyn(gcx, e, lookup))).collect(), + )), - /// (types) - Custom(Vec), -} + // Member access `lhs.member`. + ExprKind::Member(_, _) => resolve_member(gcx, expr, lookup), -impl Type { - /// Convert a [pt::Expression] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Expression] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_expression(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::Type(_, ty) => Self::from_type(ty), - - pt::Expression::Variable(ident) => Some(Self::Custom(vec![ident.name.clone()])), - - // array - pt::Expression::ArraySubscript(_, expr, num) => { - // if num is Some then this is either an index operation (arr[]) - // or a FixedArray statement (new uint256[]) - Self::from_expression(expr).and_then(|ty| { - let boxed = Box::new(ty); - let num = num.as_deref().and_then(parse_number_literal).and_then(|n| { - usize::try_from(n).ok() - }); - match expr.as_ref() { - // statement - pt::Expression::Type(_, _) => { - if let Some(num) = num { - Some(Self::FixedArray(boxed, num)) - } else { - Some(Self::Array(boxed)) - } - } - // index - pt::Expression::Variable(_) => { - Some(Self::ArrayIndex(boxed, num)) - } - _ => None - } - }) - } - pt::Expression::ArrayLiteral(_, values) => { - values.first().and_then(Self::from_expression).map(|ty| { - Self::FixedArray(Box::new(ty), values.len()) - }) - } + // Function/constructor call. + ExprKind::Call(_, _, _) => resolve_call(gcx, expr, lookup), - // tuple - pt::Expression::List(_, params) => Some(Self::Tuple(map_parameters(params))), + // `new T`: produces a value of type T. + ExprKind::New(ty) => hir_ty_to_dyn(gcx, ty), - // . - pt::Expression::MemberAccess(_, lhs, rhs) => { - Self::from_expression(lhs).map(|lhs| { - Self::Access(Box::new(lhs), rhs.name.clone()) - }) - } + // `payable(addr)`. + ExprKind::Payable(_) => Some(DynSolType::Address), - // - pt::Expression::Parenthesis(_, inner) | // () - pt::Expression::New(_, inner) | // new - pt::Expression::UnaryPlus(_, inner) | // + - // ops - pt::Expression::BitwiseNot(_, inner) | // ~ - pt::Expression::ArraySlice(_, inner, _, _) | // [*start*:*end*] - // assign ops - pt::Expression::PreDecrement(_, inner) | // -- - pt::Expression::PostDecrement(_, inner) | // -- - pt::Expression::PreIncrement(_, inner) | // ++ - pt::Expression::PostIncrement(_, inner) | // ++ - pt::Expression::Assign(_, inner, _) | // = ... - pt::Expression::AssignAdd(_, inner, _) | // += ... - pt::Expression::AssignSubtract(_, inner, _) | // -= ... - pt::Expression::AssignMultiply(_, inner, _) | // *= ... - pt::Expression::AssignDivide(_, inner, _) | // /= ... - pt::Expression::AssignModulo(_, inner, _) | // %= ... - pt::Expression::AssignAnd(_, inner, _) | // &= ... - pt::Expression::AssignOr(_, inner, _) | // |= ... - pt::Expression::AssignXor(_, inner, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, inner, _) | // <<= ... - pt::Expression::AssignShiftRight(_, inner, _) // >>= ... - => Self::from_expression(inner), - - // *condition* ? : - pt::Expression::ConditionalOperator(_, _, if_true, if_false) => { - Self::from_expression(if_true).or_else(|| Self::from_expression(if_false)) - } + // Ternary: prefer truthy branch's type, fall back to else branch. + ExprKind::Ternary(_, t, e) => { + expr_to_dyn(gcx, t, lookup).or_else(|| expr_to_dyn(gcx, e, lookup)) + } - // address - pt::Expression::AddressLiteral(_, _) => Some(Self::Builtin(DynSolType::Address)), - pt::Expression::HexNumberLiteral(_, s, _) => { - match s.parse::
() { - Ok(addr) if *s == addr.to_checksum(None) => { - Some(Self::Builtin(DynSolType::Address)) + // Delete has no return type. + ExprKind::Delete(_) => None, + + // Unary operations. + ExprKind::Unary(op, inner) => match op.kind { + UnOpKind::Neg => expr_to_dyn(gcx, inner, lookup).map(|ty| match ty { + DynSolType::Uint(n) => DynSolType::Int(n), + DynSolType::Int(n) => DynSolType::Uint(n), + x => x, + }), + UnOpKind::Not => Some(DynSolType::Bool), + UnOpKind::BitNot + | UnOpKind::PreInc + | UnOpKind::PreDec + | UnOpKind::PostInc + | UnOpKind::PostDec => expr_to_dyn(gcx, inner, lookup), + }, + + // Binary operations. + ExprKind::Binary(lhs, op, rhs) => match op.kind { + BinOpKind::Lt + | BinOpKind::Le + | BinOpKind::Gt + | BinOpKind::Ge + | BinOpKind::Eq + | BinOpKind::Ne + | BinOpKind::And + | BinOpKind::Or => Some(DynSolType::Bool), + BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul | BinOpKind::Div => { + match (expr_to_dyn(gcx, lhs, false), expr_to_dyn(gcx, rhs, false)) { + (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) + | (Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { + Some(DynSolType::Int(256)) } - _ => Some(Self::Builtin(DynSolType::Uint(256))), + _ => Some(DynSolType::Uint(256)), } } + BinOpKind::Rem + | BinOpKind::Pow + | BinOpKind::BitAnd + | BinOpKind::BitOr + | BinOpKind::BitXor + | BinOpKind::Shl + | BinOpKind::Shr + | BinOpKind::Sar => Some(DynSolType::Uint(256)), + }, + + // Assignments: type of the lhs. + ExprKind::Assign(lhs, _, _) => expr_to_dyn(gcx, lhs, lookup), + + ExprKind::Err(_) => None, + } +} - // uint and int - // invert - pt::Expression::Negate(_, inner) => Self::from_expression(inner).map(Self::invert_int), - - // int if either operand is int - // TODO: will need an update for Solidity v0.8.18 user defined operators: - // https://github.com/ethereum/solidity/issues/13718#issuecomment-1341058649 - pt::Expression::Add(_, lhs, rhs) | - pt::Expression::Subtract(_, lhs, rhs) | - pt::Expression::Multiply(_, lhs, rhs) | - pt::Expression::Divide(_, lhs, rhs) => { - match (Self::ethabi(lhs, None), Self::ethabi(rhs, None)) { - (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) | -(Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { - Some(Self::Builtin(DynSolType::Int(256))) - } - _ => { - Some(Self::Builtin(DynSolType::Uint(256))) - } +/// Converts a [`HirType`] to a [`DynSolType`]. +fn hir_ty_to_dyn(gcx: Gcx<'_>, ty: &HirType<'_>) -> Option { + match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + TypeKind::Array(arr) => { + let elem = hir_ty_to_dyn(gcx, &arr.element)?; + if let Some(size) = arr.size { + let n = parse_number_literal(size).and_then(|n| usize::try_from(n).ok()); + if let Some(n) = n { + Some(DynSolType::FixedArray(Box::new(elem), n)) + } else { + Some(DynSolType::Array(Box::new(elem))) } + } else { + Some(DynSolType::Array(Box::new(elem))) } - - // always assume uint - pt::Expression::Modulo(_, _, _) | - pt::Expression::Power(_, _, _) | - pt::Expression::BitwiseOr(_, _, _) | - pt::Expression::BitwiseAnd(_, _, _) | - pt::Expression::BitwiseXor(_, _, _) | - pt::Expression::ShiftRight(_, _, _) | - pt::Expression::ShiftLeft(_, _, _) | - pt::Expression::NumberLiteral(_, _, _, _) => Some(Self::Builtin(DynSolType::Uint(256))), - - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(_, _, _, _, _) => { - Some(Self::Builtin(DynSolType::Uint(256))) + } + TypeKind::Function(f) => match f.returns.len() { + 0 => None, + 1 => { + let var = gcx.hir.variable(f.returns[0]); + hir_ty_to_dyn(gcx, &var.ty) } + _ => Some(DynSolType::Tuple( + f.returns + .iter() + .filter_map(|&pid| hir_ty_to_dyn(gcx, &gcx.hir.variable(pid).ty)) + .collect(), + )), + }, + TypeKind::Mapping(m) => hir_ty_to_dyn(gcx, &m.value), + TypeKind::Custom(item) => solar_ty_to_dyn(gcx, gcx.type_of_item(*item)), + TypeKind::Err(_) => None, + } +} - // bool - pt::Expression::BoolLiteral(_, _) | - pt::Expression::And(_, _, _) | - pt::Expression::Or(_, _, _) | - pt::Expression::Equal(_, _, _) | - pt::Expression::NotEqual(_, _, _) | - pt::Expression::Less(_, _, _) | - pt::Expression::LessEqual(_, _, _) | - pt::Expression::More(_, _, _) | - pt::Expression::MoreEqual(_, _, _) | - pt::Expression::Not(_, _) => Some(Self::Builtin(DynSolType::Bool)), - - // string - pt::Expression::StringLiteral(_) => Some(Self::Builtin(DynSolType::String)), - - // bytes - pt::Expression::HexLiteral(_) => Some(Self::Builtin(DynSolType::Bytes)), - - // function - pt::Expression::FunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(Self::from_expression).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } - pt::Expression::NamedFunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(|arg| Self::from_expression(&arg.expr)).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } +/// Resolves a member-access expression (`lhs.member`) to its [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Member`. +fn resolve_member(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Member(lhs, ident) = &expr.kind else { return None }; + let member = ident.name; + + // `type(T).member` — type introspection. + if let ExprKind::TypeCall(ty) = &lhs.kind { + return match member.as_str() { + "name" => Some(DynSolType::String), + "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), + "interfaceId" => Some(DynSolType::FixedBytes(4)), + // Only valid for integer types; custom types (enums) fall back to Uint(256). + "min" | "max" => match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + _ => Some(DynSolType::Uint(256)), + }, + _ => None, + }; + } - // explicitly None - pt::Expression::Delete(_, _) | pt::Expression::FunctionCallBlock(_, _, _) => None, - } + // Built-in namespace identifier: `block.timestamp`, `msg.sender`, `abi.encode`, etc. + if let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + && let Some(ty) = builtin_member(b.name().as_str(), member.as_str()) + { + return Some(ty); } - /// Convert a [pt::Type] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Type] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_type(ty: &pt::Type) -> Option { - let ty = match ty { - pt::Type::Address | pt::Type::AddressPayable | pt::Type::Payable => { - Self::Builtin(DynSolType::Address) - } - pt::Type::Bool => Self::Builtin(DynSolType::Bool), - pt::Type::String => Self::Builtin(DynSolType::String), - pt::Type::Int(size) => Self::Builtin(DynSolType::Int(*size as usize)), - pt::Type::Uint(size) => Self::Builtin(DynSolType::Uint(*size as usize)), - pt::Type::Bytes(size) => Self::Builtin(DynSolType::FixedBytes(*size as usize)), - pt::Type::DynamicBytes => Self::Builtin(DynSolType::Bytes), - pt::Type::Mapping { value, .. } => Self::from_expression(value)?, - pt::Type::Function { params, returns, .. } => { - let params = map_parameters(params); - let returns = returns - .as_ref() - .map(|(returns, _)| map_parameters(returns)) - .unwrap_or_default(); - Self::Function( - Box::new(Self::Custom(vec!["__fn_type__".to_string()])), - params, - returns, - ) - } - // TODO: Rational numbers - pt::Type::Rational => return None, + // Elementary type used as a namespace: `address.balance`, `bytes.concat`, etc. + if let ExprKind::Type(ty) = &lhs.kind + && let TypeKind::Elementary(et) = &ty.kind + { + return match et { + ElementaryType::Address(_) => match member.as_str() { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + ElementaryType::Bytes => match member.as_str() { + "concat" => Some(DynSolType::Bytes), + _ => None, + }, + ElementaryType::String => match member.as_str() { + "concat" => Some(DynSolType::String), + _ => None, + }, + _ => None, }; - Some(ty) } - /// Handle special expressions like [global variables](https://docs.soliditylang.org/en/latest/cheatsheet.html#global-variables) - /// - /// See: - fn map_special(self) -> Self { - if !matches!(self, Self::Function(_, _, _) | Self::Access(_, _) | Self::Custom(_)) { - return self; - } + // Members on a resolved DynSolType (`.length`, `.pop`, `.selector`, `.address`). + if let Some(lhs_ty) = expr_to_dyn(gcx, lhs, lookup) + && let Some(ty) = dyn_member(&lhs_ty, member.as_str()) + { + return Some(ty); + } - let mut types = Vec::with_capacity(5); - let mut args = None; - self.recurse(&mut types, &mut args); + // HIR lookup for user-defined type members. + if lookup && let Some(mut chain) = expr_name_chain(gcx, lhs) { + chain.insert(0, member); + return infer_custom_type(gcx, &mut chain, None).ok().flatten(); + } - let len = types.len(); - if len == 0 { - return self; - } + None +} + +/// Returns the type of `builtin_ns.member` for built-in global namespaces. +fn builtin_member(builtin: &str, member: &str) -> Option { + match builtin { + "block" => match member { + "coinbase" => Some(DynSolType::Address), + "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" | "chainid" + | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), + _ => None, + }, + "msg" => match member { + "sender" => Some(DynSolType::Address), + "gas" | "value" => Some(DynSolType::Uint(256)), + "data" => Some(DynSolType::Bytes), + "sig" => Some(DynSolType::FixedBytes(4)), + _ => None, + }, + "tx" => match member { + "origin" => Some(DynSolType::Address), + "gasprice" => Some(DynSolType::Uint(256)), + _ => None, + }, + "address" => match member { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + _ => None, + } +} + +/// Returns the type of `ty.member` for a known [`DynSolType`]. +fn dyn_member(ty: &DynSolType, member: &str) -> Option { + match member { + "length" => match ty { + DynSolType::Array(_) + | DynSolType::FixedArray(_, _) + | DynSolType::Bytes + | DynSolType::String + | DynSolType::FixedBytes(_) => Some(DynSolType::Uint(256)), + _ => None, + }, + "pop" => match ty { + DynSolType::Array(inner) => Some(*inner.clone()), + _ => None, + }, + // Address members. + "balance" => match ty { + DynSolType::Address => Some(DynSolType::Uint(256)), + _ => None, + }, + "code" => match ty { + DynSolType::Address => Some(DynSolType::Bytes), + _ => None, + }, + "codehash" => match ty { + DynSolType::Address => Some(DynSolType::FixedBytes(32)), + _ => None, + }, + "send" => match ty { + DynSolType::Address => Some(DynSolType::Bool), + _ => None, + }, + // External function members. + "selector" => Some(DynSolType::FixedBytes(4)), + "address" => Some(DynSolType::Address), + _ => None, + } +} - // Type members, like array, bytes etc - #[expect(clippy::single_match)] - #[allow(clippy::collapsible_match)] - match &self { - Self::Access(inner, access) => { - if let Some(ty) = inner.as_ref().clone().try_as_ethabi(None) { - // Array / bytes members - let ty = Self::Builtin(ty); - match access.as_str() { - "length" if ty.is_dynamic() || ty.is_array() || ty.is_fixed_bytes() => { - return Self::Builtin(DynSolType::Uint(256)); +/// Resolves a call expression to its return [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Call`. +fn resolve_call(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Call(callee, args, _named) = &expr.kind else { return None }; + + // Type cast: `uint256(x)`, `address(y)`, etc. + if let ExprKind::Type(ty) = &callee.kind { + return hir_ty_to_dyn(gcx, ty); + } + + // Member call: `ns.method(...)`. + if let ExprKind::Member(lhs, method) = &callee.kind + && let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + { + match b.name().as_str() { + "abi" => { + return match method.as_str() { + "decode" => { + let last = args.exprs().last()?; + match expr_to_dyn(gcx, last, false)? { + DynSolType::Tuple(tys) => Some(DynSolType::Tuple(tys)), + ty => Some(DynSolType::Tuple(vec![ty])), } - "pop" if ty.is_dynamic_array() => return ty, - _ => {} } - } + s if s.starts_with("encode") => Some(DynSolType::Bytes), + _ => None, + }; } + "string" if method.as_str() == "concat" => return Some(DynSolType::String), + "bytes" if method.as_str() == "concat" => return Some(DynSolType::Bytes), _ => {} } + } - let this = { - let name = types.last().unwrap().as_str(); - match len { - 0 => unreachable!(), - 1 => match name { + // Simple identifier call: built-in global functions and HIR function calls. + if let ExprKind::Ident(reses) = &callee.kind { + match reses.first() { + Some(Res::Builtin(b)) => { + return match b.name().as_str() { "gasleft" | "addmod" | "mulmod" => Some(DynSolType::Uint(256)), "keccak256" | "sha256" | "blockhash" => Some(DynSolType::FixedBytes(32)), "ripemd160" => Some(DynSolType::FixedBytes(20)), "ecrecover" => Some(DynSolType::Address), _ => None, - }, - 2 => { - let access = types.first().unwrap().as_str(); - match name { - "block" => match access { - "coinbase" => Some(DynSolType::Address), - "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" - | "chainid" | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), - _ => None, - }, - "msg" => match access { - "sender" => Some(DynSolType::Address), - "gas" => Some(DynSolType::Uint(256)), - "value" => Some(DynSolType::Uint(256)), - "data" => Some(DynSolType::Bytes), - "sig" => Some(DynSolType::FixedBytes(4)), - _ => None, - }, - "tx" => match access { - "origin" => Some(DynSolType::Address), - "gasprice" => Some(DynSolType::Uint(256)), - _ => None, - }, - "abi" => match access { - "decode" => { - // args = Some([Bytes(_), Tuple(args)]) - // unwrapping is safe because this is first compiled by solc so - // it is guaranteed to be a valid call - let mut args = args.unwrap(); - let last = args.pop().unwrap(); - match last { - Some(ty) => { - return match ty { - Self::Tuple(_) => ty, - ty => Self::Tuple(vec![Some(ty)]), - }; - } - None => None, - } - } - s if s.starts_with("encode") => Some(DynSolType::Bytes), - _ => None, - }, - "address" => match access { - "balance" => Some(DynSolType::Uint(256)), - "code" => Some(DynSolType::Bytes), - "codehash" => Some(DynSolType::FixedBytes(32)), - "send" => Some(DynSolType::Bool), - _ => None, - }, - "type" => match access { - "name" => Some(DynSolType::String), - "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), - "interfaceId" => Some(DynSolType::FixedBytes(4)), - "min" | "max" => Some( - // Either a builtin or an enum - (|| args?.pop()??.into_builtin())() - .unwrap_or(DynSolType::Uint(256)), - ), - _ => None, - }, - "string" => match access { - "concat" => Some(DynSolType::String), - _ => None, - }, - "bytes" => match access { - "concat" => Some(DynSolType::Bytes), - _ => None, - }, - _ => None, - } - } - _ => None, - } - }; - - this.map(Self::Builtin).unwrap_or_else(|| match types.last().unwrap().as_str() { - "this" | "super" => Self::Custom(types), - _ => match self { - Self::Custom(_) | Self::Access(_, _) => Self::Custom(types), - Self::Function(_, _, _) => self, - _ => unreachable!(), - }, - }) - } - - /// Recurses over itself, appending all the idents and function arguments in the order that they - /// are found - fn recurse(&self, types: &mut Vec, args: &mut Option>>) { - match self { - Self::Builtin(ty) => types.push(ty.to_string()), - Self::Custom(tys) => types.extend(tys.clone()), - Self::Access(expr, name) => { - types.push(name.clone()); - expr.recurse(types, args); + }; } - Self::Function(fn_name, fn_args, _fn_ret) => { - if args.is_none() && !fn_args.is_empty() { - *args = Some(fn_args.clone()); + Some(Res::Item(ItemId::Function(fid))) if lookup => { + let func = gcx.hir.function(*fid); + if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) { + return None; } - fn_name.recurse(types, args); + let ret_id = *func.returns.first()?; + return solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())); } _ => {} } } - /// Infers a custom type's true type by recursing up the parse tree - /// - /// ### Takes - /// - A reference to the [IntermediateOutput] - /// - An array of custom types generated by the `MemberAccess` arm of [Self::from_expression] - /// - An optional contract name. This should always be `None` when this function is first - /// called. - /// - /// ### Returns - /// - /// If successful, an `Ok(Some(DynSolType))` variant. - /// If gracefully failed, an `Ok(None)` variant. - /// If failed, an `Err(e)` variant. - fn infer_custom_type( - intermediate: &IntermediateOutput, - custom_type: &mut Vec, - contract_name: Option, - ) -> Result> { - if let Some("this" | "super") = custom_type.last().map(String::as_str) { - custom_type.pop(); + // Fall back to the callee's resolved type. + expr_to_dyn(gcx, callee, lookup) +} + +/// Extracts a name chain from a member-access expression tree for HIR lookup. +/// +/// The chain is ordered outermost-first so `a.b.c` produces `["c", "b", "a"]` with the root +/// identifier at the back. This matches the convention expected by [`infer_custom_type`]. +fn expr_name_chain(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option> { + match &expr.kind { + ExprKind::Ident(reses) => { + let res = reses.first()?; + let name = match *res { + Res::Item(ItemId::Variable(vid)) => gcx.hir.variable(vid).name?.name, + Res::Item(ItemId::Function(fid)) => gcx.hir.function(fid).name?.name, + Res::Item(ItemId::Contract(cid)) => gcx.hir.contract(cid).name.name, + Res::Builtin(b) => b.name(), + _ => return None, + }; + Some(vec![name]) } - if custom_type.is_empty() { - return Ok(None); + ExprKind::Member(lhs, ident) => { + let mut chain = expr_name_chain(gcx, lhs)?; + chain.insert(0, ident.name); + Some(chain) } + _ => None, + } +} - // If a contract exists with the given name, check its definitions for a match. - // Otherwise look in the `run` - if let Some(contract_name) = contract_name { - let intermediate_contract = intermediate - .intermediate_contracts - .get(&contract_name) - .ok_or_else(|| eyre::eyre!("Could not find intermediate contract!"))?; - - let cur_type = custom_type.last().unwrap(); - if let Some(func) = intermediate_contract.function_definitions.get(cur_type) { - // Check if the custom type is a function pointer member access - if let res @ Some(_) = func_members(func, custom_type) { - return Ok(res); - } - - // Because tuple types cannot be passed to `abi.encode`, we will only be - // receiving functions that have 0 or 1 return parameters here. - if func.returns.is_empty() { - eyre::bail!( - "This call expression does not return any values to inspect. Insert as statement." - ) - } +/// Infers a custom type's true type by recursing through the HIR. +/// +/// `custom_type` is a name chain ordered outermost-first (root at back). This is mutated during +/// resolution. `contract_id` narrows the search to a specific contract scope. +fn infer_custom_type( + gcx: Gcx<'_>, + custom_type: &mut Vec, + contract_id: Option, +) -> Result> { + if let Some(last) = custom_type.last() + && (last.as_str() == "this" || last.as_str() == "super") + { + custom_type.pop(); + } + if custom_type.is_empty() { + return Ok(None); + } - // Empty return types check is done above - let (_, param) = func.returns.first().unwrap(); - // Return type should always be present - let return_ty = ¶m.as_ref().unwrap().ty; - - // If the return type is a variable (not a type expression), re-enter the recursion - // on the same contract for a variable / struct search. It could be a contract, - // struct, array, etc. - if let pt::Expression::Variable(ident) = return_ty { - custom_type.push(ident.name.clone()); - return Self::infer_custom_type(intermediate, custom_type, Some(contract_name)); - } + if let Some(cid) = contract_id { + let hir = &gcx.hir; + let contract = hir.contract(cid); - // Check if our final function call alters the state. If it does, we bail so that it - // will be inserted normally without inspecting. If the state mutability was not - // expressly set, the function is inferred to alter state. - if let Some(pt::FunctionAttribute::Mutability(_mut)) = func - .attributes - .iter() - .find(|attr| matches!(attr, pt::FunctionAttribute::Mutability(_))) - { - if let pt::Mutability::Payable(_) = _mut { - eyre::bail!("This function mutates state. Insert as a statement.") - } - } else { - eyre::bail!("This function mutates state. Insert as a statement.") - } + let cur_name = *custom_type.last().unwrap(); + let cur = cur_name.as_str(); - Ok(Self::ethabi(return_ty, Some(intermediate))) - } else if let Some(var) = intermediate_contract.variable_definitions.get(cur_type) { - Self::infer_var_expr(&var.ty, Some(intermediate), custom_type) - } else if let Some(strukt) = intermediate_contract.struct_definitions.get(cur_type) { - let inner_types = strukt - .fields - .iter() - .map(|var| { - Self::ethabi(&var.ty, Some(intermediate)) - .ok_or_else(|| eyre::eyre!("Struct `{cur_type}` has invalid fields")) - }) - .collect::>>()?; - Ok(Some(DynSolType::Tuple(inner_types))) - } else { - eyre::bail!( - "Could not find any definition in contract \"{contract_name}\" for type: {custom_type:?}" - ) - } - } else { - // Check if the custom type is a variable or function within the REPL contract before - // anything. If it is, we can stop here. - if let Ok(res) = Self::infer_custom_type(intermediate, custom_type, Some("REPL".into())) - { + // Function? + if let Some(fid) = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + let func = hir.function(fid); + if let res @ Some(_) = func_members(func, custom_type) { return Ok(res); } - // Check if the first element of the custom type is a known contract. If it is, begin - // our recursion on that contract's definitions. - let name = custom_type.last().unwrap(); - let contract = intermediate.intermediate_contracts.get(name); - if contract.is_some() { - let contract_name = custom_type.pop(); - return Self::infer_custom_type(intermediate, custom_type, contract_name); + if func.returns.is_empty() { + eyre::bail!( + "This call expression does not return any values to inspect. Insert as statement." + ) } - // See [`Type::infer_var_expr`] - let name = custom_type.last().unwrap(); - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - return Self::infer_var_expr(expr, Some(intermediate), custom_type); + let sm = func.state_mutability; + if !matches!(sm, StateMutability::View | StateMutability::Pure) { + eyre::bail!("This function mutates state. Insert as a statement.") } - // The first element of our custom type was neither a variable or a function within the - // REPL contract, move on to globally available types gracefully. - Ok(None) + let ret_id = func.returns[0]; + let ret_var = hir.variable(ret_id); + return Ok(solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) + .or_else(|| hir_ty_to_dyn(gcx, &ret_var.ty))); } - } - /// Infers the type from a variable's type - fn infer_var_expr( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - custom_type: &mut Vec, - ) -> Result> { - // Resolve local (in `run` function) or global (in the `REPL` or other contract) variable - let res = match &expr { - // Custom variable handling - pt::Expression::Variable(ident) => { - let name = &ident.name; - - if let Some(intermediate) = intermediate { - // expression in `run` - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - Self::infer_var_expr(expr, Some(intermediate), custom_type) - } else if intermediate.intermediate_contracts.contains_key(name) { - if custom_type.len() > 1 { - // There is still some recursing left to do: jump into the contract. - custom_type.pop(); - Self::infer_custom_type(intermediate, custom_type, Some(name.clone())) - } else { - // We have no types left to recurse: return the address of the contract. - Ok(Some(DynSolType::Address)) - } - } else { - Err(eyre::eyre!("Could not infer variable type")) - } - } else { - Ok(None) - } - } - other_expr => Ok(Self::ethabi(other_expr, intermediate)), - }; - // re-run everything with the resolved variable in case we're accessing a builtin member - // for example array or bytes length etc - match res { - Ok(Some(ty)) => { - let box_ty = Box::new(Self::Builtin(ty.clone())); - let access = Self::Access(box_ty, custom_type.drain(..).next().unwrap_or_default()); - if let Some(mapped) = access.map_special().try_as_ethabi(intermediate) { - Ok(Some(mapped)) - } else { - Ok(Some(ty)) + // Variable? + if let Some(vid) = contract + .variables() + .find(|&v| hir.variable(v).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + if let Some(ty) = solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) { + custom_type.pop(); + if custom_type.is_empty() { + return Ok(Some(ty)); } + let next_member = custom_type.drain(..).next().unwrap_or(Symbol::DUMMY); + return Ok(dyn_member(&ty, next_member.as_str()).or(Some(ty))); } - res => res, - } - } - - /// Attempt to convert this type into a [DynSolType] - /// - /// ### Takes - /// An immutable reference to an [IntermediateOutput] - /// - /// ### Returns - /// Optionally, a [DynSolType] - fn try_as_ethabi(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - Self::Tuple(types) => Some(DynSolType::Tuple(types_to_parameters(types, intermediate))), - Self::Array(inner) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::Array(Box::new(inner))), - }, - Self::FixedArray(inner, size) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::FixedArray(Box::new(inner), size)), - }, - ty @ Self::ArrayIndex(_, _) => ty.into_array_index(intermediate), - Self::Function(ty, _, _) => ty.try_as_ethabi(intermediate), - // should have been mapped to `Custom` in previous steps - Self::Access(_, _) => None, - Self::Custom(mut types) => { - // Cover any local non-state-modifying function call expressions - intermediate.and_then(|intermediate| { - Self::infer_custom_type(intermediate, &mut types, None).ok().flatten() - }) - } + let var = hir.variable(vid); + return infer_var_ty(gcx, &var.ty, custom_type); } - } - - /// Equivalent to `Type::from_expression` + `Type::map_special` + `Type::try_as_ethabi` - fn ethabi( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - ) -> Option { - Self::from_expression(expr) - .map(Self::map_special) - .and_then(|ty| ty.try_as_ethabi(intermediate)) - } - /// Get the return type of a function call expression. - fn get_function_return_type<'a>( - contract_expr: Option<&'a pt::Expression>, - intermediate: &IntermediateOutput, - ) -> Option<(&'a pt::Expression, DynSolType)> { - let function_call = match contract_expr? { - pt::Expression::FunctionCall(_, function_call, _) => function_call, - _ => return None, - }; - let (contract_name, function_name) = match function_call.as_ref() { - pt::Expression::MemberAccess(_, contract_name, function_name) => { - (contract_name, function_name) + // Struct? + if let Some(sid) = contract.items.iter().find_map(|i| { + if let ItemId::Struct(sid) = i + && hir.strukt(*sid).name.as_str() == cur + { + Some(*sid) + } else { + None } - _ => return None, - }; - let contract_name = match contract_name.as_ref() { - pt::Expression::Variable(contract_name) => contract_name.to_owned(), - _ => return None, - }; - - let pt::Expression::Variable(contract_name) = - intermediate.repl_contract_expressions.get(&contract_name.name)? - else { - return None; - }; - - let contract = intermediate - .intermediate_contracts - .get(&contract_name.name)? - .function_definitions - .get(&function_name.name)?; - let return_parameter = contract.as_ref().returns.first()?.to_owned().1?; - Self::ethabi(&return_parameter.ty, Some(intermediate)).map(|p| (contract_expr.unwrap(), p)) - } - - /// Inverts Int to Uint and vice-versa. - fn invert_int(self) -> Self { - match self { - Self::Builtin(DynSolType::Uint(n)) => Self::Builtin(DynSolType::Int(n)), - Self::Builtin(DynSolType::Int(n)) => Self::Builtin(DynSolType::Uint(n)), - x => x, + }) { + let inner = gcx + .struct_field_types(sid) + .iter() + .map(|&t| { + solar_ty_to_dyn(gcx, t) + .ok_or_else(|| eyre::eyre!("Struct `{cur}` has invalid fields")) + }) + .collect::>>()?; + return Ok(Some(DynSolType::Tuple(inner))); } - } - /// Returns the `DynSolType` contained by `Type::Builtin` - #[inline] - fn into_builtin(self) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - _ => None, - } + eyre::bail!( + "Could not find any definition in contract \"{}\" for type: {custom_type:?}", + contract.name.as_str() + ) } - /// Returns the resulting `DynSolType` of indexing self - fn into_array_index(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Array(inner) | Self::FixedArray(inner, _) | Self::ArrayIndex(inner, _) => { - match inner.try_as_ethabi(intermediate) { - Some(DynSolType::Array(inner) | DynSolType::FixedArray(inner, _)) => { - Some(*inner) - } - Some(DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_)) => { - Some(DynSolType::FixedBytes(1)) - } - ty => ty, - } - } - _ => None, - } + let repl_id = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == "REPL").then_some(cid)); + if let Some(repl_id) = repl_id + && let Ok(res) = infer_custom_type(gcx, custom_type, Some(repl_id)) + { + return Ok(res); } - /// Returns whether this type is dynamic - #[inline] - const fn is_dynamic(&self) -> bool { - match self { - // TODO: Note, this is not entirely correct. Fixed arrays of non-dynamic types are - // not dynamic, nor are tuples of non-dynamic types. - Self::Builtin(DynSolType::Bytes | DynSolType::String | DynSolType::Array(_)) => true, - Self::Array(_) => true, - _ => false, - } + let last_name = *custom_type.last().unwrap(); + let last = last_name.as_str(); + let contract_match = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == last).then_some(cid)); + if let Some(cid) = contract_match { + custom_type.pop(); + return infer_custom_type(gcx, custom_type, Some(cid)); } - /// Returns whether this type is an array - #[inline] - const fn is_array(&self) -> bool { - matches!( - self, - Self::Array(_) - | Self::FixedArray(_, _) - | Self::Builtin(DynSolType::Array(_) | DynSolType::FixedArray(_, _)) - ) - } + Ok(None) +} - /// Returns whether this type is a dynamic array (can call push, pop) - #[inline] - const fn is_dynamic_array(&self) -> bool { - matches!(self, Self::Array(_) | Self::Builtin(DynSolType::Array(_))) +/// Infers the type from a variable's HIR type, optionally accessing a named member. +fn infer_var_ty( + gcx: Gcx<'_>, + ty: &HirType<'_>, + custom_type: &mut Vec, +) -> Result> { + let Some(ty) = hir_ty_to_dyn(gcx, ty) else { return Ok(None) }; + let next_member = custom_type.drain(..).next(); + if let Some(m) = next_member { + Ok(dyn_member(&ty, m.as_str()).or(Some(ty))) + } else { + Ok(Some(ty)) } +} - const fn is_fixed_bytes(&self) -> bool { - matches!(self, Self::Builtin(DynSolType::FixedBytes(_))) - } +/// Get the return type of a contract method call `receiver.method()`. +fn get_function_return_type(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option { + let ExprKind::Call(callee, _, _) = &expr.kind else { return None }; + let ExprKind::Member(obj, fn_ident) = &callee.kind else { return None }; + let ExprKind::Ident(reses) = &obj.kind else { return None }; + let res = reses.first()?; + let var_id = match res { + Res::Item(ItemId::Variable(vid)) => *vid, + _ => return None, + }; + let var_ty = gcx.type_of_item(var_id.into()).peel_refs(); + let cid = match var_ty.kind { + TyKind::Contract(cid) => cid, + _ => return None, + }; + + let hir = &gcx.hir; + let contract = hir.contract(cid); + let fid = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some(fn_ident.as_str()))?; + let func = hir.function(fid); + let ret_id = *func.returns.first()?; + solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) } -/// Returns Some if the custom type is a function member access +/// Returns Some if the custom type is a function member access. /// /// Ref: #[inline] -fn func_members(func: &pt::FunctionDefinition, custom_type: &[String]) -> Option { - if !matches!(func.ty, pt::FunctionTy::Function) { +fn func_members(func: &Function<'_>, custom_type: &[Symbol]) -> Option { + if !matches!(func.kind, FunctionKind::Function) { return None; } - - let vis = func.attributes.iter().find_map(|attr| match attr { - pt::FunctionAttribute::Visibility(vis) => Some(vis), - _ => None, - }); - match vis { - Some(pt::Visibility::External(_) | pt::Visibility::Public(_)) => { - match custom_type.first().unwrap().as_str() { - "address" => Some(DynSolType::Address), - "selector" => Some(DynSolType::FixedBytes(4)), - _ => None, - } - } + if !matches!(func.visibility, Visibility::External | Visibility::Public) { + return None; + } + match custom_type.first().unwrap().as_str() { + "address" => Some(DynSolType::Address), + "selector" => Some(DynSolType::FixedBytes(4)), _ => None, } } -/// Whether execution should continue after inspecting this expression +/// Whether execution should continue after inspecting this expression. #[inline] -fn should_continue(expr: &pt::Expression) -> bool { - match expr { - // assignments - pt::Expression::PreDecrement(_, _) | // -- - pt::Expression::PostDecrement(_, _) | // -- - pt::Expression::PreIncrement(_, _) | // ++ - pt::Expression::PostIncrement(_, _) | // ++ - pt::Expression::Assign(_, _, _) | // = ... - pt::Expression::AssignAdd(_, _, _) | // += ... - pt::Expression::AssignSubtract(_, _, _) | // -= ... - pt::Expression::AssignMultiply(_, _, _) | // *= ... - pt::Expression::AssignDivide(_, _, _) | // /= ... - pt::Expression::AssignModulo(_, _, _) | // %= ... - pt::Expression::AssignAnd(_, _, _) | // &= ... - pt::Expression::AssignOr(_, _, _) | // |= ... - pt::Expression::AssignXor(_, _, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, _, _) | // <<= ... - pt::Expression::AssignShiftRight(_, _, _) // >>= ... - => { - true - } - +fn should_continue(expr: &Expr<'_>) -> bool { + match &expr.kind { + // assignments and compound assignments + ExprKind::Assign(_, _, _) => true, + // ++/-- pre/post operations + ExprKind::Unary(op, _) => matches!( + op.kind, + UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec + ), // Array.pop() - pt::Expression::FunctionCall(_, lhs, _) => { - match lhs.as_ref() { - pt::Expression::MemberAccess(_, _inner, access) => access.name == "pop", - _ => false - } - } - - _ => false + ExprKind::Call(callee, _, _) => match &callee.kind { + ExprKind::Member(_, ident) => ident.as_str() == "pop", + _ => false, + }, + _ => false, } } -fn map_parameters(params: &[(pt::Loc, Option)]) -> Vec> { - params - .iter() - .map(|(_, param)| param.as_ref().and_then(|param| Type::from_expression(¶m.ty))) - .collect() +/// Parses an [`Expr`] number/hex literal into a `U256`. Returns `None` if the expression +/// is not a numeric literal. +/// +/// SubDenominations are already applied to numeric literals in solar's HIR. +const fn parse_number_literal(expr: &Expr<'_>) -> Option { + match &expr.kind { + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Number(n) => Some(*n), + _ => None, + }, + _ => None, + } } -fn types_to_parameters( - types: Vec>, - intermediate: Option<&IntermediateOutput>, -) -> Vec { - types.into_iter().filter_map(|ty| ty.and_then(|ty| ty.try_as_ethabi(intermediate))).collect() +/// Maps a solar [`ElementaryType`] to a [`DynSolType`]. +const fn elementary_to_dyn(et: ElementaryType) -> Option { + Some(match et { + ElementaryType::Address(_) => DynSolType::Address, + ElementaryType::Bool => DynSolType::Bool, + ElementaryType::String => DynSolType::String, + ElementaryType::Bytes => DynSolType::Bytes, + ElementaryType::Int(size) => DynSolType::Int(size.bits() as usize), + ElementaryType::UInt(size) => DynSolType::Uint(size.bits() as usize), + ElementaryType::FixedBytes(size) => DynSolType::FixedBytes(size.bytes() as usize), + // Fixed-point numbers are not yet representable as DynSolType. + ElementaryType::Fixed(_, _) | ElementaryType::UFixed(_, _) => return None, + }) } -fn parse_number_literal(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::NumberLiteral(_, num, exp, unit) => { - let num = num.parse::().unwrap_or(U256::ZERO); - let exp = exp.parse().unwrap_or(0u32); - if exp > 77 { - None +/// Maps a solar [`Ty`] to a [`DynSolType`]. +fn solar_ty_to_dyn<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> Option { + match ty.kind { + TyKind::Elementary(et) => elementary_to_dyn(et), + TyKind::Ref(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Array(elem, n) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + let size: usize = n.try_into().ok()?; + Some(DynSolType::FixedArray(Box::new(inner), size)) + } + TyKind::DynArray(elem) | TyKind::Slice(elem) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + Some(DynSolType::Array(Box::new(inner))) + } + TyKind::Tuple(tys) => { + Some(DynSolType::Tuple(tys.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect())) + } + TyKind::Mapping(_, _) => None, + TyKind::Struct(sid) => Some(DynSolType::Tuple( + gcx.struct_field_types(sid).iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + TyKind::Enum(_) => Some(DynSolType::Uint(8)), + TyKind::Udvt(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Contract(_) => Some(DynSolType::Address), + // For a function-pointer type we return the ABI type of what the call *produces*, not a + // representation of the pointer itself. This is intentional: chisel inspects values, so + // the interesting type is the returned value. A zero-return function pointer has no + // inspectable value, so we return `None`. + TyKind::FnPtr(f) => match f.returns.len() { + 0 => None, + 1 => solar_ty_to_dyn(gcx, f.returns[0]), + _ => Some(DynSolType::Tuple( + f.returns.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + }, + TyKind::Type(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::Meta(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::IntLiteral(neg, size) => { + let bits = (size.bits() as usize).max(8); + // Round up to the nearest multiple of 8 bits, capped at 256. + let bits = bits.div_ceil(8) * 8; + let bits = bits.min(256); + if neg { + Some(DynSolType::Int(bits.max(8))) } else { - let exp = U256::from(10usize.pow(exp)); - let unit_mul = unit_multiplier(unit).ok()?; - Some(num * exp * unit_mul) + Some(DynSolType::Uint(bits.max(8))) } } - pt::Expression::HexNumberLiteral(_, num, unit) => { - let unit_mul = unit_multiplier(unit).ok()?; - num.parse::().map(|num| num * unit_mul).ok() + TyKind::StringLiteral(valid_utf8, _) => { + if valid_utf8 { + Some(DynSolType::String) + } else { + Some(DynSolType::Bytes) + } } - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(..) => None, + TyKind::Module(_) + | TyKind::BuiltinModule(_) + | TyKind::Error(_, _) + | TyKind::Event(_, _) + | TyKind::Err(_) => None, _ => None, } } -#[inline] -fn unit_multiplier(unit: &Option) -> Result { - if let Some(unit) = unit { - let mul = match unit.name.as_str() { - "seconds" => 1, - "minutes" => 60, - "hours" => 60 * 60, - "days" => 60 * 60 * 24, - "weeks" => 60 * 60 * 24 * 7, - "wei" => 1, - "gwei" => 10_usize.pow(9), - "ether" => 10_usize.pow(18), - other => eyre::bail!("unknown unit: {other}"), - }; - Ok(U256::from(mul)) - } else { - Ok(U256::from(1)) - } -} - #[cfg(test)] mod tests { use super::*; use foundry_compilers::{error::SolcError, solc::Solc}; + use solar::sema::Compiler; use std::sync::Mutex; #[test] @@ -1558,46 +1455,66 @@ mod tests { DynSolType::FixedArray(Box::new(ty), len) } - fn parse(s: &mut SessionSource, input: &str, clear: bool) -> IntermediateOutput { + /// Lowers the given snippet appended to the REPL contract via solar's HIR pipeline (without + /// invoking solc) and returns the resulting `DynSolType` of the last expression statement in + /// the run() body. + /// + /// Tests bypass `SessionSource::build` (which routes through foundry-compilers + solc) so that + /// inputs which are syntactically valid but semantically rejected by solc (e.g. + /// `abi.decode(bytes, (uint8[13]))` or `a[0:3]` on a memory array) can still exercise the + /// HIR-based type-inference engine. + fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { if clear { s.clear(); } + // Always declare a sample enum so `Enum1` is available for `type(Enum1)` tests. *s = s.clone_with_new_line("enum Enum1 { A }".into()).unwrap().0; let input = format!("{};", input.trim_end().trim_end_matches(';')); - let (mut _s, _) = s.clone_with_new_line(input).unwrap(); - *s = _s.clone(); - let s = &mut _s; - - if let Err(e) = s.parse() { - let source = s.to_repl_source(); - panic!("{e}\n\ncould not parse input:\n{source}") - } - s.generate_intermediate_output().expect("could not generate intermediate output") - } - - fn expr(stmts: &[pt::Statement]) -> pt::Expression { - match stmts.last().expect("no statements") { - pt::Statement::Expression(_, e) => e.clone(), - s => panic!("Not an expression: {s:?}"), - } - } - - fn get_type( - s: &mut SessionSource, - input: &str, - clear: bool, - ) -> (Option, IntermediateOutput) { - let intermediate = parse(s, input, clear); - let run_func_body = intermediate.run_func_body().expect("no run func body"); - let expr = expr(run_func_body); - (Type::from_expression(&expr).map(Type::map_special), intermediate) - } + let (new_source, _) = s.clone_with_new_line(input).unwrap(); + *s = new_source.clone(); + + let src = new_source.to_repl_source(); + let sess = + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(); + let mut compiler = Compiler::new(sess); + + compiler.enter_mut(|c| -> Option { + // Stage 1: parse + lower (mutable access required). + let lowered = { + let mut pcx = c.parse(); + let file = c + .sess() + .source_map() + .new_source_file( + std::path::PathBuf::from(new_source.file_name.clone()), + src.clone(), + ) + .ok()?; + pcx.add_file(file); + pcx.parse(); + matches!(c.lower_asts(), Ok(ControlFlow::Continue(()))) + }; + if !lowered { + return None; + } - fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { - let (ty, intermediate) = get_type(s, input, clear); - ty.and_then(|ty| ty.try_as_ethabi(Some(&intermediate))) + // Stage 2: walk HIR (immutable access). + let gcx = c.gcx(); + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + let last = body.last()?; + let expr = match last.kind { + StmtKind::Expr(e) => e, + _ => return None, + }; + expr_to_dyn(gcx, expr, true) + }) } fn generic_type_test<'a, T, I>(s: &mut SessionSource, input: I) diff --git a/crates/chisel/src/source.rs b/crates/chisel/src/source.rs index 8133de364c1da..90c0bad874622 100644 --- a/crates/chisel/src/source.rs +++ b/crates/chisel/src/source.rs @@ -5,7 +5,6 @@ //! execution helpers. use eyre::Result; -use foundry_common::fs; use foundry_compilers::{ Artifact, ProjectCompileOutput, artifacts::{ConfigurableContractArtifact, Source, Sources}, @@ -16,9 +15,16 @@ use foundry_config::{Config, SolcReq}; use foundry_evm::{backend::Backend, core::bytecode::InstIter, opts::EvmOpts}; use semver::Version; use serde::{Deserialize, Serialize}; -use solang_parser::pt::{self, CodeLocation}; -use solar::interface::diagnostics::EmittedDiagnostics; -use std::{cell::OnceCell, collections::HashMap, fmt, path::PathBuf}; +use solar::{ + ast::{ItemKind, StmtKind as AstStmtKind, yul}, + interface::{Span, diagnostics::EmittedDiagnostics}, + sema::{ + CompilerRef, + hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind}, + ty::Gcx, + }, +}; +use std::{cell::OnceCell, fmt}; use walkdir::WalkDir; /// The minimum Solidity version of the `Vm` interface. @@ -30,41 +36,8 @@ static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol"); /// [`SessionSource`] build output. pub struct GeneratedOutput { output: ProjectCompileOutput, - pub(crate) intermediate: IntermediateOutput, -} - -pub struct GeneratedOutputRef<'a> { - output: &'a ProjectCompileOutput, - // compiler: &'b solar::sema::CompilerRef<'c>, - pub(crate) intermediate: &'a IntermediateOutput, -} - -/// Intermediate output for the compiled [SessionSource] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IntermediateOutput { - /// All expressions within the REPL contract's run function and top level scope. - pub repl_contract_expressions: HashMap, - /// Intermediate contracts - pub intermediate_contracts: IntermediateContracts, -} - -/// A refined intermediate parse tree for a contract that enables easy lookups -/// of definitions. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IntermediateContract { - /// All function definitions within the contract - pub function_definitions: HashMap>, - /// All event definitions within the contract - pub event_definitions: HashMap>, - /// All struct definitions within the contract - pub struct_definitions: HashMap>, - /// All variable definitions within the top level scope of the contract - pub variable_definitions: HashMap>, } -/// A defined type for a map of contract names to [IntermediateContract]s -type IntermediateContracts = HashMap; - impl fmt::Debug for GeneratedOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("GeneratedOutput").finish_non_exhaustive() @@ -72,158 +45,25 @@ impl fmt::Debug for GeneratedOutput { } impl GeneratedOutput { - pub fn enter(&self, f: impl FnOnce(GeneratedOutputRef<'_>) -> T + Send) -> T { - // TODO(dani): once intermediate is removed - // self.output - // .parser() - // .solc() - // .compiler() - // .enter(|compiler| f(GeneratedOutputRef { output: &self.output, compiler })) - f(GeneratedOutputRef { output: &self.output, intermediate: &self.intermediate }) - } -} - -impl GeneratedOutputRef<'_> { - pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> { - self.output.find_first("REPL") - } -} - -impl std::ops::Deref for GeneratedOutput { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - &self.intermediate - } -} -impl std::ops::Deref for GeneratedOutputRef<'_> { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - self.intermediate + /// Enters the solar compiler context, providing access to the HIR and `Gcx`. + pub fn enter( + &self, + f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send, + ) -> R { + self.output + .parser() + .solc() + .compiler() + .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c })) } } -impl IntermediateOutput { - pub fn get_event(&self, input: &str) -> Option<&pt::EventDefinition> { - self.intermediate_contracts - .get("REPL") - .and_then(|contract| contract.event_definitions.get(input).map(std::ops::Deref::deref)) - } - - pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { - let deployed_bytecode = contract - .get_deployed_bytecode() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - let deployed_bytecode_bytes = deployed_bytecode - .bytes() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - - let run_func_statements = self.run_func_body()?; - - // Record loc of first yul block return statement (if any). - // This is used to decide which is the final statement within the `run()` method. - // see . - let last_yul_return = run_func_statements.iter().find_map(|statement| { - if let pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } = statement - && let Some(statement) = block.statements.last() - && let pt::YulStatement::FunctionCall(yul_call) = statement - && yul_call.id.name == "return" - { - return Some(statement.loc()); - } - None - }); - - // Find the last statement within the "run()" method and get the program - // counter via the source map. - let Some(final_statement) = run_func_statements.last() else { return Ok(None) }; - - // If the final statement is some type of block (assembly, unchecked, or regular), - // we need to find the final statement within that block. Otherwise, default to - // the source loc of the final statement of the `run()` function's block. - // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. - let mut source_loc = match final_statement { - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(statement) = last_statement { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the asm block. Because we use saturating sub to get the second - // to last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - pt::Statement::Block { loc: _, unchecked: _, statements } => { - if let Some(statement) = statements.last() { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - _ => final_statement.loc(), - }; - - // Consider yul return statement as final statement (if it's loc is lower) . - if let Some(yul_return) = last_yul_return - && yul_return.end() < source_loc.start() - { - source_loc = yul_return; - } - - // Map the source location of the final statement of the `run()` function to its - // corresponding runtime program counter - let final_pc = { - let offset = source_loc.start() as u32; - let length = (source_loc.end() - source_loc.start()) as u32; - trace!(%offset, %length, "find pc"); - contract - .get_source_map_deployed() - .unwrap() - .unwrap() - .into_iter() - .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) - .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, pc)| pc) - .max() - }; - trace!(?final_pc); - Ok(final_pc) - } - - pub fn run_func_body(&self) -> Result<&Vec> { - match self - .intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find REPL intermediate contract!"))? - .function_definitions - .get("run") - .ok_or_else(|| eyre::eyre!("Could not find run function definition in REPL contract!"))? - .body - .as_ref() - .ok_or_else(|| eyre::eyre!("Could not find run function body!"))? - { - pt::Statement::Block { statements, .. } => Ok(statements), - _ => eyre::bail!("Could not find statements within run function body!"), - } - } +/// A scoped reference to a [`GeneratedOutput`] together with an entered solar compiler. +pub struct GeneratedOutputRef<'a, 'b, 'gcx> { + output: &'a ProjectCompileOutput, + pub(crate) compiler: &'b CompilerRef<'gcx>, } -// TODO(dani): further migration blocked on upstream work -#[cfg(false)] impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { pub fn gcx(&self) -> Gcx<'gcx> { self.compiler.gcx() @@ -233,8 +73,35 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { self.output.find_first("REPL") } - pub fn get_event(&self, input: &str) -> Option { - self.gcx().hir.events_enumerated().find(|(_, e)| e.name.as_str() == input).map(|(id, _)| id) + /// Looks up the REPL contract in the HIR. + pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> { + self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + } + + /// Returns the body block of the REPL `run()` function. + pub fn run_func_body(&self) -> Block<'gcx> { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); + let f = c + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) + .expect("`run()` function not found in REPL contract"); + hir.function(f).body.expect("`run()` function does not have a body") + } + + /// Returns the [`EventId`] of an event named `input` in the REPL contract, if any. + pub fn get_event(&self, input: &str) -> Option { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir()?; + c.items.iter().find_map(|id| { + if let ItemId::Event(eid) = id + && hir.event(*eid).name.as_str() == input + { + Some(*eid) + } else { + None + } + }) } pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { @@ -251,52 +118,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Record loc of first yul block return statement (if any). // This is used to decide which is the final statement within the `run()` method. // see . - let last_yul_return_span: Option = run_body.iter().find_map(|stmt| { - // TODO(dani): Yul is not yet lowered to HIR. - let _ = stmt; - /* - if let hir::StmtKind::Assembly { block, .. } = stmt { - if let Some(stmt) = block.last() { - if let pt::YulStatement::FunctionCall(yul_call) = stmt { - if yul_call.id.name == "return" { - return Some(stmt.loc()) - } - } - } - } - */ - None - }); + // + // Yul is not yet lowered to HIR (assembly statements appear as `StmtKind::Err`), + // so we walk the AST of the REPL source to find a top-level `return(...)` call + // inside any `assembly { ... }` block in `run()`. + let last_yul_return_span: Option = self.first_yul_return_span(); // Find the last statement within the "run()" method and get the program // counter via the source map. let Some(last_stmt) = run_body.last() else { return Ok(None) }; - // If the final statement is some type of block (assembly, unchecked, or regular), + // If the final statement is some type of block (unchecked or regular), // we need to find the final statement within that block. Otherwise, default to // the source loc of the final statement of the `run()` function's block. // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. + // Inline assembly blocks (lowered to `StmtKind::Err` in HIR in the pinned solar + // version) are handled separately via `trailing_assembly_last_stmt_span`, which + // walks the AST to recover the last meaningful Yul statement. let source_stmt = match &last_stmt.kind { - // TODO(dani): Yul is not yet lowered to HIR. - /* - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(stmt) = last_statement { - stmt - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - &run_body[run_body.len().saturating_sub(2)] - } - } - */ - hir::StmtKind::UncheckedBlock(stmts) | hir::StmtKind::Block(stmts) => { + HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => { if let Some(stmt) = stmts.last() { stmt } else { @@ -308,9 +148,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { } _ => last_stmt, }; - let mut source_span = self.stmt_span_without_semicolon(source_stmt); + // If the trailing statement is an assembly block, prefer the last meaningful + // (non-`let`) Yul statement's span as the source location for `final_pc`. + // See . + // + // Two guards are required: + // 1. `StmtKind::Err`, assembly lowers to an error node in the current pinned solar + // version; this ensures we don't apply the AST fallback to properly-lowered stmts. + // 2. `trailing_assembly_last_stmt_span` returning `Some`, verifies via the AST that the + // failing HIR node actually corresponds to an assembly block (not some other lowering + // failure), and supplies the concrete span to use. + let mut source_span = if matches!(last_stmt.kind, HirStmtKind::Err(_)) + && let Some(span) = self.trailing_assembly_last_stmt_span() + { + span + } else { + self.stmt_span_without_semicolon(source_stmt) + }; - // Consider yul return statement as final statement (if it's loc is lower) . + // Consider yul return statement as final statement (if it's loc is lower). if let Some(yul_return_span) = last_yul_return_span && yul_return_span.hi() < source_span.lo() { @@ -319,26 +175,32 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Map the source location of the final statement of the `run()` function to its // corresponding runtime program counter - let (_sf, range) = self.compiler.sess().source_map().span_to_source(source_span).unwrap(); - dbg!(source_span, &range, &_sf.src[range.clone()]); + let result = self + .compiler + .sess() + .source_map() + .span_to_source(source_span) + .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?; + let range = result.data; let offset = range.start as u32; let length = range.len() as u32; - let final_pc = deployed_bytecode - .source_map() + trace!(%offset, %length, "find pc"); + let final_pc = contract + .get_source_map_deployed() .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))?? .into_iter() - .zip(InstructionIter::new(deployed_bytecode_bytes)) + .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, i)| i.pc) - .max() - .unwrap_or_default(); - Ok(Some(final_pc)) + .map(|(_, pc)| pc) + .max(); + trace!(?final_pc); + Ok(final_pc) } /// Statements' ranges in the solc source map do not include the semicolon. - fn stmt_span_without_semicolon(&self, stmt: &hir::Stmt<'_>) -> Span { + fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span { match stmt.kind { - hir::StmtKind::DeclSingle(id) => { + HirStmtKind::DeclSingle(id) => { let decl = self.gcx().hir.variable(id); if let Some(expr) = decl.initializer { stmt.span.with_hi(expr.span.hi()) @@ -346,23 +208,65 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { stmt.span } } - hir::StmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), - hir::StmtKind::Expr(expr) => expr.span, + HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), + HirStmtKind::Expr(expr) => expr.span, _ => stmt.span, } } - fn run_func_body(&self) -> hir::Block<'_> { - let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); - let f = c - .functions() - .find(|&f| self.gcx().hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) - .expect("`run()` function not found in REPL contract"); - self.gcx().hir.function(f).body.expect("`run()` function does not have a body") + /// Returns the AST `run()` body of the REPL contract, if any. + /// + /// Yul/assembly is not yet lowered to HIR in the pinned solar version, so we + /// keep around the AST to be able to inspect inline assembly blocks. + fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> { + let contract = self.repl_contract_hir()?; + let source = self.gcx().sources.get(contract.source)?; + let ast = source.ast.as_ref()?; + + let contract_ast = ast.items.iter().find_map(|i| match &i.kind { + ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c), + _ => None, + })?; + contract_ast.body.iter().find_map(|i| match &i.kind { + ItemKind::Function(f) if f.header.name.is_some_and(|n| n.as_str() == "run") => { + f.body.as_ref() + } + _ => None, + }) } - fn repl_contract_hir(&self) -> Option<&hir::Contract<'_>> { - self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + /// Returns the span of the first top-level `return(...)` call inside any + /// `assembly { ... }` block in the REPL `run()` function, if any. + fn first_yul_return_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + for stmt in run_body.stmts.iter() { + let AstStmtKind::Assembly(asm) = &stmt.kind else { continue }; + for ystmt in asm.block.stmts.iter() { + if let yul::StmtKind::Expr(e) = &ystmt.kind + && let yul::ExprKind::Call(call) = &e.kind + && call.name.as_str() == "return" + { + return Some(ystmt.span); + } + } + } + None + } + + /// If the last statement of the REPL `run()` function is an `assembly { ... }` block, + /// returns the span of its last non-`let` (i.e. non-VarDecl) Yul statement. + /// + /// This mirrors the legacy behavior used to pick a meaningful end-of-function PC when + /// the trailing statement is inline assembly. + fn trailing_assembly_last_stmt_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None }; + asm.block + .stmts + .iter() + .rev() + .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _))) + .map(|s| s.span) } } @@ -584,8 +488,7 @@ impl SessionSource { return Ok(output); } let output = self.compile()?; - let intermediate = self.generate_intermediate_output()?; - let output = GeneratedOutput { output, intermediate }; + let output = GeneratedOutput { output }; Ok(self.output.get_or_init(|| output)) } @@ -602,12 +505,11 @@ impl SessionSource { eyre::bail!("{output}"); } - // TODO(dani): re-enable - if cfg!(false) { - output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { - let _ = c.lower_asts(); - }); - } + // Drive HIR lowering and analysis so that subsequent `enter` queries can use them. + output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { + let _ = c.lower_asts(); + let _ = c.analysis(); + }); Ok(output) } @@ -632,53 +534,6 @@ impl SessionSource { sources } - /// Generate intermediate contracts for all contract definitions in the compilation source. - /// - /// ### Returns - /// - /// Optionally, a map of contract names to a vec of [IntermediateContract]s. - pub fn generate_intermediate_contracts(&self) -> Result> { - let mut res_map = HashMap::default(); - let parsed_map = self.get_sources(); - for source in parsed_map.values() { - Self::get_intermediate_contract(&source.content, &mut res_map); - } - Ok(res_map) - } - - /// Generate intermediate output for the REPL contract - pub fn generate_intermediate_output(&self) -> Result { - // Parse generate intermediate contracts - let intermediate_contracts = self.generate_intermediate_contracts()?; - - // Construct variable definitions - let variable_definitions = intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find intermediate REPL contract!"))? - .variable_definitions - .clone() - .into_iter() - .map(|(k, v)| (k, v.ty)) - .collect::>(); - // Construct intermediate output - let mut intermediate_output = IntermediateOutput { - repl_contract_expressions: variable_definitions, - intermediate_contracts, - }; - - // Add all statements within the run function to the repl_contract_expressions map - for (key, val) in intermediate_output - .run_func_body()? - .clone() - .iter() - .flat_map(Self::get_statement_definitions) - { - intermediate_output.repl_contract_expressions.insert(key, val); - } - - Ok(intermediate_output) - } - /// Construct the REPL source. pub fn to_repl_source(&self) -> String { let Self { @@ -741,108 +596,6 @@ contract {contract_name} {{ }); sess.dcx.emitted_errors().unwrap() } - - /// Gets the [IntermediateContract] for a Solidity source string and inserts it into the - /// passed `res_map`. In addition, recurses on any imported files as well. - /// - /// ### Takes - /// - `content` - A Solidity source string - /// - `res_map` - A mutable reference to a map of contract names to [IntermediateContract]s - pub fn get_intermediate_contract( - content: &str, - res_map: &mut HashMap, - ) { - if let Ok((pt::SourceUnit(source_unit_parts), _)) = solang_parser::parse(content, 0) { - let func_defs = source_unit_parts - .into_iter() - .filter_map(|sup| match sup { - pt::SourceUnitPart::ImportDirective(i) => match i { - pt::Import::Plain(s, _) - | pt::Import::Rename(s, _, _) - | pt::Import::GlobalSymbol(s, _, _) => { - let s = match s { - pt::ImportPath::Filename(s) => s.string, - pt::ImportPath::Path(p) => p.to_string(), - }; - let path = PathBuf::from(s); - - match fs::read_to_string(path) { - Ok(source) => { - Self::get_intermediate_contract(&source, res_map); - None - } - Err(_) => None, - } - } - }, - pt::SourceUnitPart::ContractDefinition(cd) => { - let mut intermediate = IntermediateContract::default(); - - cd.parts.into_iter().for_each(|part| match part { - pt::ContractPart::FunctionDefinition(def) => { - // Only match normal function definitions here. - if matches!(def.ty, pt::FunctionTy::Function) { - intermediate - .function_definitions - .insert(def.name.clone().unwrap().name, def); - } - } - pt::ContractPart::EventDefinition(def) => { - let event_name = def.name.as_ref().unwrap().name.clone(); - intermediate.event_definitions.insert(event_name, def); - } - pt::ContractPart::StructDefinition(def) => { - let struct_name = def.name.as_ref().unwrap().name.clone(); - intermediate.struct_definitions.insert(struct_name, def); - } - pt::ContractPart::VariableDefinition(def) => { - let var_name = def.name.as_ref().unwrap().name.clone(); - intermediate.variable_definitions.insert(var_name, def); - } - _ => {} - }); - Some((cd.name.as_ref().unwrap().name.clone(), intermediate)) - } - _ => None, - }) - .collect::>(); - res_map.extend(func_defs); - } - } - - /// Helper to deconstruct a statement - /// - /// ### Takes - /// - /// A reference to a [pt::Statement] - /// - /// ### Returns - /// - /// A vector containing tuples of the inner expressions' names, types, and storage locations. - pub fn get_statement_definitions(statement: &pt::Statement) -> Vec<(String, pt::Expression)> { - match statement { - pt::Statement::VariableDefinition(_, def, _) => { - vec![(def.name.as_ref().unwrap().name.clone(), def.ty.clone())] - } - pt::Statement::Expression(_, pt::Expression::Assign(_, left, _)) => { - if let pt::Expression::List(_, list) = left.as_ref() { - list.iter() - .filter_map(|(_, param)| { - param.as_ref().and_then(|param| { - param - .name - .as_ref() - .map(|name| (name.name.clone(), param.ty.clone())) - }) - }) - .collect() - } else { - Vec::default() - } - } - _ => Vec::default(), - } - } } /// A Parse Tree Fragment diff --git a/crates/chisel/tests/it/repl/mod.rs b/crates/chisel/tests/it/repl/mod.rs index 704d30405eed9..338b7d2043809 100644 --- a/crates/chisel/tests/it/repl/mod.rs +++ b/crates/chisel/tests/it/repl/mod.rs @@ -153,6 +153,26 @@ assembly { repl.expect("[0x00:0x20]"); }); +// Assembly as the final statement with a return — exercises the path where both +// `first_yul_return_span` and `trailing_assembly_last_stmt_span` resolve to the same `return(...)` +// span (no subsequent Solidity statement after the assembly block). +repl_test!(assembly_return_final, |repl| { + repl.sendln("uint x = 0xbeef;"); + repl.sendln("assembly { mstore(0x0, sload(0)) return(0x0, 0x20) }"); + repl.sendln("!md"); + repl.expect("[0x00:0x20]"); +}); + +// Assembly block without a `return(...)` call as an intermediate statement, exercises +// `first_yul_return_span` returning `None` while a subsequent Solidity statement is still evaluated +// correctly. +repl_test!(assembly_no_return_intermediate, |repl| { + repl.sendln("uint x = 1;"); + repl.sendln("assembly { x := add(x, 1) }"); + repl.sendln("x"); + repl.expect("Decimal: 2"); +}); + // Issue #5051, #8978: Test EVM version normalization. repl_test!(flaky_evm_version_normalization, "--use 0.7.6 --evm-version london", |repl| { repl.sendln("uint x;\nx"); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 37340f5f4cc3c..606b8291819e4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -52,6 +52,7 @@ rayon.workspace = true regex = { workspace = true, default-features = false } serde_json.workspace = true serde.workspace = true +toml.workspace = true strsim = "0.11" strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros"] } @@ -70,7 +71,13 @@ tempfile.workspace = true tikv-jemallocator = { workspace = true, optional = true } [features] +default = ["optimism"] tracy = ["dep:tracing-tracy"] tracy-allocator = ["tracy"] jemalloc = ["dep:tikv-jemallocator"] mimalloc = ["dep:mimalloc"] +optimism = [ + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index 87f14e2039606..4fc437c7232f8 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -307,6 +307,17 @@ mod tests { assert_eq!(val, &Value::from(1000u64)); } + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + use clap::CommandFactory; + + let command = EvmArgs::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + #[test] fn can_parse_chain_id() { let args = EvmArgs { diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 8c37860446683..f846da5002354 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -66,8 +66,20 @@ impl figment::Provider for RpcOpts { impl RpcOpts { /// Returns the RPC endpoint. pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>> { + self.url_with_env(config, std::env::var("ETH_RPC_URL").ok()) + } + + fn url_with_env<'a>( + &'a self, + config: Option<&'a Config>, + env_url: Option, + ) -> Result>> { if self.flashbots { Ok(Some(Cow::Borrowed(FLASHBOTS_URL))) + } else if let Some(url) = self.common.rpc_url.as_deref() { + Ok(Some(Cow::Borrowed(url))) + } else if let Some(url) = env_url { + Ok(Some(Cow::Owned(url))) } else { self.common.url(config) } @@ -85,8 +97,10 @@ impl RpcOpts { pub fn dict(&self) -> Dict { let mut dict = self.common.dict(); - if self.flashbots { - dict.insert("eth_rpc_url".into(), FLASHBOTS_URL.into()); + // `self.url(None)` already accounts for `flashbots` and the `ETH_RPC_URL` env var, + // so a single insert here covers both. + if let Ok(Some(url)) = self.url(None) { + dict.insert("eth_rpc_url".into(), url.into_owned().into()); } if let Ok(Some(jwt)) = self.jwt(None) { dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into()); @@ -199,6 +213,7 @@ impl figment::Provider for EthereumOpts { #[cfg(test)] mod tests { use super::*; + use clap::CommandFactory; #[test] fn parse_etherscan_opts() { @@ -223,4 +238,41 @@ mod tests { let id: u64 = chain_id.deserialize().expect("chain_id should deserialize as u64"); assert_eq!(id, 9745); } + + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + let command = RpcOpts::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + + #[test] + fn rpc_url_resolves_eth_rpc_url_env() { + let args = RpcOpts::default(); + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8545"); + } + + #[test] + fn explicit_rpc_url_takes_precedence_over_eth_rpc_url_env() { + let args = RpcOpts { + common: RpcCommonOpts { + rpc_url: Some("http://127.0.0.1:8546".to_string()), + ..Default::default() + }, + ..Default::default() + }; + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8546"); + } } diff --git a/crates/cli/src/opts/rpc_common.rs b/crates/cli/src/opts/rpc_common.rs index 05b98582fa88f..6a5fe5ed4e9e4 100644 --- a/crates/cli/src/opts/rpc_common.rs +++ b/crates/cli/src/opts/rpc_common.rs @@ -17,10 +17,15 @@ use std::borrow::Cow; /// This struct holds fields that both [`super::RpcOpts`] (cast) and /// [`super::EvmArgs`] (forge/script) need, eliminating duplication and /// making the two structs composable. +/// +/// Note: `ETH_RPC_URL` is intentionally **not** bound here as a clap env +/// fallback; otherwise it would be inherited by `EvmArgs` and silently +/// fork all `forge test` runs. Cast resolves `ETH_RPC_URL` explicitly +/// at the call site (see [`super::RpcOpts::url`]). #[derive(Clone, Debug, Default, Serialize, Parser)] pub struct RpcCommonOpts { /// The RPC endpoint. - #[arg(short, long, visible_alias = "fork-url", env = "ETH_RPC_URL")] + #[arg(short, long, visible_alias = "fork-url", value_name = "URL")] #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] pub rpc_url: Option, diff --git a/crates/cli/src/opts/tempo.rs b/crates/cli/src/opts/tempo.rs index 8c2a12e661e18..88119f163b325 100644 --- a/crates/cli/src/opts/tempo.rs +++ b/crates/cli/src/opts/tempo.rs @@ -1,16 +1,26 @@ use alloy_network::{Network, TransactionBuilder}; use alloy_primitives::{Address, ruint::aliases::U256}; -use alloy_signer::Signature; +use alloy_signer::{Signature, Signer}; use clap::Parser; -use foundry_common::FoundryTransactionBuilder; -use std::{num::NonZeroU64, str::FromStr}; +use eyre::Result; +use foundry_common::{ + FoundryTransactionBuilder, + tempo::{TempoSponsor, resolve_tempo_sponsor_signer}, +}; +use std::{ + num::NonZeroU64, + path::PathBuf, + str::FromStr, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::utils::parse_fee_token_address; -/// CLI options for Tempo transactions. +/// CLI options common to Tempo transactions across commands. #[derive(Clone, Debug, Default, Parser)] #[command(next_help_heading = "Tempo")] -pub struct TempoOpts { +pub struct TempoCommonOpts { /// Fee token address for Tempo transactions. /// /// When set, builds a Tempo (type 0x76) transaction that pays gas fees @@ -21,6 +31,40 @@ pub struct TempoOpts { #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] pub fee_token: Option
, + /// Opt into TIP-1009 expiring-nonce mode with a validity window. + /// + /// Convenience flag that combines `--tempo.expiring-nonce` with a relative + /// `--tempo.valid-before`. Sets nonce_key = U256::MAX, nonce = 0, and valid_before = now + + /// seconds. + /// + /// Maximum value is 30 seconds. The transaction must be mined before the deadline or it + /// becomes permanently invalid, giving safe retry semantics: retries produce a fresh tx hash + /// and the old tx can never land late. + #[arg(long = "tempo.expires", value_name = "SECONDS", value_parser = parse_expires_seconds)] + pub expires: Option, +} + +impl TempoCommonOpts { + /// Returns `true` if any Tempo-specific option is set. + pub const fn is_tempo(&self) -> bool { + self.fee_token.is_some() || self.expires.is_some() + } + + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + let secs = self.expires?; + let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards"); + Some(now.as_secs() + secs) + } +} + +/// CLI options for Tempo transactions. +#[derive(Clone, Debug, Default, Parser)] +#[command(next_help_heading = "Tempo")] +pub struct TempoOpts { + #[command(flatten)] + pub common: TempoCommonOpts, + /// Nonce key for Tempo parallelizable nonces. /// /// When set, builds a Tempo (type 0x76) transaction with the specified nonce key, @@ -28,21 +72,69 @@ pub struct TempoOpts { /// to be executed in parallel. If not set, the protocol nonce key (0) will be used. /// /// For more information see . - #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY")] + #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY", conflicts_with = "lane")] pub nonce_key: Option, + /// Named nonce lane for Tempo parallelizable nonces. + /// + /// Resolves a friendly lane name (e.g. `deploy`, `payments`) to a `nonce_key` via a + /// shared lanes file (default: `tempo.lanes.toml` at the project root). The lanes file + /// is a TOML map of `name = ` entries, e.g.: + /// + /// ```toml + /// deploy = 1 + /// ops = 2 + /// payments = 3 + /// ``` + /// + /// Mutually exclusive with `--tempo.nonce-key`. + #[arg(long = "tempo.lane", value_name = "NAME")] + pub lane: Option, + + /// Path to the Tempo lanes file used by `--tempo.lane`. + /// + /// Defaults to `tempo.lanes.toml` at the project root. + #[arg(long = "tempo.lanes-file", value_name = "PATH")] + pub lanes_file: Option, + + /// Sponsor (fee payer) address for Tempo sponsored transactions. + #[arg(long = "tempo.sponsor", value_name = "ADDRESS")] + pub sponsor: Option
, + + /// Sign Tempo sponsor digests in-band with the given signer URI. + /// + /// Supported forms include `env://VAR`, `keystore://PATH`, `account://NAME`, + /// `ledger://`, `trezor://`, `aws://`, `gcp://`, `turnkey://`, and + /// `private-key://KEY`. + #[arg( + long = "tempo.sponsor-signer", + value_name = "SIGNER", + requires = "sponsor", + conflicts_with = "sponsor_sig" + )] + pub sponsor_signer: Option, + /// Sponsor (fee payer) signature for Tempo sponsored transactions. /// /// The sponsor signs the `fee_payer_signature_hash` to commit to paying gas fees /// on behalf of the sender. Provide as a hex-encoded signature. - #[arg(long = "tempo.sponsor-signature", value_parser = parse_signature)] - pub sponsor_signature: Option, + #[arg( + long = "tempo.sponsor-sig", + alias = "tempo.sponsor-signature", + value_parser = parse_signature, + requires = "sponsor", + conflicts_with = "sponsor_signer" + )] + pub sponsor_sig: Option, /// Print the sponsor signature hash and exit. /// /// Computes the `fee_payer_signature_hash` for the transaction so that a sponsor /// knows what hash to sign. The transaction is not sent. - #[arg(long = "tempo.print-sponsor-hash")] + #[arg( + long = "tempo.print-sponsor-hash", + conflicts_with_all = &["sponsor", "sponsor_signer", "sponsor_sig"] + )] pub print_sponsor_hash: bool, /// Access key ID for Tempo Keychain signature transactions. @@ -56,14 +148,14 @@ pub struct TempoOpts { /// /// Sets nonce to 0 and nonce_key to U256::MAX, enabling time-bounded transaction /// validity via `--tempo.valid-before` and `--tempo.valid-after`. - #[arg(long = "tempo.expiring-nonce", requires = "valid_before")] + #[arg(long = "tempo.expiring-nonce", requires = "valid_before", conflicts_with = "expires")] pub expiring_nonce: bool, /// Upper bound timestamp for Tempo expiring nonce transactions. /// /// The transaction is only valid before this unix timestamp. /// Requires `--tempo.expiring-nonce`. - #[arg(long = "tempo.valid-before")] + #[arg(long = "tempo.valid-before", conflicts_with = "expires")] pub valid_before: Option, /// Lower bound timestamp for Tempo expiring nonce transactions. @@ -77,9 +169,12 @@ pub struct TempoOpts { impl TempoOpts { /// Returns `true` if any Tempo-specific option is set. pub const fn is_tempo(&self) -> bool { - self.fee_token.is_some() + self.common.is_tempo() || self.nonce_key.is_some() - || self.sponsor_signature.is_some() + || self.lane.is_some() + || self.sponsor.is_some() + || self.sponsor_signer.is_some() + || self.sponsor_sig.is_some() || self.print_sponsor_hash || self.key_id.is_some() || self.expiring_nonce @@ -87,6 +182,58 @@ impl TempoOpts { || self.valid_after.is_some() } + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + self.common.expires_at() + } + + /// Resolves `--tempo.expires` into concrete expiring-nonce fields. + /// + /// This computes the relative deadline once so later calls to [`Self::apply`] reuse the same + /// `valid_before` timestamp instead of deriving a fresh one. + pub fn resolve_expires(&mut self) -> Option { + let ts = self.expires_at()?; + self.expiring_nonce = true; + self.valid_before = Some(ts); + self.common.expires = None; + Some(ts) + } + + /// Returns `true` if a sponsor signature should be attached before submission. + pub const fn has_sponsor_submission(&self) -> bool { + self.sponsor.is_some() || self.sponsor_signer.is_some() || self.sponsor_sig.is_some() + } + + /// Resolves sponsor CLI options into a reusable sponsor config for transaction submission. + pub async fn sponsor_config(&self) -> Result> { + let Some(sponsor) = self.sponsor else { + return Ok(None); + }; + + let signer = if let Some(spec) = &self.sponsor_signer { + Some(Arc::new(Box::pin(resolve_tempo_sponsor_signer(spec)).await?)) + } else { + None + }; + + if let Some(signer) = &signer { + let signer_address = signer.address(); + if signer_address != sponsor { + eyre::bail!( + "Tempo sponsor signer address {signer_address} does not match --tempo.sponsor {sponsor}" + ); + } + } + + if signer.is_none() && self.sponsor_sig.is_none() { + eyre::bail!( + "--tempo.sponsor requires either --tempo.sponsor-signer or --tempo.sponsor-sig" + ); + } + + Ok(Some(TempoSponsor::new(sponsor, signer, self.sponsor_sig))) + } + /// Applies Tempo-specific options to a transaction request. /// /// All setters are no-ops for non-Tempo networks, so this is safe to call unconditionally. @@ -94,8 +241,9 @@ impl TempoOpts { where N::TransactionRequest: FoundryTransactionBuilder, { - // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX - if self.expiring_nonce { + // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX. + // --tempo.expires is a convenience alias that also sets valid_before = now + duration. + if self.expiring_nonce || self.common.expires.is_some() { tx.set_nonce(0); tx.set_nonce_key(U256::MAX); } else { @@ -107,11 +255,14 @@ impl TempoOpts { } } - if let Some(fee_token) = self.fee_token { + if let Some(fee_token) = self.common.fee_token { tx.set_fee_token(fee_token); } - if let Some(valid_before) = self.valid_before + // --tempo.expires sets valid_before relative to now; --tempo.valid-before takes a raw + // unix timestamp. The two flags are mutually exclusive (enforced by clap). + let effective_valid_before = self.expires_at().or(self.valid_before); + if let Some(valid_before) = effective_valid_before && let Some(v) = NonZeroU64::new(valid_before) { tx.set_valid_before(v); @@ -131,8 +282,7 @@ impl TempoOpts { // gas estimation so that `--tempo.print-sponsor-hash` and // `--tempo.sponsor-signature` produce identical gas estimates. Callers // should call `set_fee_payer_signature` on the built tx request. - if (self.sponsor_signature.is_some() || self.print_sponsor_hash) && tx.nonce_key().is_none() - { + if (self.has_sponsor_submission() || self.print_sponsor_hash) && tx.nonce_key().is_none() { tx.set_nonce_key(U256::ZERO); } } @@ -142,11 +292,83 @@ fn parse_signature(s: &str) -> Result { Signature::from_str(s).map_err(|e| format!("invalid signature: {e}")) } +/// Parses a seconds value for `--tempo.expires`, capped at the protocol maximum of 30 seconds. +fn parse_expires_seconds(s: &str) -> Result { + let secs: u64 = s + .parse() + .map_err(|_| format!("invalid value '{s}': expected an integer number of seconds"))?; + if secs > 30 { + return Err(format!("expires must be at most 30 seconds (got {secs})")); + } + Ok(secs) +} + #[cfg(test)] mod tests { use super::*; use alloy_primitives::address; + #[test] + fn parses_lane_arg() { + let opts = TempoOpts::try_parse_from(["", "--tempo.lane", "deploy"]).unwrap(); + assert_eq!(opts.lane.as_deref(), Some("deploy")); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn lane_conflicts_with_nonce_key() { + let err = + TempoOpts::try_parse_from(["", "--tempo.lane", "deploy", "--tempo.nonce-key", "1"]) + .unwrap_err(); + assert!( + err.to_string().contains("cannot be used with"), + "expected clap conflict error, got: {err}", + ); + } + + #[test] + fn parse_expires_flag() { + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "30"]).unwrap(); + assert_eq!(opts.common.expires, Some(30)); + + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + assert_eq!(opts.common.expires, Some(10)); + + // exceeds 30s maximum + assert!(TempoOpts::try_parse_from(["", "--tempo.expires", "31"]).is_err()); + + // conflicts with --tempo.expiring-nonce + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.expires", + "30", + "--tempo.expiring-nonce", + "--tempo.valid-before", + "999" + ]) + .is_err() + ); + } + + #[test] + fn resolve_expires_materializes_valid_before() { + let before = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + let mut opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + + let resolved = opts.resolve_expires().unwrap(); + let after = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + + assert!(resolved >= before + 10); + assert!(resolved <= after + 10); + assert!(opts.expiring_nonce); + assert_eq!(opts.valid_before, Some(resolved)); + assert_eq!(opts.common.expires, None); + assert_eq!(opts.expires_at(), None); + } + #[test] fn parse_fee_token_id() { let opts = TempoOpts::try_parse_from([ @@ -155,13 +377,69 @@ mod tests { "0x20C0000000000000000000000000000000000002", ]) .unwrap(); - assert_eq!(opts.fee_token, Some(address!("0x20C0000000000000000000000000000000000002")),); + assert_eq!( + opts.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000002")), + ); // AlphaUSD token ID is 1u64 let opts_with_id = TempoOpts::try_parse_from(["", "--tempo.fee-token", "1"]).unwrap(); assert_eq!( - opts_with_id.fee_token, + opts_with_id.common.fee_token, Some(address!("0x20C0000000000000000000000000000000000001")), ); } + + #[test] + fn parse_sponsor_signer() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert_eq!(opts.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + assert!(opts.sponsor_sig.is_none()); + assert!(opts.is_tempo()); + assert!(opts.has_sponsor_submission()); + } + + #[test] + fn sponsor_signer_requires_sponsor() { + assert!( + TempoOpts::try_parse_from(["", "--tempo.sponsor-signer", "env://SPONSOR"]).is_err() + ); + } + + #[test] + fn parse_sponsor_signature_alias() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signature", + "0x0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert!(opts.sponsor_sig.is_some()); + } + + #[test] + fn print_sponsor_hash_conflicts_with_sponsor_submission() { + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.print-sponsor-hash", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + ]) + .is_err() + ); + } } diff --git a/crates/cli/src/utils/tempo.rs b/crates/cli/src/utils/tempo.rs index 647f52d316a6c..4b5715b9ebe08 100644 --- a/crates/cli/src/utils/tempo.rs +++ b/crates/cli/src/utils/tempo.rs @@ -1,8 +1,44 @@ -use std::str::FromStr; +//! Tempo utilities: fee token parsing and named nonce lanes (2D nonces). +//! +//! A "lane" is a friendly alias for a Tempo `nonce_key` (a [`U256`]). Lanes are defined in a +//! shared TOML file (default `tempo.lanes.toml` at the project root) so a team can reserve +//! independent sequential nonce streams for parallel scripts without coordinating on raw +//! `U256` selectors. +//! +//! Example `tempo.lanes.toml`: +//! +//! ```toml +//! deploy = 1 +//! ops = 2 +//! payments = 3 +//! ``` +//! +//! ```bash +//! cast erc20 transfer ... --tempo.lane payments +//! ``` -use alloy_primitives::Address; +use crate::opts::TempoOpts; +use alloy_primitives::{Address, U256}; +use eyre::{Result, eyre}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + str::FromStr, +}; use tempo_primitives::TempoAddressExt; +/// Default name of the lanes file at the project root. +pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml"; + +/// Result of resolving a `--tempo.lane ` argument against a lanes file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedLane { + /// The lane name as provided on the CLI. + pub name: String, + /// The `nonce_key` the lane resolved to. + pub nonce_key: U256, +} + /// Parses a fee token address. pub fn parse_fee_token_address(address_or_id: &str) -> eyre::Result
{ Address::from_str(address_or_id).or_else(|_| Ok(token_id_to_address(address_or_id.parse()?))) @@ -14,3 +50,156 @@ fn token_id_to_address(token_id: u64) -> Address { address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes()); Address::from(address_bytes) } + +/// Loads a TOML lanes file from `path`. +/// +/// Each top-level key is a lane name, and the value is the `nonce_key` (an integer or a +/// decimal/hex string parsed as [`U256`]). +pub fn load_lanes(path: &Path) -> Result> { + let contents = std::fs::read_to_string(path) + .map_err(|e| eyre!("failed to read tempo lanes file {}: {}", path.display(), e))?; + parse_lanes(&contents) + .map_err(|e| eyre!("failed to parse tempo lanes file {}: {}", path.display(), e)) +} + +fn parse_lanes(contents: &str) -> Result> { + let raw: BTreeMap = toml::from_str(contents)?; + let mut out = BTreeMap::new(); + for (name, value) in raw { + let nonce_key = match value { + toml::Value::Integer(n) => { + if n < 0 { + return Err(eyre!("invalid nonce_key for lane '{name}': must be non-negative")); + } + U256::from(n as u64) + } + toml::Value::String(s) => U256::from_str(s.trim()) + .map_err(|e| eyre!("invalid nonce_key for lane '{name}': {e}"))?, + other => { + return Err(eyre!( + "invalid nonce_key for lane '{name}': expected integer or string, got {}", + other.type_str(), + )); + } + }; + out.insert(name, nonce_key); + } + Ok(out) +} + +/// Resolves `opts.lane` against a lanes file and writes the resulting `nonce_key` to +/// `opts.nonce_key`. Returns the resolved lane (or `None` if no `--tempo.lane` was set). +/// +/// `root` is the project root used to locate the default lanes file +/// (`/tempo.lanes.toml`) when `--tempo.lanes-file` was not provided. +pub fn resolve_lane(opts: &mut TempoOpts, root: &Path) -> Result> { + let Some(lane_name) = opts.lane.clone() else { return Ok(None) }; + + let path: PathBuf = opts.lanes_file.clone().unwrap_or_else(|| root.join(DEFAULT_LANES_FILE)); + + if !path.exists() { + return Err(eyre!( + "tempo lanes file not found at {}\n\ + create it with `name = ` entries, e.g.:\n \ + deploy = 1\n \ + ops = 2\n \ + payments = 3", + path.display(), + )); + } + + let lanes = load_lanes(&path)?; + + let nonce_key = lanes.get(&lane_name).copied().ok_or_else(|| { + let mut known: Vec<&str> = lanes.keys().map(String::as_str).collect(); + known.sort_unstable(); + eyre!( + "lane '{lane_name}' not found in {} (known lanes: {})", + path.display(), + if known.is_empty() { "".to_string() } else { known.join(", ") }, + ) + })?; + + opts.nonce_key = Some(nonce_key); + Ok(Some(ResolvedLane { name: lane_name, nonce_key })) +} + +/// Prints `lane: (nonce_key=, nonce=)` to stderr (so it doesn't pollute +/// stdout for commands like `cast mktx` whose stdout is meant to be piped), giving +/// visibility into which 2D nonce lane was used. +pub fn maybe_print_resolved_lane(resolved: Option<&ResolvedLane>, nonce: u64) -> Result<()> { + if let Some(lane) = resolved { + sh_eprintln!("lane: {} (nonce_key={}, nonce={})", lane.name, lane.nonce_key, nonce)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_int_and_string_lane_values() { + let toml = r#" +deploy = 1 +ops = 2 +payments = "3" +big = "115792089237316195423570985008687907853269984665640564039457584007913129639935" +"#; + let lanes = parse_lanes(toml).unwrap(); + assert_eq!(lanes.get("deploy"), Some(&U256::from(1u64))); + assert_eq!(lanes.get("ops"), Some(&U256::from(2u64))); + assert_eq!(lanes.get("payments"), Some(&U256::from(3u64))); + assert_eq!(lanes.get("big"), Some(&U256::MAX)); + } + + #[test] + fn parse_lanes_rejects_invalid_string() { + let toml = "broken = \"not-a-number\""; + let err = parse_lanes(toml).unwrap_err(); + assert!(err.to_string().contains("invalid nonce_key for lane 'broken'")); + } + + #[test] + fn resolve_lane_sets_nonce_key_and_returns_resolved() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 7\npayments = 42\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let resolved = resolve_lane(&mut opts, dir.path()).unwrap().unwrap(); + assert_eq!(resolved.name, "payments"); + assert_eq!(resolved.nonce_key, U256::from(42u64)); + assert_eq!(opts.nonce_key, Some(U256::from(42u64))); + } + + #[test] + fn resolve_lane_returns_none_when_no_lane() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts::default(); + let resolved = resolve_lane(&mut opts, dir.path()).unwrap(); + assert!(resolved.is_none()); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn resolve_lane_errors_when_file_missing() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts { lane: Some("deploy".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + assert!(err.to_string().contains("tempo lanes file not found")); + } + + #[test] + fn resolve_lane_errors_when_lane_unknown() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 1\nops = 2\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("lane 'payments' not found")); + assert!(msg.contains("deploy, ops")); + } +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 7fd94c07242e8..a66a0fef2fe0a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -34,7 +34,7 @@ alloy-signer.workspace = true alloy-pubsub.workspace = true alloy-rpc-client.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "engine"] } -alloy-rpc-types-engine = { workspace = true, features = ["jwt"] } +alloy-rpc-types-engine = { workspace = true, features = ["jwt-aws-lc-rs"] } alloy-sol-types.workspace = true alloy-transport-ipc.workspace = true alloy-transport-ws.workspace = true @@ -43,8 +43,8 @@ alloy-transport.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true -op-alloy-network.workspace = true -op-alloy-rpc-types.workspace = true +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } revm.workspace = true @@ -86,6 +86,10 @@ mpp.workspace = true foundry-wallets = { workspace = true, features = ["browser", "tempo"] } tokio-tungstenite.workspace = true futures.workspace = true +alloy-signer-local.workspace = true +base64.workspace = true +sha2 = "0.10" +tempfile.workspace = true [build-dependencies] chrono.workspace = true @@ -95,4 +99,12 @@ vergen = { workspace = true, features = ["build", "emit_and_set"] } foundry-evm-hardforks.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } axum = { workspace = true } -tempfile.workspace = true +k256 = { workspace = true } + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "foundry-common-fmt/optimism", +] diff --git a/crates/common/build.rs b/crates/common/build.rs index d89e23be850f4..9afa01b5757ef 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -15,16 +15,13 @@ fn main() -> Result<(), Box> { let sha_short = &sha[..10]; let tag_name = try_env_var("TAG_NAME").unwrap_or_else(|| String::from("dev")); - let is_nightly = tag_name.contains("nightly"); - let version_suffix = if is_nightly { "nightly" } else { &tag_name }; + let version = release_version(&env_var("CARGO_PKG_VERSION"), &tag_name); + let is_nightly = tag_name.starts_with("nightly"); if is_nightly { println!("cargo:rustc-env=FOUNDRY_IS_NIGHTLY_VERSION=true"); } - let pkg_version = env_var("CARGO_PKG_VERSION"); - let version = format!("{pkg_version}-{version_suffix}"); - // `PROFILE` captures only release or debug. Get the actual name from the out directory. let out_dir = PathBuf::from(env_var("OUT_DIR")); let profile = out_dir.components().rev().nth(3).unwrap().as_os_str().to_str().unwrap(); @@ -87,6 +84,19 @@ fn env_var(name: &str) -> String { try_env_var(name).unwrap() } +fn release_version(pkg_version: &str, tag_name: &str) -> String { + if let Some(version) = tag_name.strip_prefix('v') { + return version.to_owned(); + } + + // Normalize `nightly-` to `nightly` so tarball and Docker nightly + // artifacts produce the same version string. The commit identifier is + // already included in the SemVer build metadata (after `+`). + let normalized = if tag_name.starts_with("nightly-") { "nightly" } else { tag_name }; + + format!("{pkg_version}-{normalized}") +} + fn try_env_var(name: &str) -> Option { println!("cargo:rerun-if-env-changed={name}"); std::env::var(name).ok() diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 2c8e16bccdcc6..179c71048da5b 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -20,10 +20,10 @@ eyre.workspace = true # ui alloy-consensus.workspace = true -op-alloy-consensus.workspace = true +op-alloy-consensus = { workspace = true, optional = true } alloy-network.workspace = true alloy-rpc-types = { workspace = true, features = ["eth"] } -op-alloy-rpc-types.workspace = true +op-alloy-rpc-types = { workspace = true, optional = true } alloy-serde.workspace = true serde.workspace = true serde_json.workspace = true @@ -38,3 +38,7 @@ tempo-alloy.workspace = true [dev-dependencies] foundry-macros.workspace = true similar-asserts.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:op-alloy-consensus", "dep:op-alloy-rpc-types"] diff --git a/crates/common/fmt/src/ui.rs b/crates/common/fmt/src/ui.rs index e883810dcda34..2087a85236154 100644 --- a/crates/common/fmt/src/ui.rs +++ b/crates/common/fmt/src/ui.rs @@ -18,6 +18,7 @@ use alloy_rpc_types::{ AccessListItem, Block, BlockTransactions, Header, Log, Transaction, TransactionReceipt, }; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{OpTxEnvelope, TxDeposit, TxPostExec}; use revm::context_interface::transaction::SignedAuthorization; use serde::Deserialize; @@ -448,6 +449,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxDeposit { fn pretty(&self) -> String { format!( @@ -472,6 +474,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxPostExec { fn pretty(&self) -> String { format!( @@ -606,6 +609,7 @@ type {:#x} } } +#[cfg(feature = "optimism")] impl UIfmt for OpTxEnvelope { fn pretty(&self) -> String { match self { @@ -651,6 +655,7 @@ effectiveGasPrice {} } } +#[cfg(feature = "optimism")] impl UIfmt for op_alloy_rpc_types::Transaction { fn pretty(&self) -> String { format!( @@ -786,6 +791,7 @@ impl UIfmtSignatureExt for AnyTxEnvelope { } } +#[cfg(feature = "optimism")] impl UIfmtSignatureExt for OpTxEnvelope { fn signature_pretty(&self) -> Option<(String, String, String)> { self.signature().map(|sig| { @@ -1135,6 +1141,7 @@ mod tests { assert_eq!(b.pretty(), b32.pretty()); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx() { let s = r#" @@ -1186,6 +1193,7 @@ yParity 1 ); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx_through_any() { let s = r#" diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 895b16b3b4532..95c7d4083f34e 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -383,16 +383,14 @@ impl ContractsByArtifact { &self, id: &str, ) -> Result>> { - let contracts = self - .iter() - .filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id) - .collect::>(); - - if contracts.len() > 1 { + let mut iter = + self.iter().filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id); + let first = iter.next(); + if first.is_some() && iter.next().is_some() { eyre::bail!("{id} has more than one implementation."); } - Ok(contracts.first().copied()) + Ok(first) } /// Finds abi by name or source path @@ -411,7 +409,7 @@ impl ContractsByArtifact { let mut funcs = BTreeMap::new(); let mut events = BTreeMap::new(); let mut errors_abi = JsonAbi::new(); - for (_name, contract) in self.iter() { + for contract in self.values() { for func in contract.abi.functions() { funcs.insert(func.selector(), func.clone()); } diff --git a/crates/common/src/provider/mpp/keys.rs b/crates/common/src/provider/mpp/keys.rs index 65640c48ab841..fa0fc80ed3d03 100644 --- a/crates/common/src/provider/mpp/keys.rs +++ b/crates/common/src/provider/mpp/keys.rs @@ -7,6 +7,7 @@ use crate::tempo::{TEMPO_PRIVATE_KEY_ENV, WalletType, read_tempo_keys_file}; use alloy_primitives::Address; +use std::env; use tracing::debug; /// Options for MPP key discovery filtering. @@ -55,7 +56,7 @@ pub fn discover_mpp_key() -> Option { /// target chain and the required currency. pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 1. Check TEMPO_PRIVATE_KEY env var (no keychain metadata available) - if let Ok(key) = std::env::var(TEMPO_PRIVATE_KEY_ENV) { + if let Ok(key) = env::var(TEMPO_PRIVATE_KEY_ENV) { let key = key.trim().to_string(); if !key.is_empty() { debug!("using MPP key from {TEMPO_PRIVATE_KEY_ENV} env var"); @@ -73,11 +74,17 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 2. Read $TEMPO_HOME/wallet/keys.toml (default: ~/.tempo/wallet/keys.toml) let keys_file = read_tempo_keys_file()?; + // `expiry == 0` means "no expiry" on the wire. + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // Pick primary key using the same deterministic order as // `Keystore::primary_key()` in tempo-common: // passkey > first entry with inline key > first entry // Only entries with a usable inline key can provide a signing key. - // Filter by chain_id and currency when provided. + // Filter by chain_id, currency, and freshness when provided. let candidates: Vec<_> = keys_file .keys .iter() @@ -86,6 +93,7 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { opts.currency .is_none_or(|cur| k.limits.is_empty() || k.limits.iter().any(|l| l.currency == cur)) }) + .filter(|k| k.expiry.is_none_or(|e| e == 0 || e > now)) .collect(); let primary = candidates @@ -135,6 +143,7 @@ mod tests { #[test] fn discover_from_tempo_home_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( r#" @@ -160,6 +169,7 @@ chain_id = 4217 #[test] fn discover_env_var_takes_priority_over_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let file_key = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; let env_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( @@ -187,6 +197,7 @@ key = "{file_key}" #[test] fn discover_returns_none_when_no_keys() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let (dir, _) = setup_keys_toml(""); unsafe { @@ -202,6 +213,7 @@ key = "{file_key}" #[test] fn discover_skips_entries_without_inline_key() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let toml_content = format!( r#" @@ -344,6 +356,7 @@ key = "0xthe_key" #[test] fn discover_filters_by_chain_id() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let mainnet_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let testnet_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; let toml_content = format!( @@ -416,6 +429,62 @@ chain_id = 4217 unsafe { std::env::remove_var("TEMPO_HOME") }; } + #[test] + fn discover_filters_expired_entries() { + // Expired entries must not be selected, so the next 402 re-triggers + // the device-code flow instead of returning a stale key. + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let expired_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let fresh_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + let toml_content = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 + +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000002" +key = "{fresh_key}" +chain_id = 4217 +expiry = 0 +"# + ); + let (dir, _) = setup_keys_toml(&toml_content); + unsafe { + std::env::set_var("TEMPO_HOME", dir.path()); + std::env::remove_var("TEMPO_PRIVATE_KEY"); + } + + // Even though the expired entry comes first, discovery skips it. + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert_eq!(config.as_ref().unwrap().key, fresh_key); + + // With only the expired entry present, discovery returns None so the + // 402 path can run `ensure_access_key` again. + let only_expired = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 +"# + ); + let (dir2, _) = setup_keys_toml(&only_expired); + unsafe { std::env::set_var("TEMPO_HOME", dir2.path()) }; + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert!(config.is_none(), "expired-only keys.toml must not yield a usable key"); + + unsafe { std::env::remove_var("TEMPO_HOME") }; + } + #[test] fn parse_keys_toml_unknown_fields_ignored() { let toml_str = r#" diff --git a/crates/common/src/provider/mpp/session.rs b/crates/common/src/provider/mpp/session.rs index 334166b844613..c3e87f8cf42b5 100644 --- a/crates/common/src/provider/mpp/session.rs +++ b/crates/common/src/provider/mpp/session.rs @@ -175,6 +175,16 @@ impl SessionProvider { self } + /// Address that funds payments for this provider. + pub fn funding_wallet_address(&self) -> Address { + self.signing_mode.from_address(self.signer.address()) + } + + /// Chain ID from the selected wallet key, when known. + pub const fn key_chain_id(&self) -> Option { + self.key_chain_id + } + /// Set the chain ID and currencies from the key entry used to initialize /// this provider. Used to reject challenges for incompatible chains/currencies. /// When `chain_id` is `None` (e.g. env var key), chain filtering is skipped. diff --git a/crates/common/src/provider/mpp/transport.rs b/crates/common/src/provider/mpp/transport.rs index 67354dc2bd60d..9e3b16dedd59e 100644 --- a/crates/common/src/provider/mpp/transport.rs +++ b/crates/common/src/provider/mpp/transport.rs @@ -4,6 +4,7 @@ //! handling via the MPP protocol. When the RPC endpoint returns a 402 response, //! this transport automatically pays the challenge and retries the request. +use alloy_chains::Chain; use alloy_json_rpc::{RequestPacket, ResponsePacket}; use alloy_transport::{TransportError, TransportErrorKind, TransportFut, TransportResult}; use mpp::{ @@ -16,12 +17,17 @@ use mpp::{ use reqwest::{StatusCode, header::HeaderMap}; use std::{ collections::HashMap, - fmt, - sync::{Mutex, OnceLock}, + env, fmt, io, + io::IsTerminal, + process::{Command, Stdio}, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicBool, Ordering}, + }, task, time::Duration, }; -use tokio::sync::OwnedMutexGuard; +use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard}; use tower::Service; use tracing::{Instrument, debug, debug_span, trace}; use url::Url; @@ -39,7 +45,27 @@ const MPP_RETRY_TIMEOUT: Duration = Duration::from_secs(120); /// Resolve the deposit amount from `MPP_DEPOSIT` env var or the default. fn default_deposit() -> u128 { - std::env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) + env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct FundingContext { + wallet_address: Option, + token: Option, + chain_id: Option, +} + +impl FundingContext { + fn token_line(&self) -> String { + self.token + .as_ref() + .map(|token| format!("Requested payment token: {token}\n\n")) + .unwrap_or_default() + } + + fn network(&self) -> Option { + self.chain_id.filter(|chain| chain.is_tempo()).map(|chain| chain.to_string()) + } } fn format_http_diagnostics(headers: &HeaderMap) -> String { @@ -60,12 +86,173 @@ fn format_http_diagnostics(headers: &HeaderMap) -> String { } } +fn tempo_wallet_fund_help(ctx: &FundingContext) -> String { + let mut command = "tempo wallet fund".to_string(); + if let Some(address) = ctx.wallet_address { + command.push_str(&format!(" --address {address}")); + } + if let Some(network) = ctx.network() { + command.push_str(&format!(" --network {network}")); + } + + let mut no_browser = command.clone(); + no_browser.push_str(" --no-browser"); + + format!( + "\n\nTempo wallet payment could not be funded for this paid RPC request.\n\n{}\ + Fund the wallet, then rerun the command:\n {command}\n\n\ + If this CLI is running on a remote or headless host, use:\n {no_browser}", + ctx.token_line() + ) +} + +/// Decide whether the interactive `tempo wallet fund` flow may be launched. +/// +/// Policy (library-safe): +/// - never run inside CI +/// - never run unless both stdin and stderr are real terminals +/// - `FOUNDRY_MPP_NO_AUTO_FUND` is honored as an opt-out; it must not bypass CI/TTY guards in +/// shared transport code that may be embedded inside long-running RPC daemons. +fn interactive_tempo_fund_allowed( + no_auto_fund: Option<&str>, + in_ci: bool, + stdin_is_terminal: bool, + stderr_is_terminal: bool, +) -> bool { + if no_auto_fund.is_some_and(|v| { + !(v == "0" || v.eq_ignore_ascii_case("false") || v.eq_ignore_ascii_case("off")) + }) { + return false; + } + + if in_ci { + return false; + } + + stdin_is_terminal && stderr_is_terminal +} + +fn can_run_interactive_tempo_fund() -> bool { + if cfg!(test) { + return false; + } + + interactive_tempo_fund_allowed( + std::env::var("FOUNDRY_MPP_NO_AUTO_FUND").ok().as_deref(), + std::env::var_os("CI").is_some(), + std::io::stdin().is_terminal(), + std::io::stderr().is_terminal(), + ) +} + +fn tempo_bin() -> String { + std::env::var("TEMPO_BIN").unwrap_or_else(|_| "tempo".to_string()) +} + +async fn run_interactive_tempo_fund(ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + + let tempo = tempo_bin(); + let mut args = vec!["wallet".to_string(), "fund".to_string()]; + if let Some(address) = ctx.wallet_address { + args.push("--address".to_string()); + args.push(address.to_string()); + } + if let Some(network) = ctx.network() { + args.push("--network".to_string()); + args.push(network); + } + + tracing::warn!( + token = ?ctx.token, + chain_id = ?ctx.chain_id, + "MPP payment could not be funded; opening `tempo wallet fund`" + ); + + let status = tokio::task::spawn_blocking(move || { + Command::new(tempo) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + }) + .await + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to join tempo wallet fund process: {e}" + ))) + })? + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to run `tempo wallet fund`: {e}{}", + tempo_wallet_fund_help(ctx) + ))) + })?; + + if status.success() { + Ok(true) + } else { + Err(TransportErrorKind::custom(std::io::Error::other(format!( + "`tempo wallet fund` exited with status {status}{}", + tempo_wallet_fund_help(ctx) + )))) + } +} + +/// Single-attempt guard around [`run_interactive_tempo_fund`]. +/// +/// Ensures that for one logical request we launch `tempo wallet fund` at most +/// once, regardless of how many recovery paths (`do_request`, `pay_and_retry`, +/// `handle_response_or_retry_after_fund`, ...) attempt it. +async fn maybe_auto_fund(used: &AtomicBool, ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + if used.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { + return Ok(false); + } + run_interactive_tempo_fund(ctx).await +} + +/// Returns true iff a 402 response carries a structured insufficient-balance +/// problem (RFC 9457 `PaymentErrorDetails`). +/// +/// We deliberately do **not** match on free-text body content or on generic +/// `verification-failed` problem types, as those have many non-funding causes +/// (bad signature, replay, expired challenge, clock skew, key provisioning, +/// malformed auth, ...). +fn should_suggest_tempo_fund(status: StatusCode, body: &[u8]) -> bool { + if status != StatusCode::PAYMENT_REQUIRED { + return false; + } + let Ok(problem) = serde_json::from_slice::(body) else { + return false; + }; + problem.problem_type.ends_with("/insufficient-balance") +} + +fn format_mpp_payment_failure( + error: impl fmt::Display, + ctx: &FundingContext, + suggest_fund: bool, +) -> String { + let message = error.to_string(); + if suggest_fund { + format!("MPP payment failed: {message}{}", tempo_wallet_fund_help(ctx)) + } else { + format!("MPP payment failed: {message}") + } +} + /// Process-wide payment serialization locks, keyed by origin URL. /// /// Created eagerly so the lock exists before the first provider init, /// preventing concurrent first-402 races. -static GLOBAL_PAY_LOCKS: OnceLock>>>> = - OnceLock::new(); +static GLOBAL_PAY_LOCKS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); /// Production transport: lazily discovers MPP keys from the Tempo wallet on /// first 402 response. @@ -75,24 +262,21 @@ pub type LazyMppHttpTransport = MppHttpTransport; /// Tempo wallet configuration on first use. #[derive(Clone, Debug)] pub struct LazySessionProvider { - inner: std::sync::Arc>>, + inner: Arc>>, /// Eagerly-created, process-wide payment serialization lock for this origin. - pay_lock: std::sync::Arc>, + pay_lock: Arc>, origin: String, } impl LazySessionProvider { pub(super) fn new(origin: String) -> Self { - let pay_lock = { - let global = GLOBAL_PAY_LOCKS.get_or_init(|| Mutex::new(HashMap::new())); - global - .lock() - .unwrap() - .entry(origin.clone()) - .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(()))) - .clone() - }; - Self { inner: std::sync::Arc::new(Mutex::new(None)), pay_lock, origin } + let pay_lock = GLOBAL_PAY_LOCKS + .lock() + .unwrap() + .entry(origin.clone()) + .or_insert_with(|| Arc::new(AsyncMutex::new(()))) + .clone(); + Self { inner: Arc::new(Mutex::new(None)), pay_lock, origin } } fn set_key_provisioned(&self, provisioned: bool) { @@ -125,6 +309,14 @@ impl LazySessionProvider { } } + /// Drop the cached `SessionProvider` so the next `get_or_init` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry, so a long-lived transport doesn't keep paying with + /// the superseded key. + fn invalidate(&self) { + *self.inner.lock().unwrap() = None; + } + pub(super) fn get_or_init(&self, opts: DiscoverOptions) -> TransportResult { let mut guard = self.inner.lock().unwrap(); if let Some(ref provider) = *guard { @@ -132,18 +324,20 @@ impl LazySessionProvider { } let config = discover_mpp_config(opts).ok_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "RPC endpoint returned HTTP 402 Payment Required. \ This endpoint requires payment via the Machine Payments Protocol (MPP).\n\n\ - To configure MPP, install the Tempo wallet CLI and create a key:\n\ - \n curl -sSL https://tempo.xyz/install.sh | bash\ - \n tempo wallet login\ + Authorize an access key against your Tempo wallet:\n\ + \n cast tempo login\ + \n\nIn headless environments, pass `--no-browser` to print the authorization \ + URL instead of launching a browser:\n\ + \n cast tempo login --no-browser\ \n\nSee https://docs.tempo.xyz for more information.", )) })?; let signer: mpp::PrivateKeySigner = config.key.parse().map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("invalid MPP key: {e}"))) + TransportErrorKind::custom(io::Error::other(format!("invalid MPP key: {e}"))) })?; let signing_mode = if let Some(wallet) = config.wallet_address { @@ -152,7 +346,7 @@ impl LazySessionProvider { .as_ref() .map(|hex_str| { crate::tempo::decode_key_authorization(hex_str).map(Box::new).map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "invalid MPP key_authorization: {e}" ))) }) @@ -223,6 +417,17 @@ where P::Provider: Send + Sync + 'static, { async fn do_request(self, req: RequestPacket) -> TransportResult { + // Per-request guard: launch `tempo wallet fund` at most once for one + // logical request, regardless of how many recovery paths attempt it. + let auto_fund_used = AtomicBool::new(false); + self.do_request_inner(req, &auto_fund_used).await + } + + async fn do_request_inner( + self, + req: RequestPacket, + auto_fund_used: &AtomicBool, + ) -> TransportResult { let body = serde_json::to_vec(&req).map_err(TransportErrorKind::custom)?; let headers = req.headers(); @@ -246,15 +451,53 @@ where // held until the retry response is fully handled. let _pay_guard = self.provider.lock_pay().await; - let (resolved, challenge) = Self::select_challenge(&resp, &self.provider)?; + // No local key for any offered challenge → run device-code flow, + // invalidate the cached provider, and fetch a fresh 402 (the original + // may have expired during the browser/passkey flow). + let (resolved, challenge) = + if let Some(chain_id) = tempo_chain_needing_auth(&self.url, &resp) { + debug!(chain_id, "launching wallet.tempo authorization"); + let cfg = crate::tempo::EnsureAccessKeyConfig::from_env(chain_id); + crate::tempo::ensure_access_key(cfg).await.map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "tempo access key authorization failed: {e}" + ))) + })?; + self.provider.invalidate_cached_provider(); + self.fetch_fresh_challenge(&headers, &body).await? + } else { + Self::select_challenge(&resp, &self.provider)? + }; + let funding_ctx = self.provider.funding_context(&challenge); debug!(id = %challenge.id, method = %challenge.method, intent = %challenge.intent, "received MPP 402 challenge, paying"); - let credential = resolved.pay(&challenge).await.map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let credential = match resolved.pay(&challenge).await { + Ok(credential) => credential, + Err(e) => { + // Only the explicit `InsufficientBalance` variant is treated as + // a fundable error. Any other failure must surface unchanged so + // we don't mask payment/protocol issues behind a fund prompt. + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + resolved.pay(&challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { + self.provider.rollback_pending(); TransportErrorKind::custom(std::io::Error::other(format!( "failed to format MPP credential: {e}" ))) @@ -286,9 +529,20 @@ where self.provider.commit_topup_and_track_voucher(); let resolved = self.provider.resolve()?; - let voucher_resp = self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(voucher_resp).await; + let voucher_resp = + self.pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used).await?; + + // Route the voucher response through the funding-aware handler so + // a final 402 here also gets the fund retry / contextual help. + let result = self + .handle_response_or_retry_after_fund( + voucher_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -304,7 +558,7 @@ where self.provider.rollback_pending(); self.provider.clear_channels(); - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "MPP channel not found on server (410 Gone). \ The server may have restarted or the channel was closed externally.\n\ Local channel state has been cleared. Re-run to open a new channel.", @@ -333,10 +587,19 @@ where debug!("MPP voucher stale, retrying with fresh voucher"); let resolved = self.provider.resolve()?; if resolved.supports(challenge.method.as_str(), challenge.intent.as_str()) { - let final_resp = - self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.flush_pending(); } else { @@ -372,10 +635,19 @@ where let (resolved, fresh_challenge) = self.fetch_fresh_challenge(&headers, &body).await?; - let final_resp = - self.pay_and_retry(&fresh_challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -386,9 +658,40 @@ where } self.provider.rollback_pending(); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) + && maybe_auto_fund(auto_fund_used, &funding_ctx).await? + { + let (resolved, fresh_challenge) = + self.fetch_fresh_challenge(&headers, &body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; + if result.is_ok() { + self.provider.set_key_provisioned(true); + self.provider.flush_pending(); + } else { + self.provider.rollback_pending(); + } + return result; + } + + let mut error_text = format!("{retry_text}{diagnostics}"); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) { + error_text.push_str(&tempo_wallet_fund_help(&funding_ctx)); + } return Err(TransportErrorKind::http_error( StatusCode::PAYMENT_REQUIRED.as_u16(), - format!("{retry_text}{diagnostics}"), + error_text, )); } @@ -409,15 +712,32 @@ where provider: &P::Provider, headers: &reqwest::header::HeaderMap, body: &[u8], + auto_fund_used: &AtomicBool, ) -> TransportResult { - let credential = provider.pay(challenge).await.map_err(|e| { - self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let funding_ctx = self.provider.funding_context(challenge); + let credential = match provider.pay(challenge).await { + Ok(credential) => credential, + Err(e) => { + self.provider.rollback_pending(); + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + provider.pay(challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "failed to format MPP credential: {e}" ))) })?; @@ -437,6 +757,41 @@ where }) } + async fn handle_response_or_retry_after_fund( + &self, + resp: reqwest::Response, + headers: &reqwest::header::HeaderMap, + body: &[u8], + funding_ctx: &FundingContext, + auto_fund_used: &AtomicBool, + ) -> TransportResult { + if resp.status() != StatusCode::PAYMENT_REQUIRED { + return Self::handle_response_with_funding(resp, Some(funding_ctx)).await; + } + + let diagnostics = format_http_diagnostics(resp.headers()); + let status = resp.status(); + let resp_body = resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if should_suggest_tempo_fund(status, &resp_body) + && maybe_auto_fund(auto_fund_used, funding_ctx).await? + { + self.provider.rollback_pending(); + + let (resolved, fresh_challenge) = self.fetch_fresh_challenge(headers, body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, headers, body, auto_fund_used) + .await?; + return Self::handle_response_with_funding(final_resp, Some(funding_ctx)).await; + } + + let mut error_text = format!("{}{diagnostics}", String::from_utf8_lossy(&resp_body)); + if should_suggest_tempo_fund(status, &resp_body) { + error_text.push_str(&tempo_wallet_fund_help(funding_ctx)); + } + Err(TransportErrorKind::http_error(status.as_u16(), error_text)) + } + /// Fetch a fresh 402 challenge from the server (unauthenticated request). /// /// Returns `Ok(Some((provider, challenge)))` if the server returns a 402 @@ -462,7 +817,7 @@ where // Non-402 → return whatever the server sent (could be success or error). let result = Self::handle_response(fresh_resp).await; return Err(result.err().unwrap_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "unexpected success on unauthenticated fresh probe", )) })); @@ -477,25 +832,14 @@ where resp: &reqwest::Response, provider: &P, ) -> TransportResult<(P::Provider, mpp::protocol::core::PaymentChallenge)> { - let www_auth_values: Vec<&str> = resp - .headers() - .get_all(WWW_AUTHENTICATE_HEADER) - .iter() - .filter_map(|v| v.to_str().ok()) - .collect(); - - if www_auth_values.is_empty() { - return Err(TransportErrorKind::custom(std::io::Error::other(format!( + let challenges = parse_challenges(resp); + if challenges.is_empty() && resp.headers().get(WWW_AUTHENTICATE_HEADER).is_none() { + return Err(TransportErrorKind::custom(io::Error::other(format!( "402 response missing WWW-Authenticate header{}", format_http_diagnostics(resp.headers()) )))); } - let challenges: Vec<_> = parse_www_authenticate_all(www_auth_values) - .into_iter() - .filter_map(|r| r.ok()) - .collect(); - let mut last_resolve_err: Option = None; let resolved_pair = challenges.iter().find_map(|c| { let (chain_id, currency) = extract_challenge_chain_and_currency(c); @@ -515,7 +859,7 @@ where } let offered: Vec<_> = challenges.iter().map(|c| format!("{}.{}", c.method, c.intent)).collect(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "no supported MPP challenge; server offered [{}]", offered.join(", "), ))) @@ -523,6 +867,17 @@ where } async fn handle_response(resp: reqwest::Response) -> TransportResult { + Self::handle_response_with_funding(resp, None).await + } + + /// Like [`Self::handle_response`] but, when an unsuccessful 402 looks like a + /// fundable error, appends actionable `tempo wallet fund` help that uses + /// the per-request `FundingContext` (so the suggested command includes + /// `--address` and `--network` when known). + async fn handle_response_with_funding( + resp: reqwest::Response, + funding_ctx: Option<&FundingContext>, + ) -> TransportResult { let status = resp.status(); debug!(%status, "received response from MPP transport"); let diagnostics = format_http_diagnostics(resp.headers()); @@ -536,10 +891,19 @@ where } if !status.is_success() { - return Err(TransportErrorKind::http_error( - status.as_u16(), - format!("{}{diagnostics}", String::from_utf8_lossy(&body)), - )); + let mut body_text = format!("{}{diagnostics}", String::from_utf8_lossy(&body)); + if should_suggest_tempo_fund(status, &body) { + let default_ctx; + let ctx = match funding_ctx { + Some(c) => c, + None => { + default_ctx = FundingContext::default(); + &default_ctx + } + }; + body_text.push_str(&tempo_wallet_fund_help(ctx)); + } + return Err(TransportErrorKind::http_error(status.as_u16(), body_text)); } serde_json::from_slice(&body) @@ -547,6 +911,57 @@ where } } +/// Returns `Some(chain_id)` when a 402 response should trigger the +/// `wallet.tempo.xyz` device-code authorization flow. +/// +/// Conditions: known Tempo endpoint, interactive (TTY, not `CI`), and no +/// offered Tempo challenge resolves against a local key on `(chain, currency)`. +/// The picked chain matches the first unresolved challenge — same iteration +/// order [`MppHttpTransport::select_challenge`] uses. +fn tempo_chain_needing_auth(url: &Url, resp: &reqwest::Response) -> Option { + if !io::stderr().is_terminal() || env::var_os("CI").is_some() { + return None; + } + pick_chain_needing_auth(url, &parse_challenges(resp)) +} + +/// Extract all parseable MPP challenges from a 402 response's `WWW-Authenticate` headers. +fn parse_challenges(resp: &reqwest::Response) -> Vec { + let values: Vec<&str> = resp + .headers() + .get_all(WWW_AUTHENTICATE_HEADER) + .iter() + .filter_map(|v| v.to_str().ok()) + .collect(); + parse_www_authenticate_all(values).into_iter().filter_map(|r| r.ok()).collect() +} + +/// Inner logic of [`tempo_chain_needing_auth`], factored out for testing. +fn pick_chain_needing_auth( + url: &Url, + challenges: &[mpp::protocol::core::PaymentChallenge], +) -> Option { + if !crate::tempo::is_known_tempo_endpoint(url) { + return None; + } + + let tempo_challenges: Vec<_> = + challenges.iter().filter(|c| c.method.as_str() == "tempo").collect(); + + // If any challenge already resolves with a local key, no auth needed. + let any_resolvable = tempo_challenges.iter().any(|c| { + let (chain_id, currency) = extract_challenge_chain_and_currency(c); + let currency = currency.and_then(|s| s.parse().ok()); + super::keys::discover_mpp_config(super::keys::DiscoverOptions { chain_id, currency }) + .is_some() + }); + if any_resolvable { + return None; + } + + tempo_challenges.iter().find_map(|c| extract_challenge_chain_and_currency(c).0) +} + /// Extract `(chainId, currency)` from a parsed MPP challenge. pub(super) fn extract_challenge_chain_and_currency( c: &mpp::protocol::core::PaymentChallenge, @@ -576,10 +991,28 @@ pub(crate) trait ResolveProvider { fn flush_pending(&self) {} fn rollback_pending(&self) {} fn commit_topup_and_track_voucher(&self) {} + /// Drop any cached payment provider so the next `resolve_for` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry. + fn invalidate_cached_provider(&self) {} + fn funding_wallet_address(&self) -> Option { + None + } + fn funding_chain_id(&self) -> Option { + None + } + fn funding_context(&self, challenge: &mpp::protocol::core::PaymentChallenge) -> FundingContext { + let (challenge_chain_id, token) = extract_challenge_chain_and_currency(challenge); + FundingContext { + wallet_address: self.funding_wallet_address(), + token, + chain_id: challenge_chain_id.or_else(|| self.funding_chain_id()).map(Chain::from_id), + } + } /// Acquire the payment serialization lock. The returned guard must be held /// across the entire 402 → pay → retry → response cycle to prevent /// concurrent channel opens and colliding expiring-nonce transactions. - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn lock_pay(&self) -> impl Future>> + Send { async { None } } } @@ -599,7 +1032,7 @@ impl ResolveProvider for LazySessionProvider { // regardless of opts. Re-check that the provider's key is compatible // with this challenge's chain/currency. if !provider.matches_challenge(opts.chain_id, opts.currency) { - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "cached provider does not match challenge chain/currency", ))); } @@ -623,7 +1056,16 @@ impl ResolveProvider for LazySessionProvider { fn commit_topup_and_track_voucher(&self) { Self::commit_topup_and_track_voucher(self) } - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn invalidate_cached_provider(&self) { + Self::invalidate(self) + } + fn funding_wallet_address(&self) -> Option { + self.inner.lock().unwrap().as_ref().map(|p| p.funding_wallet_address()) + } + fn funding_chain_id(&self) -> Option { + self.inner.lock().unwrap().as_ref().and_then(|p| p.key_chain_id()) + } + fn lock_pay(&self) -> impl Future>> + Send { let lock = self.pay_lock.clone(); async move { Some(lock.lock_owned().await) } } @@ -685,7 +1127,7 @@ mod tests { fn pay( &self, challenge: &PaymentChallenge, - ) -> impl std::future::Future> + Send { + ) -> impl Future> + Send { let echo = challenge.to_echo(); async move { Ok(PaymentCredential::with_source( @@ -697,6 +1139,21 @@ mod tests { } } + #[derive(Clone, Debug)] + struct InsufficientBalanceProvider; + + impl PaymentProvider for InsufficientBalanceProvider { + fn supports(&self, method: &str, intent: &str) -> bool { + method == "tempo" && (intent == "session" || intent == "charge") + } + + async fn pay(&self, _challenge: &PaymentChallenge) -> Result { + Err(MppError::InsufficientBalance(Some( + "wallet has 0 pathUSD but needs 100000".to_string(), + ))) + } + } + fn test_challenge() -> (PaymentChallenge, String) { let request = Base64UrlJson::from_value(&serde_json::json!({ "amount": "1000", @@ -853,8 +1310,238 @@ mod tests { handle.abort(); } + #[tokio::test] + async fn test_mpp_transport_payment_failure_suggests_tempo_wallet_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move || { + let www_auth = www_auth.clone(); + async move { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required", + ) + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + InsufficientBalanceProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + #[tokio::test] + async fn test_mpp_transport_retry_402_insufficient_balance_suggests_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail( + "Insufficient pathUSD balance: have 0, need 100000", + ), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("InsufficientBalanceError"), "got: {msg}"); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + /// Generic `verification-failed` has many non-funding causes (bad signature, + /// replay, expired challenge, clock skew, ...). The transport must surface + /// the original error verbatim and must NOT add a "fund your wallet" hint. + #[tokio::test] + async fn test_mpp_transport_final_402_verification_failed_does_not_suggest_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Verification Failed"), "got: {msg}"); + assert!( + !msg.contains("Tempo wallet payment could not be funded"), + "verification-failed must not be classified as fundable; got: {msg}" + ); + + handle.abort(); + } + + // --- Classifier unit tests -------------------------------------------- + + #[test] + fn classifier_only_triggers_on_explicit_insufficient_balance_problem() { + // explicit insufficient-balance → true + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail("Insufficient pathUSD balance"), + ) + .unwrap(); + assert!(should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_verification_failed() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_unrelated_text_with_balance_words() { + // Free-text 402 body that just happens to mention the word "balance" + // must NOT trigger the fund suggestion (no structured problem details). + let body = + b"402 Payment Required: server could not balance ledger entries; insufficient inputs."; + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, body)); + } + + #[test] + fn classifier_does_not_trigger_outside_402() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_detail("Insufficient balance"), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::INTERNAL_SERVER_ERROR, &body)); + assert!(!should_suggest_tempo_fund(StatusCode::OK, &body)); + } + + #[test] + fn fund_help_includes_address_and_network_for_known_chain() { + let ctx = FundingContext { + wallet_address: Some("0x000000000000000000000000000000000000dEaD".parse().unwrap()), + token: Some("0x20c0".to_string()), + chain_id: Some(Chain::from_id(42431)), + }; + let help = tempo_wallet_fund_help(&ctx); + assert!(help.contains("--address 0x"), "missing --address: {help}"); + assert!(help.contains("--network tempo-moderato"), "missing --network: {help}"); + assert!(help.contains("--no-browser"), "missing --no-browser: {help}"); + assert!(help.contains("Requested payment token: 0x20c0"), "missing token: {help}"); + + let mainnet = FundingContext { chain_id: Some(Chain::from_id(4217)), ..ctx }; + let help2 = tempo_wallet_fund_help(&mainnet); + assert!(help2.contains("--network tempo"), "missing tempo network: {help2}"); + } + + #[test] + fn auto_fund_policy_blocks_in_ci_and_non_tty() { + assert!(!interactive_tempo_fund_allowed(Some("1"), true, true, true), "must not run in CI"); + assert!( + interactive_tempo_fund_allowed(Some("0"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=0 must not disable" + ); + assert!( + interactive_tempo_fund_allowed(Some("false"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=false must not disable" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, false, true), + "stdin must be a terminal" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, true, false), + "stderr must be a terminal" + ); + assert!(!interactive_tempo_fund_allowed(Some("1"), false, true, true)); + assert!(!interactive_tempo_fund_allowed(Some("true"), false, true, true)); + assert!(interactive_tempo_fund_allowed(None, false, true, true)); + } + #[tokio::test] async fn test_plain_http_402_shows_mpp_setup_instructions() { + let _g = crate::tempo::test_env_mutex().lock().await; let (_, www_auth) = test_challenge(); let app = axum::Router::new().route( @@ -920,6 +1607,32 @@ mod tests { ); } + /// `invalidate_cached_provider` clears the cache so the next + /// `get_or_init` re-runs discovery — the path `do_request` takes after + /// `ensure_access_key` writes a fresh `keys.toml` entry. + #[tokio::test] + async fn lazy_session_provider_invalidate_clears_cache() { + let _g = crate::tempo::test_env_mutex().lock().await; + // TEMPO_PRIVATE_KEY lets discovery succeed without a keys.toml. + let key_hex = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + unsafe { + std::env::set_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV, key_hex); + std::env::remove_var(crate::tempo::TEMPO_HOME_ENV); + } + + let lazy = LazySessionProvider::new("https://rpc.example.com".into()); + let _ = lazy.get_or_init(Default::default()).expect("discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected provider to be cached"); + + ResolveProvider::invalidate_cached_provider(&lazy); + assert!(lazy.inner.lock().unwrap().is_none(), "expected cache to be cleared"); + + let _ = lazy.get_or_init(Default::default()).expect("re-discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected re-init to repopulate cache"); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV) }; + } + #[test] fn challenge_chain_and_currency_extraction() { let extract = |headers: Vec<&str>| -> Vec<(Option, Option)> { @@ -955,4 +1668,73 @@ mod tests { ); assert_eq!(extract(vec![&no_details]), vec![(None, Some("0x20c0".into()))]); } + + /// Auth must trigger when a key matches the chain but not the currency. + #[test] + fn pick_chain_needing_auth_currency_aware() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let dir = tempfile::tempdir().unwrap(); + let wallet = dir.path().join("wallet"); + std::fs::create_dir_all(&wallet).unwrap(); + std::fs::write( + wallet.join("keys.toml"), + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +chain_id = 4217 + +[[keys.limits]] +currency = "0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +limit = "1000" +"#, + ) + .unwrap(); + unsafe { + std::env::set_var(crate::tempo::TEMPO_HOME_ENV, dir.path()); + std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV); + } + + let url = Url::parse("https://rpc.mpp.tempo.xyz").unwrap(); + let mk = |currency: &str| -> PaymentChallenge { + PaymentChallenge { + id: "x".into(), + realm: "api".into(), + method: MethodName::new("tempo"), + intent: IntentName::new("charge"), + request: Base64UrlJson::from_value(&serde_json::json!({ + "amount": "1", + "currency": currency, + "recipient": "0xabc", + "methodDetails": { "chainId": 4217 } + })) + .unwrap(), + expires: None, + description: None, + digest: None, + opaque: None, + } + }; + + // Currency mismatch → auth needed. + let mismatched = mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + assert_eq!(pick_chain_needing_auth(&url, &[mismatched]), Some(4217)); + + // Currency match → no auth. + let matched = mk("0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert_eq!(pick_chain_needing_auth(&url, &[matched]), None); + + // Non-Tempo host → never triggers, even without a key. + let stripe_url = Url::parse("https://api.stripe.com").unwrap(); + assert_eq!( + pick_chain_needing_auth( + &stripe_url, + &[mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")] + ), + None, + ); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_HOME_ENV) }; + } } diff --git a/crates/common/src/provider/mpp/ws.rs b/crates/common/src/provider/mpp/ws.rs index f631d0b08a2fc..69aef7d4f4cbc 100644 --- a/crates/common/src/provider/mpp/ws.rs +++ b/crates/common/src/provider/mpp/ws.rs @@ -378,6 +378,8 @@ mod tests { /// MPP server sends challenge → client pays → server sends receipt. #[tokio::test] async fn test_ws_mpp_challenge_credential_receipt() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); @@ -452,6 +454,8 @@ mod tests { /// MPP server sends challenge, client pays, server closes → rollback. #[tokio::test] async fn test_ws_mpp_rollback_on_post_pay_close() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); diff --git a/crates/common/src/provider/runtime_transport.rs b/crates/common/src/provider/runtime_transport.rs index 7db1ebd1b3f91..f59a2efa75b8e 100644 --- a/crates/common/src/provider/runtime_transport.rs +++ b/crates/common/src/provider/runtime_transport.rs @@ -36,7 +36,11 @@ fn is_known_mpp_endpoint(url: &Url) -> bool { /// Only meant to be used internally by [RuntimeTransport]. #[derive(Clone, Debug)] pub enum InnerTransport { - /// HTTP transport with lazy MPP 402 handling + /// HTTP transport with lazy MPP 402 handling. + /// + /// For known Tempo endpoints, the MPP layer additionally runs the + /// `wallet.tempo.xyz` device-code flow on a 402 when no local access key + /// is configured (see [`crate::tempo::ensure_access_key`]). Http(LazyMppHttpTransport), /// WebSocket transport Ws(PubSubFrontend), diff --git a/crates/common/src/tempo/auth.rs b/crates/common/src/tempo/auth.rs new file mode 100644 index 0000000000000..d79306cfb74f2 --- /dev/null +++ b/crates/common/src/tempo/auth.rs @@ -0,0 +1,494 @@ +//! Tempo wallet device-code authorization flow. +//! +//! Implements the CLI side of the tempoxyz/accounts `cli-auth` device-code +//! protocol: generates a local secp256k1 access key, creates a PKCE-protected +//! device code, opens `wallet.tempo.xyz/cli-auth?code=` in the browser, +//! polls until the user authorizes the key on their passkey wallet, and writes +//! the resulting `keyAuthorization` to `~/.tempo/wallet/keys.toml`. + +use crate::tempo::{ + KeyEntry, KeyType, StoredTokenLimit, WalletType, decode_key_authorization, upsert_key_entry, +}; +use alloy_primitives::{Address, B256, hex}; +use alloy_signer_local::PrivateKeySigner; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use eyre::Result; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +#[cfg(any(unix, windows))] +use std::process::Command; +use std::{ + env, + sync::LazyLock, + time::{Duration, Instant}, +}; +use tempo_primitives::transaction::{SignatureType, SignedKeyAuthorization}; +use tokio::sync::Mutex; + +/// Default device-code service URL (production wallet.tempo.xyz). +const DEFAULT_CLI_AUTH_URL: &str = "https://wallet.tempo.xyz/cli-auth"; + +/// Returns `true` if `url`'s host is `tempo.xyz` or a subdomain of it. +pub(crate) fn is_known_tempo_endpoint(url: &url::Url) -> bool { + url.host_str().is_some_and(|host| host == "tempo.xyz" || host.ends_with(".tempo.xyz")) +} + +/// Env var to override the device-code service URL (for tests / staging). +const TEMPO_CLI_AUTH_URL_ENV: &str = "TEMPO_CLI_AUTH_URL"; + +const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(2); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); + +/// Per-process serialization of concurrent `ensure_access_key` calls. +/// +/// Prevents two `cast` invocations in the same process from racing two browser +/// popups for the same chain. +static AUTH_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +/// Configuration for [`ensure_access_key`]. +#[derive(Clone, Debug)] +pub struct EnsureAccessKeyConfig { + /// Chain ID the access key is being authorized for. + pub chain_id: u64, + /// Device-code service base URL. Defaults to [`DEFAULT_CLI_AUTH_URL`]. + pub(crate) service_url: String, + /// Poll interval. + pub(crate) poll_interval: Duration, + /// Total timeout for the authorization flow. + pub(crate) timeout: Duration, + /// If `true`, print the authorization URL to stderr instead of opening a + /// browser. + pub no_browser: bool, +} + +impl EnsureAccessKeyConfig { + /// Build a config from the environment for the given chain. + /// + /// `no_browser` defaults to `true` under `CI`; callers (e.g. `cast tempo + /// login --no-browser`) may override it. + pub fn from_env(chain_id: u64) -> Self { + Self { + chain_id, + service_url: env::var(TEMPO_CLI_AUTH_URL_ENV) + .unwrap_or_else(|_| DEFAULT_CLI_AUTH_URL.to_string()), + poll_interval: DEFAULT_POLL_INTERVAL, + timeout: DEFAULT_TIMEOUT, + no_browser: env::var_os("CI").is_some(), + } + } +} + +/// Open `url` via the OS default browser handler. On platforms without a known +/// opener, this is a no-op (the URL is still printed by [`ensure_access_key`]). +fn open_browser(_url: &str) { + #[cfg(target_os = "macos")] + let _ = Command::new("open").arg(_url).spawn(); + #[cfg(target_os = "windows")] + let _ = Command::new("cmd").args(["/c", "start", "", _url]).spawn(); + #[cfg(all(unix, not(target_os = "macos")))] + let _ = Command::new("xdg-open").arg(_url).spawn(); +} + +/// Result of [`ensure_access_key`]. +#[derive(Debug, Clone)] +pub struct AccessKeyOutcome { + pub wallet_address: Address, + pub key_address: Address, + pub chain_id: u64, +} + +/// Run the device-code flow, persist the resulting key to `keys.toml`, and +/// return the new entry's identifying fields. +pub async fn ensure_access_key(cfg: EnsureAccessKeyConfig) -> Result { + let _guard = AUTH_LOCK.lock().await; + + let signer = PrivateKeySigner::random(); + let key_address = signer.address(); + // The server requires uncompressed SEC1 (65-byte `0x04 || X || Y`); the + // default `to_sec1_bytes()` would emit the compressed 33-byte form. + let pub_key_hex = format!( + "0x{}", + hex::encode(signer.credential().verifying_key().to_encoded_point(false).as_bytes()), + ); + + let code_verifier = random_code_verifier(); + let client = reqwest::Client::builder().timeout(Duration::from_secs(30)).build()?; + let service = cfg.service_url.trim_end_matches('/'); + + let create_req = CreateCodeRequest { + chain_id: cfg.chain_id, + code_challenge: sha256_b64url(&code_verifier), + key_type: "secp256k1", + pub_key: pub_key_hex, + }; + let code = create_code_with_retry(&client, service, &create_req, cfg.timeout).await?; + + let browser_url = format!("{service}?code={code}"); + if cfg.no_browser { + let _ = crate::sh_eprintln!("Open this URL to authorize: {browser_url}"); + } else { + let _ = crate::sh_eprintln!( + "Opening wallet.tempo to authorize an access key…\n {browser_url}" + ); + open_browser(&browser_url); + } + + let poll = PollRequest { code_verifier }; + let started = Instant::now(); + loop { + // Retry transient network/5xx/429 failures within `cfg.timeout`. + let send_res = client.post(format!("{service}/poll/{code}")).json(&poll).send().await; + + let resp = match send_res { + Ok(r) => r, + Err(e) if is_transient_error(&e) && started.elapsed() < cfg.timeout => { + tracing::debug!(error = %e, "transient error polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + Err(e) => return Err(e.into()), + }; + + let status = resp.status(); + if !status.is_success() { + if is_transient_status(status) && started.elapsed() < cfg.timeout { + tracing::debug!(%status, "transient HTTP status polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code poll failed ({status}): {body}"); + } + + let body: PollResponse = resp.json().await?; + match body { + PollResponse::Pending => { + if started.elapsed() > cfg.timeout { + eyre::bail!("timed out waiting for wallet authorization (code {code})"); + } + tokio::time::sleep(cfg.poll_interval).await; + } + PollResponse::Expired => { + eyre::bail!("device code {code} expired before authorization"); + } + PollResponse::Authorized { account_address, key_authorization } => { + let hex_str = key_authorization.ok_or_else(|| { + eyre::eyre!("wallet authorized response missing key_authorization") + })?; + let signed: SignedKeyAuthorization = decode_key_authorization(&hex_str)?; + // Reject mismatches before persisting — an unusable keys.toml + // entry would silently break the next 402 retry. + if signed.authorization.key_id != key_address { + eyre::bail!( + "wallet authorized key {} but the locally generated key is {}", + signed.authorization.key_id, + key_address, + ); + } + if signed.authorization.chain_id != cfg.chain_id { + eyre::bail!( + "wallet authorized chain {} but {} was requested", + signed.authorization.chain_id, + cfg.chain_id, + ); + } + if signed.authorization.key_type != SignatureType::Secp256k1 { + eyre::bail!( + "wallet returned keyType {:?} but secp256k1 was requested", + signed.authorization.key_type, + ); + } + let chain_id = signed.authorization.chain_id; + let key_authorization = + if hex_str.starts_with("0x") { hex_str } else { format!("0x{hex_str}") }; + let entry = KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: account_address, + chain_id, + key_type: match signed.authorization.key_type { + SignatureType::P256 => KeyType::P256, + SignatureType::WebAuthn => KeyType::WebAuthn, + _ => KeyType::Secp256k1, + }, + key_address: Some(key_address), + key: Some(format!("0x{}", hex::encode(signer.to_bytes()))), + key_authorization: Some(key_authorization), + expiry: signed.authorization.expiry.map(|n| n.get()), + limits: signed + .authorization + .limits + .unwrap_or_default() + .into_iter() + .map(|l| StoredTokenLimit { currency: l.token, limit: l.limit.to_string() }) + .collect(), + }; + upsert_key_entry(entry)?; + return Ok(AccessKeyOutcome { + wallet_address: account_address, + key_address, + chain_id, + }); + } + } + } +} + +fn is_transient_error(err: &reqwest::Error) -> bool { + err.is_timeout() || err.is_connect() || err.is_request() +} + +fn is_transient_status(status: reqwest::StatusCode) -> bool { + status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS +} + +/// POST `/code` with exponential backoff on transient errors, bounded by `timeout`. +async fn create_code_with_retry( + client: &reqwest::Client, + service: &str, + req: &CreateCodeRequest, + timeout: Duration, +) -> Result { + let started = Instant::now(); + let mut backoff = Duration::from_millis(500); + loop { + let send_res = client.post(format!("{service}/code")).json(req).send().await; + + match send_res { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + let CreateCodeResponse { code } = resp.json().await?; + return Ok(code); + } + if is_transient_status(status) && started.elapsed() < timeout { + tracing::debug!(%status, "transient HTTP status creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code create failed ({status}): {body}"); + } + Err(e) if is_transient_error(&e) && started.elapsed() < timeout => { + tracing::debug!(error = %e, "transient error creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + } + Err(e) => return Err(e.into()), + } + } +} + +fn random_code_verifier() -> String { + let bytes = B256::random(); + URL_SAFE_NO_PAD.encode(bytes.as_slice()) +} + +fn sha256_b64url(input: &str) -> String { + let digest = Sha256::digest(input.as_bytes()); + URL_SAFE_NO_PAD.encode(digest) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CreateCodeRequest { + /// `0x`-hex per the SDK schema (server accepts hex string or bigint, not a plain JSON number). + #[serde(serialize_with = "serialize_u64_hex")] + chain_id: u64, + code_challenge: String, + key_type: &'static str, + pub_key: String, +} + +fn serialize_u64_hex(v: &u64, s: S) -> std::result::Result { + s.serialize_str(&format!("0x{v:x}")) +} + +#[derive(Deserialize)] +struct CreateCodeResponse { + code: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PollRequest { + code_verifier: String, +} + +/// Matches `tempoxyz/wallet` poll response shape. +#[derive(Deserialize)] +#[serde(tag = "status", rename_all = "lowercase")] +enum PollResponse { + Pending, + Expired, + Authorized { + account_address: Address, + #[serde(default)] + key_authorization: Option, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tempo::{TEMPO_HOME_ENV, read_tempo_keys_file, test_env_mutex}; + use axum::{Json, Router, extract::State, routing::post}; + use std::sync::{Arc, Mutex}; + + #[test] + fn pkce_challenge_matches_sdk_format() { + // Vector from RFC 7636 §4.2. + let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + let challenge = sha256_b64url(verifier); + assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); + } + + /// Recover the EOA from a SEC1-encoded public key (compressed or + /// uncompressed). + fn address_from_sec1_hex(s: &str) -> Address { + let stripped = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(stripped).expect("valid hex"); + let vk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes).expect("valid SEC1 pubkey"); + Address::from_public_key(&vk) + } + + #[derive(Clone)] + struct MockState { + wallet: Arc>>, + /// Derived from the `pubKey` posted to `/code` so `/poll` can echo + /// back a matching `keyId`, like a real wallet would. + key_id: Arc>>, + /// Chain ID the mock `/poll` returns in `keyAuthorization`. + poll_chain_id: u64, + } + + async fn create_code_handler( + State(state): State, + Json(body): Json, + ) -> Json { + // Sanity: required fields present and chainId is a 0x-hex string, + // matching the SDK wire format the live server enforces. + let pub_key = body + .get("pubKey") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("pubKey missing: {body}")); + assert!(body.get("codeChallenge").is_some(), "codeChallenge missing: {body}"); + let chain_id = body.get("chainId").unwrap_or_else(|| panic!("chainId missing: {body}")); + let chain_str = chain_id + .as_str() + .unwrap_or_else(|| panic!("chainId must be string, got {chain_id}: {body}")); + assert!(chain_str.starts_with("0x"), "chainId must be 0x-hex, got {chain_str}"); + let wallet: Address = "0x0000000000000000000000000000000000000042".parse().unwrap(); + *state.wallet.lock().unwrap() = Some(wallet); + *state.key_id.lock().unwrap() = Some(address_from_sec1_hex(pub_key)); + Json(serde_json::json!({ "code": "ABCDEFGH" })) + } + + /// Build the RLP-hex `SignedKeyAuthorization` blob the live server returns + /// in the `key_authorization` field. + fn signed_key_auth_hex(chain_id: u64, key_id: Address, expiry: u64) -> String { + use alloy_rlp::Encodable; + use tempo_primitives::transaction::{KeyAuthorization, PrimitiveSignature}; + let auth = KeyAuthorization::unrestricted(chain_id, SignatureType::Secp256k1, key_id) + .with_expiry(expiry); + let sig: PrimitiveSignature = serde_json::from_value(serde_json::json!({ + "type": "secp256k1", "r": "0x0", "s": "0x0", "yParity": 0 + })) + .unwrap(); + let signed = auth.into_signed(sig); + let mut buf = Vec::new(); + signed.encode(&mut buf); + format!("0x{}", hex::encode(buf)) + } + + async fn poll_handler(State(state): State) -> Json { + let wallet = state.wallet.lock().unwrap().expect("create_code must be called first"); + let key_id = state.key_id.lock().unwrap().expect("create_code must be called first"); + Json(serde_json::json!({ + "status": "authorized", + "account_address": wallet, + "key_authorization": signed_key_auth_hex(state.poll_chain_id, key_id, 9_999_999_999), + })) + } + + /// Spawn a mock wallet.tempo server whose `/poll` echoes `poll_chain_id`. + async fn spawn_mock_wallet(poll_chain_id: u64) -> (String, tokio::task::JoinHandle<()>) { + let app = Router::new() + .route("/code", post(create_code_handler)) + .route("/poll/{code}", post(poll_handler)) + .with_state(MockState { + wallet: Arc::default(), + key_id: Arc::default(), + poll_chain_id, + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let handle = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + (format!("http://{addr}"), handle) + } + + fn test_cfg(service_url: String) -> EnsureAccessKeyConfig { + EnsureAccessKeyConfig { + chain_id: 4217, + service_url, + poll_interval: Duration::from_millis(10), + timeout: Duration::from_secs(2), + no_browser: true, + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_happy_path_writes_keys_toml() { + // SAFETY: serialized with other tests that mutate TEMPO_HOME. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(4217).await; + let outcome = ensure_access_key(test_cfg(service_url)).await.unwrap(); + + let expected_wallet: Address = + "0x0000000000000000000000000000000000000042".parse().unwrap(); + assert_eq!(outcome.chain_id, 4217); + assert_eq!(outcome.wallet_address, expected_wallet); + + let file = read_tempo_keys_file().expect("keys.toml written"); + assert_eq!(file.keys.len(), 1); + let entry = &file.keys[0]; + assert_eq!(entry.wallet_address, outcome.wallet_address); + assert_eq!(entry.key_address, Some(outcome.key_address)); + assert_eq!(entry.chain_id, 4217); + assert_eq!(entry.expiry, Some(9_999_999_999)); + let decoded: tempo_primitives::transaction::SignedKeyAuthorization = + crate::tempo::decode_key_authorization(entry.key_authorization.as_deref().unwrap()) + .expect("RLP roundtrip"); + assert_eq!(decoded.authorization.chain_id, 4217); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_rejects_wrong_chain_id() { + // Wallet returns chain 99999 but client requested 4217 → must reject + // and persist nothing, else discovery would later fail to find a key + // for the requested chain. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(99999).await; + let err = ensure_access_key(test_cfg(service_url)).await.unwrap_err(); + assert!( + err.to_string().contains("wallet authorized chain 99999 but 4217 was requested"), + "expected chain mismatch error, got: {err}" + ); + assert!(read_tempo_keys_file().is_none_or(|f| f.keys.is_empty())); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } +} diff --git a/crates/common/src/tempo/keystore.rs b/crates/common/src/tempo/keystore.rs index 18edf39be59bd..b4f9527d1b106 100644 --- a/crates/common/src/tempo/keystore.rs +++ b/crates/common/src/tempo/keystore.rs @@ -5,8 +5,8 @@ use alloy_primitives::{Address, hex}; use alloy_rlp::Decodable; -use serde::Deserialize; -use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use std::{env, fs, io::Write, path::PathBuf}; /// Environment variable for an ephemeral Tempo private key. pub const TEMPO_PRIVATE_KEY_ENV: &str = "TEMPO_PRIVATE_KEY"; @@ -21,7 +21,7 @@ pub const DEFAULT_TEMPO_HOME: &str = ".tempo"; pub const WALLET_KEYS_PATH: &str = "wallet/keys.toml"; /// Wallet type matching `tempo-common`'s `WalletType` enum. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum WalletType { #[default] @@ -30,7 +30,7 @@ pub enum WalletType { } /// Cryptographic key type. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum KeyType { #[default] @@ -40,7 +40,7 @@ pub enum KeyType { } /// Per-token spending limit stored in `keys.toml`. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct StoredTokenLimit { pub currency: Address, pub limit: String, @@ -50,7 +50,7 @@ pub struct StoredTokenLimit { /// /// Mirrors the fields from `tempo-common::keys::model::KeyEntry`. /// Unknown fields are ignored by serde. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct KeyEntry { /// Wallet type: "local" or "passkey". #[serde(default)] @@ -65,20 +65,20 @@ pub struct KeyEntry { #[serde(default)] pub key_type: KeyType, /// Key address (the EOA derived from the private key). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key_address: Option
, /// Key private key, stored inline in keys.toml. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key: Option, /// RLP-encoded signed key authorization (hex string). /// Used in keychain mode to atomically provision the access key on-chain. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key_authorization: Option, /// Expiry timestamp. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub expiry: Option, /// Per-token spending limits. - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub limits: Vec, } @@ -90,17 +90,27 @@ impl KeyEntry { } /// The top-level structure of `keys.toml`. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct KeysFile { #[serde(default)] pub keys: Vec, } +/// Process-wide mutex used by tests that mutate `TEMPO_HOME`. +/// +/// Returns a [`tokio::sync::Mutex`] so async tests can hold it across `.await` +/// points without tripping `clippy::await_holding_lock`. +#[cfg(test)] +pub(crate) fn test_env_mutex() -> &'static tokio::sync::Mutex<()> { + static M: std::sync::OnceLock> = std::sync::OnceLock::new(); + M.get_or_init(|| tokio::sync::Mutex::new(())) +} + /// Resolve the Tempo home directory. /// /// Uses `TEMPO_HOME` env var if set, otherwise `~/.tempo`. pub fn tempo_home() -> Option { - if let Ok(home) = std::env::var(TEMPO_HOME_ENV) { + if let Ok(home) = env::var(TEMPO_HOME_ENV) { return Some(PathBuf::from(home)); } dirs::home_dir().map(|h| h.join(DEFAULT_TEMPO_HOME)) @@ -122,7 +132,7 @@ pub fn read_tempo_keys_file() -> Option { return None; } - let contents = match std::fs::read_to_string(&keys_path) { + let contents = match fs::read_to_string(&keys_path) { Ok(c) => c, Err(e) => { tracing::warn!(?keys_path, %e, "failed to read tempo keys file"); @@ -148,3 +158,112 @@ pub fn decode_key_authorization(hex_str: &str) -> eyre::Result let auth = T::decode(&mut bytes.as_slice())?; Ok(auth) } + +/// Atomically upsert a [`KeyEntry`] into `keys.toml`. +/// +/// Replaces any existing entry for the same `(wallet_address, chain_id)`. +/// Each Tempo wallet has at most one active access key per chain, so a fresh +/// login always supersedes the previous entry regardless of the new key +/// address. Creates the file (and parent directories) if missing. Writes via +/// temp file + rename so a crash mid-write cannot corrupt the file. +pub(crate) fn upsert_key_entry(entry: KeyEntry) -> eyre::Result<()> { + let path = tempo_keys_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?; + let dir = path.parent().ok_or_else(|| eyre::eyre!("invalid keys path: {}", path.display()))?; + fs::create_dir_all(dir)?; + + let mut file = read_tempo_keys_file().unwrap_or_default(); + file.keys + .retain(|k| !(k.wallet_address == entry.wallet_address && k.chain_id == entry.chain_id)); + file.keys.push(entry); + + let body = toml::to_string_pretty(&file)?; + let contents = format!( + "# Tempo wallet keys — managed by Foundry / Tempo CLI.\n# Do not edit manually.\n\n{body}" + ); + + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; + tmp.write_all(contents.as_bytes())?; + tmp.flush()?; + tmp.persist(&path).map_err(|e| eyre::eyre!("failed to persist keys.toml: {e}"))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + fn with_tempo_home(f: F) { + let tmp = tempfile::tempdir().unwrap(); + // SAFETY: process-global env access is serialized via the shared mutex. + let _g = test_env_mutex().blocking_lock(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + f(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[test] + fn upsert_replaces_matching_entry_atomically() { + with_tempo_home(|| { + let wallet = Address::from_str("0x0000000000000000000000000000000000000001").unwrap(); + let key = Address::from_str("0x0000000000000000000000000000000000000abc").unwrap(); + + let mk = |expiry: u64| KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: wallet, + chain_id: 4217, + key_type: KeyType::Secp256k1, + key_address: Some(key), + key: Some("0xdead".to_string()), + key_authorization: Some("0xbeef".to_string()), + expiry: Some(expiry), + limits: vec![], + }; + + upsert_key_entry(mk(100)).unwrap(); + upsert_key_entry(mk(200)).unwrap(); + + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 1); + assert_eq!(file.keys[0].expiry, Some(200)); + + // Different chain_id => separate entry. + let mut other = mk(300); + other.chain_id = 42431; + upsert_key_entry(other).unwrap(); + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 2); + }); + } + + #[test] + fn upsert_replaces_when_key_address_changes() { + // Re-login produces a fresh random key address; the new entry must + // supersede the old one for the same (wallet, chain), not coexist. + with_tempo_home(|| { + let wallet = Address::from_str("0x0000000000000000000000000000000000000001").unwrap(); + let old_key = Address::from_str("0x000000000000000000000000000000000000aaaa").unwrap(); + let new_key = Address::from_str("0x000000000000000000000000000000000000bbbb").unwrap(); + + let mk = |key_addr: Address| KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: wallet, + chain_id: 4217, + key_type: KeyType::Secp256k1, + key_address: Some(key_addr), + key: Some("0xdead".to_string()), + key_authorization: Some("0xbeef".to_string()), + expiry: Some(100), + limits: vec![], + }; + + upsert_key_entry(mk(old_key)).unwrap(); + upsert_key_entry(mk(new_key)).unwrap(); + + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 1, "old entry must be replaced, not duplicated"); + assert_eq!(file.keys[0].key_address, Some(new_key)); + }); + } +} diff --git a/crates/common/src/tempo/mod.rs b/crates/common/src/tempo/mod.rs index ec51dc607b5ab..ef8d0212bd453 100644 --- a/crates/common/src/tempo/mod.rs +++ b/crates/common/src/tempo/mod.rs @@ -1,8 +1,24 @@ //! Tempo network utilities. +pub mod auth; + +use crate::FoundryTransactionBuilder; +use alloy_network::Network; +use alloy_primitives::{Address, B256, Signature}; +use alloy_signer::Signer; +use eyre::{Context, Result}; +use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; +use std::sync::Arc; + mod keystore; + +pub(crate) use auth::is_known_tempo_endpoint; +pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key}; pub use keystore::*; +#[cfg(test)] +pub(crate) use keystore::test_env_mutex; + #[cfg(test)] mod tests; @@ -16,3 +32,173 @@ mod tests; /// /// See pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000; + +/// Gas sponsor configuration for Tempo fee-payer signatures. +#[derive(Clone, Debug)] +pub struct TempoSponsor { + sponsor: Address, + signer: Option>, + signature: Option, +} + +impl TempoSponsor { + pub const fn new( + sponsor: Address, + signer: Option>, + signature: Option, + ) -> Self { + Self { sponsor, signer, signature } + } + + pub const fn sponsor(&self) -> Address { + self.sponsor + } + + pub async fn attach_and_print( + &self, + tx: &mut N::TransactionRequest, + sender: Address, + ) -> Result + where + N::TransactionRequest: FoundryTransactionBuilder, + { + if self.sponsor == sender { + eyre::bail!( + "invalid Tempo sponsorship: sponsor {} must not equal transaction sender", + self.sponsor + ); + } + + let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| { + eyre::eyre!( + "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction" + ) + })?; + + let preview = TempoSponsorPreview { + sponsor: self.sponsor, + fee_token: tx.fee_token(), + valid_before: tx.valid_before().map(|v| v.get()), + valid_after: tx.valid_after().map(|v| v.get()), + digest, + }; + preview.print()?; + + let signature = if let Some(signature) = self.signature { + signature + } else if let Some(signer) = &self.signer { + signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")? + } else { + eyre::bail!("missing Tempo sponsor signature or signer") + }; + + let recovered = signature + .recover_address_from_prehash(&digest) + .context("failed to recover Tempo sponsor signature")?; + if recovered != self.sponsor { + eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor); + } + if recovered == sender { + eyre::bail!( + "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender" + ); + } + + tx.set_fee_payer_signature(signature); + Ok(preview) + } +} + +/// User-visible sponsor digest metadata for a single outgoing Tempo transaction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TempoSponsorPreview { + pub sponsor: Address, + pub fee_token: Option
, + pub valid_before: Option, + pub valid_after: Option, + pub digest: B256, +} + +impl TempoSponsorPreview { + pub fn print(&self) -> Result<()> { + crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?; + crate::sh_eprintln!( + "Tempo fee token: {}", + self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string()) + )?; + crate::sh_eprintln!( + "Tempo validity: after {}, before {}", + self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()), + self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string()) + )?; + crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?; + Ok(()) + } +} + +/// Resolves a `--tempo.sponsor-signer` URI into a Foundry wallet signer. +pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result { + let spec = spec.trim(); + let (scheme, value) = spec + .split_once("://") + .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value)) + .unwrap_or_else(|| (spec.to_ascii_lowercase(), "")); + + match scheme.as_str() { + "env" => { + if value.is_empty() { + eyre::bail!("env:// sponsor signer requires an environment variable name"); + } + let private_key = std::env::var(value) + .wrap_err_with(|| format!("{value} environment variable is required"))?; + foundry_wallets::utils::create_private_key_signer(&private_key) + } + "private-key" => { + if value.is_empty() { + eyre::bail!("private-key:// sponsor signer requires a private key"); + } + foundry_wallets::utils::create_private_key_signer(value) + } + "keystore" => { + if value.is_empty() { + eyre::bail!("keystore:// sponsor signer requires a keystore path"); + } + WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "account" => { + if value.is_empty() { + eyre::bail!("account:// sponsor signer requires an account name"); + } + WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "ledger" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { ledger: true, raw, ..Default::default() }.signer().await + } + "trezor" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { trezor: true, raw, ..Default::default() }.signer().await + } + "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await, + "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await, + "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await, + "browser" => { + eyre::bail!( + "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer" + ) + } + _ => eyre::bail!( + "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY" + ), + } +} diff --git a/crates/common/src/transactions/builder.rs b/crates/common/src/transactions/builder.rs index de03cf3adc73e..aa4c971680d00 100644 --- a/crates/common/src/transactions/builder.rs +++ b/crates/common/src/transactions/builder.rs @@ -9,7 +9,9 @@ use alloy_primitives::{Address, B256, Signature, TxKind, U256}; use alloy_provider::Provider; use alloy_signer::Signer; use eyre::Result; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionRequest; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_primitives::{ @@ -244,6 +246,24 @@ pub trait FoundryTransactionBuilder: NetworkTransactionBuilder { /// on-chain as part of this transaction. fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {} + /// Embeds key authorization before gas estimation/signing if the access key is not yet + /// provisioned on-chain. + /// + /// This mirrors the mutation performed by [`Self::sign_with_access_key`], but makes the final + /// transaction body available before fee-payer sponsor digests are computed. + fn prepare_access_key_authorization<'a>( + &'a mut self, + _provider: &'a impl Provider, + _wallet_address: Address, + _key_address: Address, + _key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + async { Ok(()) } + } + /// Converts a CREATE transaction into an AA-compatible call entry. /// /// Tempo AA transactions use a `calls` list instead of `to`+`input`. Must be @@ -355,6 +375,7 @@ impl FoundryTransactionBuilder for ::Transact } } +#[cfg(feature = "optimism")] impl FoundryTransactionBuilder for OpTransactionRequest { fn reset_gas_limit(&mut self) { self.as_mut().gas = None; @@ -439,6 +460,35 @@ impl FoundryTransactionBuilder for ::Tran self.key_authorization = Some(key_authorization); } + fn prepare_access_key_authorization<'a>( + &'a mut self, + provider: &'a impl Provider, + wallet_address: Address, + key_address: Address, + key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + let auth = key_authorization.cloned(); + + async move { + if let Some(auth) = auth { + let is_provisioned = provider + .get_keychain_key(wallet_address, key_address) + .await + .map(|info| info.keyId != Address::ZERO) + .unwrap_or(false); + + if !is_provisioned { + self.set_key_authorization(auth); + } + } + + Ok(()) + } + } + fn convert_create_to_call(&mut self) { if self.calls.is_empty() && self.inner.to.is_some_and(|to| to.is_create()) { let input = self.inner.input.input().cloned().unwrap_or_default(); @@ -473,7 +523,12 @@ impl FoundryTransactionBuilder for ::Tran let is_provisioned = provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false); - if !is_provisioned { + if !is_provisioned && self.key_authorization.is_none() { + if self.fee_payer_signature.is_some() { + eyre::bail!( + "cannot add Tempo key authorization after fee payer signature was attached" + ); + } self.set_key_authorization(auth); } } diff --git a/crates/common/src/transactions/receipt.rs b/crates/common/src/transactions/receipt.rs index 9ca6cb02b10ee..c2e34419248c4 100644 --- a/crates/common/src/transactions/receipt.rs +++ b/crates/common/src/transactions/receipt.rs @@ -7,6 +7,7 @@ use alloy_provider::{ use alloy_rpc_types::{BlockId, TransactionReceipt}; use eyre::Result; use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr}; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionReceipt; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionReceipt; @@ -23,6 +24,7 @@ impl FoundryReceiptResponse for TransactionReceipt { } } +#[cfg(feature = "optimism")] impl FoundryReceiptResponse for OpTransactionReceipt { fn set_contract_address(&mut self, contract_address: Address) { self.inner.contract_address = Some(contract_address); diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index bab59137b0130..8f63718e086cd 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -10,6 +10,10 @@ use std::path::PathBuf; pub struct FuzzConfig { /// The number of test cases that must execute for each property test pub runs: u32, + /// Optional 1-based fuzz run to execute. + pub run: Option, + /// Optional fuzz worker ID to pair with `run`. + pub worker: Option, /// Fails the fuzzed test if a revert occurs. pub fail_on_revert: bool, /// The maximum number of test case rejections allowed, @@ -37,6 +41,8 @@ impl Default for FuzzConfig { fn default() -> Self { Self { runs: 256, + run: None, + worker: None, fail_on_revert: true, max_test_rejects: 65536, seed: None, diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 270df14a6c291..000cefc26737a 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use crate::Config; use alloy_primitives::map::HashMap; use figment::{ @@ -5,6 +7,7 @@ use figment::{ value::{Dict, Map, Value}, }; use foundry_compilers::ProjectCompileOutput; +use foundry_evm_networks::NetworkVariant; use itertools::Itertools; mod natspec; @@ -123,6 +126,42 @@ impl InlineConfig { self.get_function(contract, function).is_some_and(|map| !map.is_empty()) } + /// Returns the configured [`NetworkVariant`] for a given test, checking function-level first + /// then contract-level. Returns `None` if no network annotation is present. + pub fn network_for( + &self, + profile: &Profile, + contract: &str, + function: &str, + ) -> Option { + let data = self.provide(contract, function).data().ok()?; + let dict = data.get(profile).or_else(|| data.get(&Profile::Default))?; + if let Some(Value::Dict(_, networks)) = dict.get("networks") + && let Some(Value::String(_, s)) = networks.get("network") + { + return s.parse().ok(); + } + None + } + + /// Returns all distinct [`NetworkVariant`]s referenced in any inline config annotation. + /// + /// This is used to determine whether a multi-network test pass is needed. + pub fn referenced_override_networks(&self, profile: &Profile) -> Vec { + let mut seen = BTreeSet::new(); + for (contract, function) in self.fn_level.keys() { + if let Some(v) = self.network_for(profile, contract, function) { + seen.insert(v); + } + } + for contract in self.contract_level.keys() { + if let Some(v) = self.network_for(profile, contract, "") { + seen.insert(v); + } + } + seen.into_iter().collect() + } + fn get_contract(&self, contract: &str) -> Option<&DataMap> { self.contract_level.get(contract) } diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index 3c8cad85bae10..cc3dabd32d4bf 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -29,3 +29,11 @@ ratatui = { version = "0.30", default-features = false, features = [ revm.workspace = true tracing.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 809e15b077c37..814beab402729 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -32,3 +32,7 @@ thiserror.workspace = true toml.workspace = true tracing.workspace = true regex.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index b8fd3760cd850..888f6269623a5 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -72,8 +72,8 @@ impl AsDoc for CommentsRef<'_> { writer.writeln_raw(format!( "{}{}: {}", if customs.len() == 1 { "" } else { "- " }, - &c.tag, - &c.value + c.tag, + c.value ))?; writer.writeln()?; } diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 03d569c17f500..801e813026a39 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -36,7 +36,7 @@ alloy-primitives = { workspace = true, features = [ alloy-provider.workspace = true alloy-network.workspace = true alloy-consensus.workspace = true -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-rpc-types = { workspace = true, features = ["anvil"] } alloy-sol-types.workspace = true alloy-rlp.workspace = true @@ -54,9 +54,10 @@ revm = { workspace = true, features = [ "blst", ] } revm-inspectors.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-network.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } tempo-revm.workspace = true tempo-alloy.workspace = true tempo-contracts.workspace = true @@ -77,7 +78,18 @@ url.workspace = true [dev-dependencies] alloy-serde.workspace = true -op-alloy-consensus.workspace = true -op-alloy-rpc-types.workspace = true anvil.workspace = true foundry-test-utils.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "foundry-common/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", +] diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 0cfd56a44219c..b836023a968b7 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -223,8 +223,8 @@ fn trimmed_hex(s: &[u8]) -> String { } else { format!( "{}…{} ({} bytes)", - &hex::encode(&s[..n / 2]), - &hex::encode(&s[s.len() - n / 2..]), + hex::encode(&s[..n / 2]), + hex::encode(&s[s.len() - n / 2..]), s.len(), ) } diff --git a/crates/evm/core/src/env.rs b/crates/evm/core/src/env.rs index 132b986f55e7f..bfc45b6b5d773 100644 --- a/crates/evm/core/src/env.rs +++ b/crates/evm/core/src/env.rs @@ -4,13 +4,9 @@ use alloy_consensus::Typed2718; pub use alloy_evm::EvmEnv; use alloy_evm::FromRecoveredTx; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, U256}; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; -use op_revm::{ - OpTransaction, - transaction::{OpTxTr, deposit::DEPOSIT_TRANSACTION_TYPE}, -}; +#[cfg(feature = "optimism")] +use op_revm::transaction::deposit::DEPOSIT_TRANSACTION_TYPE; use revm::{ Context, Database, Journal, context::{Block, BlockEnv, Cfg, CfgEnv, Transaction, TxEnv}, @@ -236,9 +232,16 @@ pub trait FoundryTransaction: Transaction { /// Sets whether the transaction is a system transaction fn set_system_transaction(&mut self, _is_system_transaction: bool) {} - /// Returns `true` if transaction is of type [`DEPOSIT_TRANSACTION_TYPE`]. + /// Returns `true` if transaction is an Optimism deposit transaction. fn is_deposit(&self) -> bool { - self.tx_type() == DEPOSIT_TRANSACTION_TYPE + #[cfg(feature = "optimism")] + { + self.tx_type() == DEPOSIT_TRANSACTION_TYPE + } + #[cfg(not(feature = "optimism"))] + { + false + } } // Tempo methods @@ -320,188 +323,6 @@ impl FoundryTransaction for TxEnv { } } -impl FoundryTransaction for OpTransaction { - fn set_tx_type(&mut self, tx_type: u8) { - self.base.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.base.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.base.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.base.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.base.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.base.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.base.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.base.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.base.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.base.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.base.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.base.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - OpTxTr::enveloped_tx(self) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.enveloped_tx = Some(bytes); - } - - fn source_hash(&self) -> Option { - OpTxTr::source_hash(self) - } - - fn set_source_hash(&mut self, source_hash: B256) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.source_hash = source_hash; - } - } - - fn mint(&self) -> Option { - OpTxTr::mint(self) - } - - fn set_mint(&mut self, mint: u128) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.mint = Some(mint); - } - } - - fn is_system_transaction(&self) -> bool { - OpTxTr::is_system_transaction(self) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.is_system_transaction = is_system_transaction; - } - } -} - -impl FoundryTransaction for OpTx { - fn set_tx_type(&mut self, tx_type: u8) { - self.0.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.0.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.0.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.0.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.0.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.0.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.0.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.0.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.0.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.0.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.0.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.0.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - FoundryTransaction::enveloped_tx(&self.0) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.0.set_enveloped_tx(bytes); - } - - fn source_hash(&self) -> Option { - FoundryTransaction::source_hash(&self.0) - } - - fn set_source_hash(&mut self, source_hash: B256) { - self.0.set_source_hash(source_hash); - } - - fn mint(&self) -> Option { - FoundryTransaction::mint(&self.0) - } - - fn set_mint(&mut self, mint: u128) { - self.0.set_mint(mint); - } - - fn is_system_transaction(&self) -> bool { - FoundryTransaction::is_system_transaction(&self.0) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - self.0.set_system_transaction(is_system_transaction); - } -} - impl FoundryTransaction for TempoTxEnv { fn set_tx_type(&mut self, tx_type: u8) { self.inner.set_tx_type(tx_type); @@ -687,32 +508,6 @@ impl FromAnyRpcTransaction for TxEnv { } } -impl FromAnyRpcTransaction for OpTx { - fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { - if let Some(envelope) = tx.as_envelope() { - return Ok(Self(OpTransaction:: { - base: TxEnv::from_recovered_tx(envelope, tx.from()), - enveloped_tx: None, - deposit: Default::default(), - })); - } - - // Handle OP deposit transactions from `Unknown` envelope variant. - if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner - && unknown.ty() == DEPOSIT_TX_TYPE_ID - { - let mut fields = unknown.inner.fields.clone(); - fields.insert("from".to_string(), serde_json::to_value(tx.from())?); - let deposit_tx: TxDeposit = fields - .deserialize_into() - .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; - return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); - } - - eyre::bail!("cannot convert unknown transaction type to OpTransaction") - } -} - impl FromAnyRpcTransaction for TempoTxEnv { fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { use alloy_consensus::Transaction as _; @@ -747,6 +542,222 @@ impl FromAnyRpcTransaction for TempoTxEnv { } } +#[cfg(feature = "optimism")] +mod optimism { + use super::*; + use alloy_op_evm::OpTx; + use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; + use op_revm::{OpTransaction, transaction::OpTxTr}; + + impl FoundryTransaction for OpTransaction { + fn set_tx_type(&mut self, tx_type: u8) { + self.base.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.base.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.base.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.base.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.base.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.base.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.base.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.base.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.base.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.base.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.base.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.base.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + OpTxTr::enveloped_tx(self) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.enveloped_tx = Some(bytes); + } + + fn source_hash(&self) -> Option { + OpTxTr::source_hash(self) + } + + fn set_source_hash(&mut self, source_hash: B256) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.source_hash = source_hash; + } + } + + fn mint(&self) -> Option { + OpTxTr::mint(self) + } + + fn set_mint(&mut self, mint: u128) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.mint = Some(mint); + } + } + + fn is_system_transaction(&self) -> bool { + OpTxTr::is_system_transaction(self) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.is_system_transaction = is_system_transaction; + } + } + } + + impl FoundryTransaction for OpTx { + fn set_tx_type(&mut self, tx_type: u8) { + self.0.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.0.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.0.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.0.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.0.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.0.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.0.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.0.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.0.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.0.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.0.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.0.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + FoundryTransaction::enveloped_tx(&self.0) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.0.set_enveloped_tx(bytes); + } + + fn source_hash(&self) -> Option { + FoundryTransaction::source_hash(&self.0) + } + + fn set_source_hash(&mut self, source_hash: B256) { + self.0.set_source_hash(source_hash); + } + + fn mint(&self) -> Option { + FoundryTransaction::mint(&self.0) + } + + fn set_mint(&mut self, mint: u128) { + self.0.set_mint(mint); + } + + fn is_system_transaction(&self) -> bool { + FoundryTransaction::is_system_transaction(&self.0) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + self.0.set_system_transaction(is_system_transaction); + } + } + + impl FromAnyRpcTransaction for OpTx { + fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { + if let Some(envelope) = tx.as_envelope() { + return Ok(Self(OpTransaction:: { + base: TxEnv::from_recovered_tx(envelope, tx.from()), + enveloped_tx: None, + deposit: Default::default(), + })); + } + + // Handle OP deposit transactions from `Unknown` envelope variant. + if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner + && unknown.ty() == DEPOSIT_TX_TYPE_ID + { + let mut fields = unknown.inner.fields.clone(); + fields.insert("from".to_string(), serde_json::to_value(tx.from())?); + let deposit_tx: TxDeposit = fields + .deserialize_into() + .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; + return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); + } + + eyre::bail!("cannot convert unknown transaction type to OpTransaction") + } + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU64; @@ -755,14 +766,10 @@ mod tests { use alloy_consensus::{Sealed, Signed, TxEip1559, transaction::Recovered}; use alloy_evm::{EthEvmFactory, EvmFactory}; use alloy_network::{AnyTxType, UnknownTxEnvelope, UnknownTypedTransaction}; - use alloy_op_evm::OpEvmFactory; use alloy_primitives::Signature; use alloy_rpc_types::{Transaction as RpcTransaction, TransactionInfo}; use alloy_serde::WithOtherFields; use foundry_evm_hardforks::TempoHardfork; - use op_alloy_consensus::{OpTxEnvelope, transaction::OpTransactionInfo}; - use op_alloy_rpc_types::Transaction as OpRpcTransaction; - use op_revm::OpSpecId; use revm::database::EmptyDB; use tempo_alloy::primitives::{ AASigned, TempoSignature, TempoTransaction, TempoTxEnvelope, @@ -793,30 +800,6 @@ mod tests { evm.ctx_mut().set_evm(evm_env); } - #[test] - fn op_evm_foundry_context_ext_implementation() { - let mut evm = - OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); - - // Test EVM Context Block mutation - evm.ctx_mut().block_mut().set_number(U256::from(123)); - assert_eq!(evm.ctx().block().number(), U256::from(123)); - - // Test EVM Context Tx mutation - evm.ctx_mut().tx_mut().set_nonce(99); - assert_eq!(evm.ctx().tx().nonce(), 99); - - // Test EVM Context Cfg mutation - evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; - assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); - - // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env - let tx_env = evm.ctx().tx_clone(); - evm.ctx_mut().set_tx(tx_env); - let evm_env = evm.ctx().evm_clone(); - evm.ctx_mut().set_evm(evm_env); - } - #[test] fn tempo_evm_foundry_context_ext_implementation() { let mut evm = TempoEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default()); @@ -874,23 +857,6 @@ mod tests { assert_eq!(tx_env.kind, TxKind::Call(Address::with_last_byte(0xBB))); } - #[test] - fn from_any_rpc_transaction_for_op() { - let from = Address::random(); - let signed_tx = make_signed_eip1559(); - - // Build the eth TxEnv to compare against op base - let rpc_tx = RpcTransaction::from_transaction( - Recovered::new_unchecked(signed_tx.into(), from), - TransactionInfo::default(), - ); - let any_tx = >::from(rpc_tx); - let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base, expected_base); - } - #[test] fn from_any_rpc_transaction_unknown_envelope_errors() { let unknown = AnyTxEnvelope::Unknown(UnknownTxEnvelope { @@ -915,39 +881,6 @@ mod tests { assert!(result.to_string().contains("unknown transaction type")); } - #[test] - fn from_any_rpc_transaction_for_op_deposit() { - let from = Address::random(); - let source_hash = B256::random(); - let deposit = TxDeposit { - source_hash, - from, - to: TxKind::Call(Address::with_last_byte(0xCC)), - mint: 1111, - value: U256::from(200), - gas_limit: 21000, - is_system_transaction: true, - input: Default::default(), - }; - - // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as AnyRpcTransaction. - let op_rpc_tx = OpRpcTransaction::from_transaction( - Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), - OpTransactionInfo::default(), - ); - let json = serde_json::to_value(&op_rpc_tx).unwrap(); - let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base.caller, from); - assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); - assert_eq!(op_tx_env.base.value, U256::from(200)); - assert_eq!(op_tx_env.base.gas_limit, 21000); - assert_eq!(op_tx_env.deposit.source_hash, source_hash); - assert_eq!(op_tx_env.deposit.mint, Some(1111)); - assert!(op_tx_env.deposit.is_system_transaction); - } - #[test] fn from_any_rpc_transaction_for_tempo_eth_envelope() { let from = Address::random(); @@ -1004,4 +937,88 @@ mod tests { assert_eq!(tx_env.inner.chain_id, Some(42431)); assert_eq!(tx_env.fee_token, fee_token); } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + use alloy_op_evm::{OpEvmFactory, OpTx}; + use op_alloy_consensus::{OpTxEnvelope, TxDeposit, transaction::OpTransactionInfo}; + use op_alloy_rpc_types::Transaction as OpRpcTransaction; + use op_revm::OpSpecId; + + #[test] + fn op_evm_foundry_context_ext_implementation() { + let mut evm = + OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); + + // Test EVM Context Block mutation + evm.ctx_mut().block_mut().set_number(U256::from(123)); + assert_eq!(evm.ctx().block().number(), U256::from(123)); + + // Test EVM Context Tx mutation + evm.ctx_mut().tx_mut().set_nonce(99); + assert_eq!(evm.ctx().tx().nonce(), 99); + + // Test EVM Context Cfg mutation + evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; + assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); + + // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env + let tx_env = evm.ctx().tx_clone(); + evm.ctx_mut().set_tx(tx_env); + let evm_env = evm.ctx().evm_clone(); + evm.ctx_mut().set_evm(evm_env); + } + + #[test] + fn from_any_rpc_transaction_for_op() { + let from = Address::random(); + let signed_tx = make_signed_eip1559(); + + // Build the eth TxEnv to compare against op base + let rpc_tx = RpcTransaction::from_transaction( + Recovered::new_unchecked(signed_tx.into(), from), + TransactionInfo::default(), + ); + let any_tx = >::from(rpc_tx); + let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base, expected_base); + } + + #[test] + fn from_any_rpc_transaction_for_op_deposit() { + let from = Address::random(); + let source_hash = B256::random(); + let deposit = TxDeposit { + source_hash, + from, + to: TxKind::Call(Address::with_last_byte(0xCC)), + mint: 1111, + value: U256::from(200), + gas_limit: 21000, + is_system_transaction: true, + input: Default::default(), + }; + + // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as + // AnyRpcTransaction. + let op_rpc_tx = OpRpcTransaction::from_transaction( + Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), + OpTransactionInfo::default(), + ); + let json = serde_json::to_value(&op_rpc_tx).unwrap(); + let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base.caller, from); + assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); + assert_eq!(op_tx_env.base.value, U256::from(200)); + assert_eq!(op_tx_env.base.gas_limit, 21000); + assert_eq!(op_tx_env.deposit.source_hash, source_hash); + assert_eq!(op_tx_env.deposit.mint, Some(1111)); + assert!(op_tx_env.deposit.is_system_transaction); + } + } } diff --git a/crates/evm/core/src/evm/mod.rs b/crates/evm/core/src/evm/mod.rs index 708226be003a2..fc9e9e7d2810f 100644 --- a/crates/evm/core/src/evm/mod.rs +++ b/crates/evm/core/src/evm/mod.rs @@ -10,14 +10,11 @@ use alloy_evm::{ EthEvmFactory, Evm, EvmEnv, EvmFactory, FromRecoveredTx, precompiles::PrecompilesMap, }; use alloy_network::{Ethereum, Network}; -use alloy_op_evm::OpEvmFactory; use alloy_primitives::{Address, Signature, U256}; use alloy_rlp::Decodable; use foundry_common::{FoundryReceiptResponse, FoundryTransactionBuilder, fmt::UIfmt}; use foundry_config::FromEvmVersion; use foundry_fork_db::{DatabaseError, ForkBlockEnv}; -use op_alloy_network::Optimism; -use op_revm::OpHaltReason; use revm::{ Database, context::{ @@ -36,10 +33,12 @@ use tempo_evm::evm::TempoEvmFactory; use tempo_revm::TempoHaltReason; pub mod eth; +#[cfg(feature = "optimism")] pub mod op; pub mod tempo; pub use eth::*; +#[cfg(feature = "optimism")] pub use op::*; pub use tempo::*; @@ -75,13 +74,6 @@ impl FoundryEvmNetwork for TempoEvmNetwork { type EvmFactory = TempoEvmFactory; } -#[derive(Clone, Copy, Debug, Default)] -pub struct OpEvmNetwork; -impl FoundryEvmNetwork for OpEvmNetwork { - type Network = Optimism; - type EvmFactory = OpEvmFactory; -} - /// Convenience type aliases for accessing associated types through [`FoundryEvmNetwork`]. pub type EvmFactoryFor = ::EvmFactory; pub type FoundryContextFor<'db, FEN> = @@ -249,15 +241,6 @@ impl IntoInstructionResult for HaltReason { } } -impl IntoInstructionResult for OpHaltReason { - fn into_instruction_result(self) -> InstructionResult { - match self { - Self::Base(eth) => eth.into(), - Self::FailedDeposit => InstructionResult::Stop, - } - } -} - impl IntoInstructionResult for TempoHaltReason { fn into_instruction_result(self) -> InstructionResult { match self { diff --git a/crates/evm/core/src/evm/op.rs b/crates/evm/core/src/evm/op.rs index cb8bf272d9a05..efb74ad3abf50 100644 --- a/crates/evm/core/src/evm/op.rs +++ b/crates/evm/core/src/evm/op.rs @@ -1,6 +1,7 @@ use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; use alloy_op_evm::{OpEvm, OpEvmContext, OpEvmFactory, OpTx}; use foundry_fork_db::DatabaseError; +use op_alloy_network::Optimism; use op_revm::{OpEvm as RevmEvm, OpHaltReason, OpSpecId, OpTransactionError, handler::OpHandler}; use revm::{ context::{ @@ -10,16 +11,33 @@ use revm::{ handler::{EthFrame, EvmTr, FrameResult, Handler, instructions::EthInstructions}, inspector::InspectorHandler, interpreter::{ - FrameInput, SharedMemory, interpreter::EthInterpreter, interpreter_action::FrameInit, + FrameInput, InstructionResult, SharedMemory, interpreter::EthInterpreter, + interpreter_action::FrameInit, }, }; use crate::{ FoundryContextExt, FoundryInspectorExt, backend::{DatabaseExt, JournaledState}, - evm::{FoundryEvmFactory, NestedEvm}, + evm::{FoundryEvmFactory, FoundryEvmNetwork, IntoInstructionResult, NestedEvm}, }; +#[derive(Clone, Copy, Debug, Default)] +pub struct OpEvmNetwork; +impl FoundryEvmNetwork for OpEvmNetwork { + type Network = Optimism; + type EvmFactory = OpEvmFactory; +} + +impl IntoInstructionResult for OpHaltReason { + fn into_instruction_result(self) -> InstructionResult { + match self { + Self::Base(eth) => eth.into(), + Self::FailedDeposit => InstructionResult::Stop, + } + } +} + type OpEvmHandler<'db, I> = OpHandler, EVMError, EthFrame>; diff --git a/crates/evm/core/src/fork/database.rs b/crates/evm/core/src/fork/database.rs index aefa0e2ee9741..2284823047ca6 100644 --- a/crates/evm/core/src/fork/database.rs +++ b/crates/evm/core/src/fork/database.rs @@ -212,13 +212,18 @@ pub struct ForkDbStateSnapshot { } impl ForkDbStateSnapshot { - fn get_storage(&self, address: Address, index: U256) -> Option { - self.local - .cache - .accounts - .get(&address) - .and_then(|account| account.storage.get(&index)) - .copied() + /// Lookup storage in `state_snapshot`, then fall back to the backend (remote RPC). + fn storage_from_snapshot_or_backend( + &self, + address: Address, + index: U256, + ) -> Result { + // Check state_snapshot.storage first (data fetched by SharedBackend / disk cache). + if let Some(val) = self.state_snapshot.storage.get(&address).and_then(|s| s.get(&index)) { + return Ok(*val); + } + // Fall back to the underlying backend (SharedBackend → remote RPC). + DatabaseRef::storage_ref(&self.local, address, index) } } @@ -250,15 +255,9 @@ impl DatabaseRef for ForkDbStateSnapshot { match self.local.cache.accounts.get(&address) { Some(account) => match account.storage.get(&index) { Some(entry) => Ok(*entry), - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), - }, - }, - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), + None => self.storage_from_snapshot_or_backend(address, index), }, + None => self.storage_from_snapshot_or_backend(address, index), } } @@ -303,4 +302,28 @@ mod tests { assert!(loaded.is_some()); assert_eq!(loaded.unwrap(), info); } + + /// Verifies that `ForkDbStateSnapshot::storage_ref` reads from `state_snapshot.storage` + /// when the slot is missing from `local.cache.accounts`. Without this lookup the call + /// would fall through to the backend and return the unrelated remote value. + #[tokio::test(flavor = "multi_thread")] + async fn fork_db_state_snapshot_reads_storage_from_snapshot() { + let rpc = foundry_test_utils::rpc::next_http_rpc_endpoint(); + let provider = get_http_provider(rpc.clone()); + let meta = BlockchainDbMeta::new(BlockEnv::default(), rpc); + let db = BlockchainDb::new(meta, None); + let backend = SharedBackend::spawn_backend(Arc::new(provider), db, None).await; + + let address = Address::random(); + let slot = U256::from(42u64); + let expected = U256::from(0xdeadbeefu64); + + let mut state_snapshot = StateSnapshot::default(); + state_snapshot.storage.entry(address).or_default().insert(slot, expected); + + let snapshot = ForkDbStateSnapshot { local: CacheDB::new(backend), state_snapshot }; + + let got = DatabaseRef::storage_ref(&snapshot, address, slot).unwrap(); + assert_eq!(got, expected); + } } diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index 1b2201a9b8b84..c2edbb9dfd33b 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -5,6 +5,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::constants::DEFAULT_CREATE2_DEPLOYER; use alloy_primitives::{Address, map::HashMap}; use auto_impl::auto_impl; diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 82efd4a1aaaaa..ab68eb08821e8 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -137,8 +137,12 @@ impl EvmOpts { /// [`NetworkConfigs::with_chain_id`] to auto-enable the correct network /// (e.g. Tempo, OP Stack) based on the chain ID. pub async fn infer_network_from_fork(&mut self) { + #[cfg(feature = "optimism")] + let already_op = self.networks.is_optimism(); + #[cfg(not(feature = "optimism"))] + let already_op = false; if !self.networks.is_tempo() - && !self.networks.is_optimism() + && !already_op && let Some(ref fork_url) = self.fork_url && let Ok(provider) = self.fork_provider_with_url::(fork_url) && let Ok(chain_id) = provider.get_chain_id().await @@ -474,6 +478,7 @@ mod tests { // Plain anvil (chain id 31337) without tempo flag -> Ethereum (no network flags set). assert!(!evm_opts.networks.is_tempo()); + #[cfg(feature = "optimism")] assert!(!evm_opts.networks.is_optimism()); assert!(!evm_opts.networks.is_celo()); assert_eq!(evm_opts.networks, NetworkConfigs::default()); diff --git a/crates/evm/coverage/Cargo.toml b/crates/evm/coverage/Cargo.toml index d2d7b077ee9f0..758604564726e 100644 --- a/crates/evm/coverage/Cargo.toml +++ b/crates/evm/coverage/Cargo.toml @@ -25,3 +25,7 @@ semver.workspace = true tracing.workspace = true rayon.workspace = true solar.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 70bce50a89882..5dbf07c7a356c 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -61,3 +61,16 @@ serde.workspace = true uuid.workspace = true rayon.workspace = true tokio.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm-core/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 33152b73dda3c..1932a834ab397 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -17,7 +17,7 @@ use foundry_evm_core::{ use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError, - FuzzFixtures, FuzzTestResult, + FuzzFixtures, FuzzRunMetadata, FuzzTestResult, strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state}, }; use foundry_evm_traces::SparsedTraceArena; @@ -71,6 +71,8 @@ struct WorkerState { runs: u32, /// Failure reason if this worker failed failure: Option, + /// Fuzz run metadata that produced the failure. + failure_run: Option, /// Last run timestamp in milliseconds /// /// Used to identify which worker ran last and collect its traces and call breakpoints @@ -93,6 +95,7 @@ impl WorkerState { deprecated_cheatcodes: HashMap::default(), runs: 0, failure: None, + failure_run: None, last_run_timestamp: 0, failed_corpus_replays: 0, } @@ -196,8 +199,14 @@ impl FuzzedExecutor { config: FuzzConfig, persisted_failure: Option, ) -> Self { - let max_workers = - if config.runs == 0 { 0 } else { Ord::max(1, config.runs / MIN_RUNS_PER_WORKER) }; + let run_limit = if config.run.is_some() { 1 } else { config.runs }; + let max_workers = if run_limit == 0 { + 0 + } else if config.run.is_some() { + 1 + } else { + Ord::max(1, run_limit / MIN_RUNS_PER_WORKER) + }; let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize); Self { executor_f: executor, runner, sender, config, persisted_failure, num_workers } } @@ -221,8 +230,9 @@ impl FuzzedExecutor { ) -> Result { let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone()); - debug!(n = self.num_workers, "spawning workers"); - let workers = (0..self.num_workers) + let worker_ids = self.worker_ids(); + debug!(n = worker_ids.len(), "spawning workers"); + let workers = worker_ids .into_par_iter() .map(|worker_id| { let _guard = tokio_handle.enter(); @@ -364,8 +374,14 @@ impl FuzzedExecutor { } else { vec![] }; + let fuzz = failed_worker.failure_run.unwrap_or_default(); result.counterexample = Some(CounterExample::Single( - BaseCounterExample::from_fuzz_call(calldata, args, call.traces), + BaseCounterExample::from_fuzz_call(calldata, args, call.traces) + .with_fuzz_metadata(FuzzRunMetadata::new( + fuzz.seed.or(self.config.seed), + fuzz.run, + fuzz.worker, + )), )); } Some(TestCaseError::Reject(reason)) => { @@ -453,16 +469,7 @@ impl FuzzedExecutor { runner_config.cases = worker_runs; let mut runner = if let Some(seed) = self.config.seed { - // For deterministic parallel fuzzing, derive a unique seed for each worker - let worker_seed = if worker_id == 0 { - // Master worker uses the provided seed as is. - seed - } else { - // Derive a worker-specific seed using keccak256(seed || worker_id) - let seed_data = - [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); - U256::from_be_bytes(keccak256(seed_data).0) - }; + let worker_seed = Self::fuzz_worker_seed(seed, worker_id); trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}"); let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>()); TestRunner::new_with_rng(runner_config, rng) @@ -470,11 +477,25 @@ impl FuzzedExecutor { TestRunner::new(runner_config) }; - let mut persisted_failure = self.persisted_failure.as_ref().filter(|_| worker_id == 0); + if let Some(target_run) = self.config.run { + for _ in 1..target_run { + if let Err(err) = corpus.new_input(&mut runner, &shared_state.state, func) { + worker.failure = Some(TestCaseError::fail(format!( + "failed to generate fuzzed input in worker {}: {err}", + worker.id + ))); + shared_state.try_claim_failure(worker_id); + return Ok(worker); + } + } + } + + let mut persisted_failure = + self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none()); // Offset to stagger corpus syncs across workers; so that workers don't sync at the same // time. - let sync_offset = worker_id as u32 * 100; + let sync_offset = (worker_id as u32).saturating_mul(100); let sync_threshold = SYNC_INTERVAL + sync_offset; let mut runs_since_sync = sync_threshold; // Always sync at the start. let mut last_metrics_report = Instant::now(); @@ -483,11 +504,27 @@ impl FuzzedExecutor { // 2. Worker hasn't reached its specific run limit 'stop: while shared_state.should_continue() && worker.runs < worker_runs { // If counterexample recorded, replay it first, without incrementing runs. - let input = if worker_id == 0 + let (input, fuzz_run) = if worker_id == 0 && let Some(failure) = persisted_failure.take() && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector) { - failure.calldata.clone() + let seed = failure.fuzz.seed.or(self.config.seed); + if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() + && let Some(seed) = seed + { + let run = failure.fuzz.run.unwrap_or(1); + let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize; + cheats.set_seed(Self::fuzz_run_seed(seed, worker, run)); + } + + ( + failure.calldata.clone(), + Some(FuzzRunMetadata::new( + seed, + failure.fuzz.run, + Some(failure.fuzz.worker.unwrap_or(worker_id as u32)), + )), + ) } else { runs_since_sync += 1; if runs_since_sync >= sync_threshold { @@ -503,13 +540,14 @@ impl FuzzedExecutor { runs_since_sync = 0; } + let fuzz_run = self.config.run.unwrap_or(worker.runs + 1); if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() && let Some(seed) = self.config.seed { - cheats.set_seed(seed.wrapping_add(U256::from(worker.runs))); + cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run)); } - match corpus.new_input(&mut runner, &shared_state.state, func) { + let input = match corpus.new_input(&mut runner, &shared_state.state, func) { Ok(input) => input, Err(err) => { worker.failure = Some(TestCaseError::fail(format!( @@ -519,13 +557,24 @@ impl FuzzedExecutor { shared_state.try_claim_failure(worker_id); break 'stop; } - } + }; + + ( + input, + Some(FuzzRunMetadata::new( + self.config.seed, + Some(fuzz_run), + Some(worker_id as u32), + )), + ) }; let mut inc_runs = || { let total_runs = shared_state.increment_runs(); debug_assert!( - shared_state.timer.is_enabled() || total_runs <= self.config.runs, + shared_state.timer.is_enabled() + || total_runs + <= if self.config.run.is_some() { 1 } else { self.config.runs }, "worker runs were not distributed correctly" ); worker.runs += 1; @@ -595,6 +644,7 @@ impl FuzzedExecutor { .. }) => { inc_runs(); + worker.failure_run = fuzz_run; // Only classify magic skip payloads when the revert originates from the // cheatcode address. @@ -656,7 +706,7 @@ impl FuzzedExecutor { /// Determines the number of runs per worker. const fn runs_per_worker(&self, worker_id: usize) -> u32 { let worker_id = worker_id as u32; - let total_runs = self.config.runs; + let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs }; let n = self.num_workers as u32; let runs = total_runs / n; let remainder = total_runs % n; @@ -664,4 +714,29 @@ impl FuzzedExecutor { // assuming `worker_id` is in `0..n`. if worker_id < remainder { runs + 1 } else { runs } } + + /// Returns the worker IDs to execute. + fn worker_ids(&self) -> Vec { + if self.config.run.is_some() { + vec![self.config.worker.unwrap_or(0) as usize] + } else { + (0..self.num_workers).collect() + } + } + + /// Derives the deterministic RNG seed for a fuzz worker. + fn fuzz_worker_seed(seed: U256, worker_id: usize) -> U256 { + if worker_id == 0 { + seed + } else { + let worker_id = worker_id as u32; + let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); + U256::from_be_bytes(keccak256(seed_data).0) + } + } + + /// Derives the deterministic RNG seed for cheatcode randomness in a worker-local run. + fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 { + Self::fuzz_worker_seed(seed, worker_id).wrapping_add(U256::from(run.saturating_sub(1))) + } } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 27d8e6a0ed588..e02cdbc393ee6 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -736,7 +736,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { if !msg.is_empty() { msg.push_str(", "); } - msg.push_str(&format!("{}", &corpus_manager.metrics)); + msg.push_str(&format!("{}", corpus_manager.metrics)); } progress.set_message(msg); } diff --git a/crates/evm/fuzz/Cargo.toml b/crates/evm/fuzz/Cargo.toml index 62e4e80a73674..5629b17d936da 100644 --- a/crates/evm/fuzz/Cargo.toml +++ b/crates/evm/fuzz/Cargo.toml @@ -50,3 +50,12 @@ rand.workspace = true serde.workspace = true thiserror.workspace = true tracing.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 44d71fb6deee3..9c3e7d179c7f6 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -33,6 +33,27 @@ pub use strategies::LiteralMaps; mod inspector; pub use inspector::Fuzzer; +/// Metadata needed to reproduce a fuzz run. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct FuzzRunMetadata { + /// Seed used for the worker's input stream. + #[serde(default, rename = "fuzz_seed", skip_serializing_if = "Option::is_none")] + pub seed: Option, + /// 1-based run inside the worker's input stream. + #[serde(default, rename = "fuzz_run", skip_serializing_if = "Option::is_none")] + pub run: Option, + /// Worker that generated the input stream. + #[serde(default, rename = "fuzz_worker", skip_serializing_if = "Option::is_none")] + pub worker: Option, +} + +impl FuzzRunMetadata { + /// Creates metadata for reproducing a fuzz run. + pub const fn new(seed: Option, run: Option, worker: Option) -> Self { + Self { seed, run, worker } + } +} + /// Details of a transaction generated by fuzz strategy for fuzzing a target. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BasicTxDetails { @@ -102,6 +123,9 @@ pub struct BaseCounterExample { /// Whether to display sequence as solidity. #[serde(skip)] pub show_solidity: bool, + /// Fuzz metadata needed to reproduce this counterexample. + #[serde(flatten)] + pub fuzz: FuzzRunMetadata, } impl BaseCounterExample { @@ -137,6 +161,7 @@ impl BaseCounterExample { ), traces, show_solidity, + fuzz: FuzzRunMetadata::default(), }; } } @@ -154,6 +179,7 @@ impl BaseCounterExample { raw_args: None, traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } @@ -176,8 +202,15 @@ impl BaseCounterExample { raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()), traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } + + /// Sets fuzz metadata for reproducing this counterexample. + pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self { + self.fuzz = fuzz; + self + } } impl fmt::Display for BaseCounterExample { @@ -229,7 +262,7 @@ impl fmt::Display for BaseCounterExample { if let Some(sig) = &self.signature { write!(f, "calldata={sig}")? } else { - write!(f, "calldata={}", &self.calldata)? + write!(f, "calldata={}", self.calldata)? } if let Some(args) = &self.args { diff --git a/crates/evm/hardforks/Cargo.toml b/crates/evm/hardforks/Cargo.toml index 9bf318028487a..68f6fb23bab07 100644 --- a/crates/evm/hardforks/Cargo.toml +++ b/crates/evm/hardforks/Cargo.toml @@ -16,10 +16,14 @@ workspace = true [dependencies] alloy-chains.workspace = true alloy-hardforks = { workspace = true, features = ["serde"] } -alloy-op-hardforks = { workspace = true, features = ["serde"] } +alloy-op-hardforks = { workspace = true, features = ["serde"], optional = true } alloy-rpc-types.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } revm.workspace = true serde = { workspace = true, features = ["derive"] } tempo-chainspec.workspace = true foundry-compilers.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "dep:op-revm"] diff --git a/crates/evm/hardforks/src/lib.rs b/crates/evm/hardforks/src/lib.rs index 8a29ebb7af4ec..a8e0d51738263 100644 --- a/crates/evm/hardforks/src/lib.rs +++ b/crates/evm/hardforks/src/lib.rs @@ -8,11 +8,13 @@ use std::str::FromStr; use alloy_chains::Chain; use alloy_rpc_types::BlockNumberOrTag; use foundry_compilers::artifacts::EvmVersion; +#[cfg(feature = "optimism")] use op_revm::OpSpecId; use revm::primitives::hardfork::SpecId; use serde::{Deserialize, Serialize}; pub use alloy_hardforks::EthereumHardfork; +#[cfg(feature = "optimism")] pub use alloy_op_hardforks::OpHardfork; pub use tempo_chainspec::hardfork::TempoHardfork; @@ -20,6 +22,7 @@ pub use tempo_chainspec::hardfork::TempoHardfork; #[serde(into = "String")] pub enum FoundryHardfork { Ethereum(EthereumHardfork), + #[cfg(feature = "optimism")] Optimism(OpHardfork), Tempo(TempoHardfork), } @@ -28,6 +31,7 @@ impl From for String { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(h) => format!("optimism:{h}"), FoundryHardfork::Tempo(h) => format!("tempo:{h}"), } @@ -64,6 +68,7 @@ impl FromStr for FoundryHardfork { .map(Self::Ethereum) .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")), + #[cfg(feature = "optimism")] "op" | "optimism" => OpHardfork::from_str(&fork) .map(Self::Optimism) .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")), @@ -83,6 +88,7 @@ impl FoundryHardfork { Self::Ethereum(h) } + #[cfg(feature = "optimism")] pub const fn optimism(h: OpHardfork) -> Self { Self::Optimism(h) } @@ -95,6 +101,7 @@ impl FoundryHardfork { pub fn name(&self) -> String { match self { Self::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] Self::Optimism(h) => format!("{h}"), Self::Tempo(h) => format!("{h}"), } @@ -106,6 +113,7 @@ impl FoundryHardfork { pub const fn namespace(&self) -> Option<&'static str> { match self { Self::Ethereum(_) => None, + #[cfg(feature = "optimism")] Self::Optimism(_) => Some("optimism"), Self::Tempo(_) => Some("tempo"), } @@ -119,6 +127,7 @@ impl FoundryHardfork { if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Ethereum(fork)); } + #[cfg(feature = "optimism")] if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Optimism(fork)); } @@ -143,12 +152,14 @@ impl From for EthereumHardfork { } } +#[cfg(feature = "optimism")] impl From for FoundryHardfork { fn from(value: OpHardfork) -> Self { Self::Optimism(value) } } +#[cfg(feature = "optimism")] impl From for OpHardfork { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -177,12 +188,14 @@ impl From for SpecId { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork).into(), FoundryHardfork::Tempo(hardfork) => hardfork.into(), } } } +#[cfg(feature = "optimism")] impl From for OpSpecId { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -223,6 +236,7 @@ pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId { } /// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`. +#[cfg(feature = "optimism")] pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId { match hardfork { OpHardfork::Bedrock => OpSpecId::BEDROCK, @@ -265,6 +279,7 @@ impl FromEvmVersion for SpecId { } } +#[cfg(feature = "optimism")] impl FromEvmVersion for OpSpecId { fn from_evm_version(version: EvmVersion) -> Self { match version { @@ -324,16 +339,6 @@ mod tests { assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA); } - #[test] - fn test_optimism_spec_id_mapping() { - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); - - // Test latest hardforks - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); - } - #[test] fn test_tempo_spec_id_mapping() { assert_eq!(SpecId::from(TempoHardfork::Genesis), SpecId::OSAKA); @@ -371,25 +376,40 @@ mod tests { } #[test] - fn test_from_chain_and_timestamp_op_mainnet() { - let op_chain_id = 10; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); + fn test_from_chain_and_timestamp_unknown_chain() { + assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); } - #[test] - fn test_from_chain_and_timestamp_base() { - let base_chain_id = 8453; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); - } + #[cfg(feature = "optimism")] + mod optimism { + use super::*; - #[test] - fn test_from_chain_and_timestamp_unknown_chain() { - assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); + #[test] + fn test_optimism_spec_id_mapping() { + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); + + // Test latest hardforks + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); + } + + #[test] + fn test_from_chain_and_timestamp_op_mainnet() { + let op_chain_id = 10; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } + + #[test] + fn test_from_chain_and_timestamp_base() { + let base_chain_id = 8453; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } } } diff --git a/crates/evm/networks/Cargo.toml b/crates/evm/networks/Cargo.toml index a63ed34ba61cf..00c9abf0f90f7 100644 --- a/crates/evm/networks/Cargo.toml +++ b/crates/evm/networks/Cargo.toml @@ -19,7 +19,7 @@ foundry-evm-hardforks.workspace = true alloy-chains.workspace = true alloy-eips.workspace = true alloy-evm.workspace = true -alloy-op-hardforks.workspace = true +alloy-op-hardforks = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = [ "serde", "getrandom", @@ -43,4 +43,8 @@ clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } serde.workspace = true [dev-dependencies] -serde_json.workspace = true \ No newline at end of file +serde_json.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "foundry-evm-hardforks/optimism"] diff --git a/crates/evm/networks/src/lib.rs b/crates/evm/networks/src/lib.rs index 303b9ca8b7a13..384cee5a7bed3 100644 --- a/crates/evm/networks/src/lib.rs +++ b/crates/evm/networks/src/lib.rs @@ -11,7 +11,6 @@ use alloy_chains::{ }; use alloy_eips::eip1559::BaseFeeParams; use alloy_evm::precompiles::PrecompilesMap; -use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; use alloy_primitives::{Address, ChainId, map::AddressHashMap}; use clap::Parser; use foundry_evm_hardforks::FoundryHardfork; @@ -20,20 +19,52 @@ use std::collections::BTreeMap; pub mod celo; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] +#[cfg(feature = "optimism")] +mod optimism; + +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + clap::ValueEnum, +)] #[serde(rename_all = "lowercase")] #[clap(rename_all = "lowercase")] pub enum NetworkVariant { #[default] Ethereum, + #[cfg(feature = "optimism")] Optimism, Tempo, } +impl std::str::FromStr for NetworkVariant { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "ethereum" => Ok(Self::Ethereum), + #[cfg(feature = "optimism")] + "optimism" => Ok(Self::Optimism), + "tempo" => Ok(Self::Tempo), + _ => Err(format!("unknown network variant: {s}")), + } + } +} + impl NetworkVariant { pub const fn name(&self) -> &'static str { match self { Self::Ethereum => "ethereum", + #[cfg(feature = "optimism")] Self::Optimism => "optimism", Self::Tempo => "tempo", } @@ -50,32 +81,37 @@ impl From for NetworkVariant { fn from(chain_id: ChainId) -> Self { let chain = Chain::from_id(chain_id); if chain.is_tempo() { - Self::Tempo - } else if chain.is_optimism() { - Self::Optimism - } else { - Self::Ethereum + return Self::Tempo; + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::Optimism; } + Self::Ethereum } } #[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)] pub struct NetworkConfigs { /// Enable a specific network family. - #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "optimism", "tempo"])] + #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] #[serde(default)] - network: Option, + pub(crate) network: Option, /// Enable Celo network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "optimism", "tempo"])] + #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] celo: bool, /// Enable Optimism network features (deprecated: use --network optimism). + #[cfg(feature = "optimism")] #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])] // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the // canonical form is `network = "optimism"`. #[serde(default)] - optimism: bool, + pub(crate) optimism: bool, /// Enable Tempo network features (deprecated: use --network tempo). - #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "optimism"])] + #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the // canonical form is `network = "tempo"`. #[serde(default)] @@ -102,10 +138,6 @@ impl Serialize for NetworkConfigs { } impl NetworkConfigs { - pub fn with_optimism() -> Self { - Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } - } - pub fn with_celo() -> Self { Self { celo: true, ..Default::default() } } @@ -114,11 +146,7 @@ impl NetworkConfigs { Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() } } - pub fn is_optimism(&self) -> bool { - matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) - } - - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { matches!(self.resolved_network(), Some(NetworkVariant::Tempo)) } @@ -127,14 +155,18 @@ impl NetworkConfigs { } /// Returns the resolved network variant, folding legacy flags. - fn resolved_network(&self) -> Option { - self.network.or(if self.optimism { - Some(NetworkVariant::Optimism) - } else if self.tempo { - Some(NetworkVariant::Tempo) - } else { - None - }) + const fn resolved_network(&self) -> Option { + if let Some(n) = self.network { + return Some(n); + } + #[cfg(feature = "optimism")] + if self.optimism { + return Some(NetworkVariant::Optimism); + } + if self.tempo { + return Some(NetworkVariant::Tempo); + } + None } /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum. @@ -150,16 +182,12 @@ impl NetworkConfigs { /// For Optimism networks, returns Canyon parameters if the Canyon hardfork is active /// at the given timestamp, otherwise returns pre-Canyon parameters. pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_hardforks = OpChainHardforks::op_mainnet(); - if op_hardforks.is_canyon_active_at_timestamp(timestamp) { - BaseFeeParams::optimism_canyon() - } else { - BaseFeeParams::optimism() - } - } else { - BaseFeeParams::ethereum() + return self.op_base_fee_params(timestamp); } + let _ = timestamp; + BaseFeeParams::ethereum() } pub fn bypass_prevrandao(&self, chain_id: u64) -> bool { @@ -174,21 +202,23 @@ impl NetworkConfigs { pub fn with_chain_id(self, chain_id: u64) -> Self { let chain = Chain::from_id(chain_id); - if self.resolved_network().is_none() { - if chain.is_tempo() { - Self::with_tempo() - } else if chain.is_optimism() { - Self::with_optimism() + if self.resolved_network().is_some() { + return if !self.celo + && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) + { + Self::with_celo() } else { self - } - } else if !self.celo - && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) - { - Self::with_celo() - } else { - self + }; + } + if chain.is_tempo() { + return Self::with_tempo(); + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::with_optimism(); } + self } /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an @@ -208,6 +238,7 @@ impl NetworkConfigs { let network = match hardfork { FoundryHardfork::Ethereum(_) => self, FoundryHardfork::Tempo(_) => Self::with_tempo(), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(_) => Self::with_optimism(), }; @@ -243,6 +274,21 @@ impl NetworkConfigs { } } +impl From for NetworkConfigs { + fn from(network: NetworkVariant) -> Self { + match network { + NetworkVariant::Ethereum => Self::default(), + NetworkVariant::Tempo => { + Self { network: Some(network), tempo: true, ..Default::default() } + } + #[cfg(feature = "optimism")] + NetworkVariant::Optimism => { + Self { network: Some(network), optimism: true, ..Default::default() } + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -254,17 +300,6 @@ mod tests { let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() }; let via_old = NetworkConfigs { tempo: true, ..Default::default() }; assert_eq!(via_new.is_tempo(), via_old.is_tempo()); - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.active_network_name(), via_old.active_network_name()); - } - - #[test] - fn new_optimism_flag_equivalent_to_legacy() { - let via_new = - NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; - let via_old = NetworkConfigs { optimism: true, ..Default::default() }; - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.is_tempo(), via_old.is_tempo()); assert_eq!(via_new.active_network_name(), via_old.active_network_name()); } @@ -276,31 +311,11 @@ mod tests { assert_eq!(cfg.active_network_name(), Some("tempo")); } - #[test] - fn active_network_name_optimism() { - let cfg = NetworkConfigs::with_optimism(); - assert_eq!(cfg.active_network_name(), Some("optimism")); - } - #[test] fn active_network_name_default_is_none() { assert_eq!(NetworkConfigs::default().active_network_name(), None); } - // --- new flag takes precedence over legacy flag --- - - #[test] - fn new_flag_wins_over_legacy_when_both_set() { - // --network optimism --tempo: network field wins - let cfg = NetworkConfigs { - network: Some(NetworkVariant::Optimism), - tempo: true, - ..Default::default() - }; - assert!(cfg.is_optimism()); - assert!(!cfg.is_tempo()); - } - // --- Serde round-trip --- #[test] @@ -309,16 +324,6 @@ mod tests { let json = serde_json::to_string(&original).unwrap(); let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); assert!(restored.is_tempo()); - assert!(!restored.is_optimism()); - } - - #[test] - fn serde_roundtrip_optimism() { - let original = NetworkConfigs::with_optimism(); - let json = serde_json::to_string(&original).unwrap(); - let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); - assert!(restored.is_optimism()); - assert!(!restored.is_tempo()); } #[test] @@ -345,8 +350,55 @@ mod tests { let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#; let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap(); assert!(cfg_tempo.is_tempo()); - let json_optimism = r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; - let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); - assert!(cfg_optimism.is_optimism()); + } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + + #[test] + fn new_optimism_flag_equivalent_to_legacy() { + let via_new = + NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; + let via_old = NetworkConfigs { optimism: true, ..Default::default() }; + assert_eq!(via_new.is_optimism(), via_old.is_optimism()); + assert_eq!(via_new.is_tempo(), via_old.is_tempo()); + assert_eq!(via_new.active_network_name(), via_old.active_network_name()); + } + + #[test] + fn active_network_name_optimism() { + let cfg = NetworkConfigs::with_optimism(); + assert_eq!(cfg.active_network_name(), Some("optimism")); + } + + #[test] + fn new_flag_wins_over_legacy_when_both_set() { + // --network optimism --tempo: network field wins + let cfg = NetworkConfigs { + network: Some(NetworkVariant::Optimism), + tempo: true, + ..Default::default() + }; + assert!(cfg.is_optimism()); + assert!(!cfg.is_tempo()); + } + + #[test] + fn serde_roundtrip_optimism() { + let original = NetworkConfigs::with_optimism(); + let json = serde_json::to_string(&original).unwrap(); + let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); + assert!(restored.is_optimism()); + assert!(!restored.is_tempo()); + } + + #[test] + fn serde_optimism_field_deserialized() { + let json_optimism = + r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; + let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); + assert!(cfg_optimism.is_optimism()); + } } } diff --git a/crates/evm/networks/src/optimism.rs b/crates/evm/networks/src/optimism.rs new file mode 100644 index 0000000000000..5fffa38a333c7 --- /dev/null +++ b/crates/evm/networks/src/optimism.rs @@ -0,0 +1,25 @@ +//! Optimism-specific extensions for [`NetworkConfigs`] and related helpers. + +use crate::{NetworkConfigs, NetworkVariant}; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; + +impl NetworkConfigs { + pub fn with_optimism() -> Self { + Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } + } + + pub const fn is_optimism(&self) -> bool { + matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) + } + + /// Optimism-specific base fee parameters, picking Canyon vs pre-Canyon based on `timestamp`. + pub(crate) fn op_base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + let op_hardforks = OpChainHardforks::op_mainnet(); + if op_hardforks.is_canyon_active_at_timestamp(timestamp) { + BaseFeeParams::optimism_canyon() + } else { + BaseFeeParams::optimism() + } + } +} diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index 90d2db724cebc..73d64d3ab5d07 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -50,3 +50,7 @@ tempfile.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true yansi.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml index bad5c577bc69e..b6f11772620f9 100644 --- a/crates/fmt/Cargo.toml +++ b/crates/fmt/Cargo.toml @@ -26,3 +26,7 @@ foundry-test-utils.workspace = true toml.workspace = true snapbox.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index 89a9bf152c8c2..4b986017b71dd 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -711,7 +711,7 @@ impl<'sess> State<'sess, '_> { // Merge the lines and let the wrapper handle breaking if needed let merged_line = format!( "{current_line} {next_content}", - next_content = &next_line[prefix.len()..].trim_start() + next_content = next_line[prefix.len()..].trim_start() ); result.push(merged_line); diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 064834d248d5f..667da6b442ca1 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -117,7 +117,7 @@ tempfile.workspace = true alloy-signer-local.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -126,3 +126,15 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cli/optimism", + "forge-script/optimism", + "forge-verify/optimism", + "forge-doc/optimism", + "forge-fmt/optimism", + "forge-lint/optimism", + "forge-sol-macro-gen/optimism", +] diff --git a/crates/forge/assets/tempo/MailTemplate.s.sol b/crates/forge/assets/tempo/MailTemplate.s.sol index 27512efe4d5ec..45006f7cd0e06 100644 --- a/crates/forge/assets/tempo/MailTemplate.s.sol +++ b/crates/forge/assets/tempo/MailTemplate.s.sol @@ -14,7 +14,7 @@ contract MailScript is Script { function run(string memory salt) public { vm.startBroadcast(); - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); ITIP20 token = ITIP20( diff --git a/crates/forge/assets/tempo/MailTemplate.t.sol b/crates/forge/assets/tempo/MailTemplate.t.sol index b1749db5df0bf..19760303860a1 100644 --- a/crates/forge/assets/tempo/MailTemplate.t.sol +++ b/crates/forge/assets/tempo/MailTemplate.t.sol @@ -17,7 +17,7 @@ contract MailTest is Test { address public constant BOB = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); function setUp() public virtual { - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); token = ITIP20( diff --git a/crates/forge/src/cmd/coverage.rs b/crates/forge/src/cmd/coverage.rs index ea034bce87185..b8ce2a9b945b1 100644 --- a/crates/forge/src/cmd/coverage.rs +++ b/crates/forge/src/cmd/coverage.rs @@ -87,8 +87,11 @@ impl CoverageArgs { config = self.load_config()?; } - // Set fuzz seed so coverage reports are deterministic - config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so coverage reports are deterministic, + // but allow the user to override it via `--fuzz-seed` or `[fuzz] seed` in config. + if config.fuzz.seed.is_none() { + config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let (paths, mut output) = { let (project, output) = self.build(&config)?; diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 765bb64f95fdd..4f638b6edcdf8 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -13,7 +13,10 @@ use eyre::{Context, ContextCompat, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{LoadConfig, find_contract_artifacts, read_constructor_args_file}, + utils::{ + LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane, + read_constructor_args_file, resolve_lane, + }, }; use foundry_common::{ FoundryTransactionBuilder, @@ -203,6 +206,11 @@ impl CreateArgs { self.tx.tempo.key_id = Some(ak.key_address); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane. + // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key. + let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?; + // Whether to broadcast the transaction or not let dry_run = !self.broadcast; @@ -223,6 +231,7 @@ impl CreateArgs { dry_run, None, Some(browser), + resolved_lane, ) .await } else if self.unlocked { @@ -239,6 +248,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } else if let Some(ak) = access_key { @@ -259,6 +269,7 @@ impl CreateArgs { dry_run, Some((signer, ak)), None, + resolved_lane, ) .await } else { @@ -282,6 +293,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } @@ -362,6 +374,7 @@ impl CreateArgs { dry_run: bool, tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>, browser_signer: Option>, + resolved_lane: Option, ) -> Result<()> where N::TransactionRequest: FoundryTransactionBuilder + serde::Serialize, @@ -398,7 +411,7 @@ impl CreateArgs { // If Tempo chain fee token must be set if chain.is_tempo() { - if let Some(fee_token) = self.tx.tempo.fee_token { + if let Some(fee_token) = self.tx.tempo.common.fee_token { deployer.tx.set_fee_token(fee_token); } else { deployer.tx.set_fee_token(DEFAULT_FEE_TOKEN); @@ -408,15 +421,18 @@ impl CreateArgs { // Apply user-provided gas, fee, nonce, and Tempo options. self.tx.apply::(&mut deployer.tx, is_legacy); - // For keychain mode, set key_id and nonce_key before gas estimation. // Convert the CREATE into an AA-compatible call entry since Tempo AA // transactions use a `calls` list instead of `to`+`input`. + if chain.is_tempo() { + deployer.tx.convert_create_to_call(); + } + + // For keychain mode, set key_id and nonce_key before gas estimation. if let Some((_, ref ak)) = tempo_keychain { deployer.tx.set_key_id(ak.key_address); if deployer.tx.nonce_key().is_none() { deployer.tx.set_nonce_key(U256::ZERO); } - deployer.tx.convert_create_to_call(); } // Fetch defaults from provider for values not specified by user. @@ -424,6 +440,20 @@ impl CreateArgs { deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?); } + maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?; + + if let Some((_, ref ak)) = tempo_keychain { + deployer + .tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } + // set access list if specified if let Some(access_list) = match self.tx.access_list { None => None, @@ -500,6 +530,11 @@ impl CreateArgs { return Ok(()); } + let tempo_sponsor = self.tx.tempo.sponsor_config().await?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut deployer.tx, deployer_address).await?; + } + // Deploy the actual contract let (deployed_contract, receipt) = if let Some(browser) = browser_signer { // Browser wallet signs and sends the transaction diff --git a/crates/forge/src/cmd/snapshot.rs b/crates/forge/src/cmd/snapshot.rs index c8dc2ba72aae1..7c6fb51ce3266 100644 --- a/crates/forge/src/cmd/snapshot.rs +++ b/crates/forge/src/cmd/snapshot.rs @@ -99,8 +99,11 @@ impl GasSnapshotArgs { } pub async fn run(mut self) -> Result<()> { - // Set fuzz seed so gas snapshots are deterministic - self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so gas snapshots are deterministic, + // but allow the user to override it via `--fuzz-seed`. + if self.test.fuzz_seed.is_none() { + self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let outcome = self.test.compile_and_run().await?; outcome.ensure_ok(false)?; diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 94376dc5238cd..dd8f3afd56197 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -3,7 +3,7 @@ use crate::{ MultiContractRunner, MultiContractRunnerBuilder, decode::decode_console_logs, gas_report::GasReport, - multi_runner::matches_artifact, + multi_runner::{MultiNetworkConfig, matches_artifact}, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ CallTraceDecoderBuilder, InternalTraceMode, TraceKind, @@ -31,7 +31,7 @@ use foundry_compilers::{ utils::source_files_iter, }; use foundry_config::{ - Config, figment, + Config, InlineConfig, figment, figment::{ Metadata, Profile, Provider, value::{Dict, Map}, @@ -39,10 +39,11 @@ use foundry_config::{ filter::GlobMatcher, }; use foundry_debugger::Debugger; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::evm::{ - BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, SpecFor, TempoEvmNetwork, - TxEnvFor, + BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor, }, opts::EvmOpts, traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth}, @@ -169,6 +170,14 @@ pub struct TestArgs { #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")] pub fuzz_runs: Option, + /// Run only the fuzz case at the given 1-based run index. + #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")] + pub fuzz_run: Option, + + /// Run the fuzz case from the given worker. Requires `--fuzz-run`. + #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")] + pub fuzz_worker: Option, + /// Timeout for each fuzz run in seconds. #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")] pub fuzz_timeout: Option, @@ -301,6 +310,10 @@ impl TestArgs { filter: &ProjectPathsAwareFilter, coverage: bool, ) -> Result { + if config.fuzz.run == Some(0) { + bail!("`fuzz.run` must be greater than 0"); + } + // Explicitly enable isolation for gas reports for more correct gas accounting. if self.gas_report { evm_opts.isolate = true; @@ -342,40 +355,80 @@ impl TestArgs { // Auto-detect network from fork chain ID when not explicitly configured. evm_opts.infer_network_from_fork().await; - // Dispatch based on network type. - let (libraries, mut outcome) = if evm_opts.networks.is_tempo() { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? - } else if evm_opts.networks.is_optimism() { - self.build_and_run_tests::( + // Parse inline config early to detect per-test network annotations. + let inline_config = InlineConfig::new_parsed(output, &config)?; + let override_networks = inline_config.referenced_override_networks(&config.profile); + + let (libraries, mut outcome) = if override_networks.is_empty() { + // Single-pass: no per-test network overrides, use global network setting. + self.dispatch_network( + &evm_opts, config, - evm_opts, + evm_opts.clone(), output, filter, coverage, should_debug, decode_internal, + MultiNetworkConfig::default(), ) .await? } else { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? + // Multi-pass: run each distinct network separately and merge results. + let all_override_networks = override_networks.clone(); + let multi_pass_timer = Instant::now(); + + // Default pass: global network, runs tests without an explicit network annotation. + let (libraries, mut outcome) = self + .dispatch_network( + &evm_opts, + config.clone(), + evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: None, + }, + ) + .await?; + + // Override passes: one per annotated network. + for &network in &override_networks { + let mut pass_evm_opts = evm_opts.clone(); + pass_evm_opts.networks = network.into(); + let (_, pass_outcome) = self + .dispatch_network( + &pass_evm_opts, + config.clone(), + pass_evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: Some(network), + }, + ) + .await?; + merge_outcomes(&mut outcome, pass_outcome); + } + + // Print the merged summary (per-pass summaries are suppressed in `run_tests_inner`). + if !self.summary && !shell::is_json() { + sh_println!("{}", outcome.summary(multi_pass_timer.elapsed()))?; + } + if self.summary && !outcome.results.is_empty() { + let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); + sh_println!("{}", &summary_report)?; + } + + (libraries, outcome) }; if should_draw { @@ -461,6 +514,7 @@ impl TestArgs { coverage: bool, should_debug: bool, decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, ) -> eyre::Result<(Libraries, TestOutcome)> { let verbosity = evm_opts.verbosity; let (evm_env, tx_env, fork_block) = @@ -476,6 +530,7 @@ impl TestArgs { .enable_isolation(evm_opts.isolate) .fail_fast(self.fail_fast) .set_coverage(coverage) + .with_multi_network(multi_network) .build::(output, evm_env, tx_env, evm_opts)?; let libraries = runner.libraries.clone(); @@ -483,6 +538,62 @@ impl TestArgs { Ok((libraries, outcome)) } + /// Dispatches `build_and_run_tests` to the correct network type based on `evm_opts.networks`. + #[allow(clippy::too_many_arguments)] + async fn dispatch_network( + &self, + dispatch_opts: &EvmOpts, + config: Config, + evm_opts: EvmOpts, + output: &ProjectCompileOutput, + filter: &ProjectPathsAwareFilter, + coverage: bool, + should_debug: bool, + decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, + ) -> eyre::Result<(Libraries, TestOutcome)> { + if dispatch_opts.networks.is_tempo() { + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } else { + #[cfg(feature = "optimism")] + if dispatch_opts.networks.is_optimism() { + return self + .build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await; + } + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } + } + /// Run all tests that matches the filter predicate from a test runner async fn run_tests_inner( &self, @@ -586,6 +697,11 @@ impl TestArgs { let libraries = runner.libraries.clone(); + // Capture multi-pass state before moving `runner` into the spawn task. + // In multi-pass mode the per-pass summary is suppressed; the merged summary is + // printed once by the caller after all passes complete. + let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty(); + // Run tests in a streaming fashion. let (tx, rx) = channel::<(String, SuiteResult)>(); let timer = Instant::now(); @@ -643,6 +759,13 @@ impl TestArgs { let tests = &mut suite_result.test_results; let has_tests = !tests.is_empty(); + // In multi-pass (per-test network override) mode, skip suites that contributed no + // tests to this pass so we don't emit a stray blank line in the suite header or + // pollute the outcome with empty entries. + if is_multi_pass && !has_tests && suite_result.warnings.is_empty() { + continue; + } + // Clear the addresses and labels from previous test. decoder.clear_addresses(); @@ -903,17 +1026,17 @@ impl TestArgs { if let Some(gas_report) = gas_report { let finalized = gas_report.finalize(); - sh_println!("{}", &finalized)?; + sh_println!("{finalized}")?; outcome.gas_report = Some(finalized); } - if !self.summary && !shell::is_json() { + if !is_multi_pass && !self.summary && !shell::is_json() { sh_println!("{}", outcome.summary(duration))?; } - if self.summary && !outcome.results.is_empty() { + if !is_multi_pass && self.summary && !outcome.results.is_empty() { let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); - sh_println!("{}", &summary_report)?; + sh_println!("{summary_report}")?; } // Reattach the task. @@ -980,6 +1103,12 @@ impl Provider for TestArgs { if let Some(fuzz_runs) = self.fuzz_runs { fuzz_dict.insert("runs".to_string(), fuzz_runs.into()); } + if let Some(fuzz_run) = self.fuzz_run { + fuzz_dict.insert("run".to_string(), fuzz_run.into()); + } + if let Some(fuzz_worker) = self.fuzz_worker { + fuzz_dict.insert("worker".to_string(), fuzz_worker.into()); + } if let Some(fuzz_timeout) = self.fuzz_timeout { fuzz_dict.insert("timeout".to_string(), fuzz_timeout.into()); } @@ -1023,6 +1152,29 @@ fn list( Ok(TestOutcome::empty(Some(runner.known_contracts), false)) } +/// Merges `other` into `base` by extending suite results. +/// +/// For suites that appear in both, test results are combined (function-level pass routing ensures +/// each function appears in exactly one pass, so there are no key conflicts in practice). +fn merge_outcomes(base: &mut TestOutcome, other: TestOutcome) { + for (suite_id, other_suite) in other.results { + match base.results.entry(suite_id) { + std::collections::btree_map::Entry::Vacant(e) => { + e.insert(other_suite); + } + std::collections::btree_map::Entry::Occupied(mut e) => { + let base_suite = e.get_mut(); + base_suite.test_results.extend(other_suite.test_results); + base_suite.warnings.extend(other_suite.warnings); + base_suite.duration = base_suite.duration.max(other_suite.duration); + } + } + } + if let Some(decoder) = other.last_run_decoder { + base.last_run_decoder = Some(decoder); + } +} + /// Load persisted filter (with last test run failures) from file. fn last_run_failures(config: &Config) -> Option { match fs::read_to_string(&config.test_failures_file) { @@ -1131,6 +1283,14 @@ mod tests { assert!(args.fuzz_seed.is_some()); } + #[test] + fn fuzz_run() { + let args: TestArgs = + TestArgs::parse_from(["foundry-cli", "--fuzz-run", "10", "--fuzz-worker", "2"]); + assert_eq!(args.fuzz_run, Some(10)); + assert_eq!(args.fuzz_worker, Some(2)); + } + #[test] fn extract_chain() { let test = |arg: &str, expected: Chain| { diff --git a/crates/forge/src/cmd/test/summary.rs b/crates/forge/src/cmd/test/summary.rs index f8a72272af53c..a0123e896d0bf 100644 --- a/crates/forge/src/cmd/test/summary.rs +++ b/crates/forge/src/cmd/test/summary.rs @@ -25,9 +25,9 @@ impl TestSummaryReport { impl Display for TestSummaryReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "{}", self.format_json_output(&self.is_detailed, &self.outcome))?; } else { - writeln!(f, "\n{}", &self.format_table_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "\n{}", self.format_table_output(&self.is_detailed, &self.outcome))?; } Ok(()) } diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index 6c93dc03b28b5..58b11d98874ed 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -146,7 +146,7 @@ impl GasReport { impl Display for GasReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output())?; + writeln!(f, "{}", self.format_json_output())?; } else { for (name, contract) in &self.contracts { if contract.functions.is_empty() { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 88bbc6156c812..675f0c3e6c99c 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -27,6 +27,7 @@ use foundry_evm::{ opts::EvmOpts, traces::{InternalTraceMode, TraceMode}, }; +use foundry_evm_networks::NetworkVariant; use foundry_linking::{LinkOutput, Linker}; use rayon::prelude::*; @@ -280,6 +281,25 @@ impl MultiContractRunner { } } +/// Tracks network assignment across a multi-network test run. +/// +/// When inline config specifies different networks for different tests, the runner performs one +/// pass per distinct network. This struct encodes which pass we're in so each `ContractRunner` +/// can skip tests that belong to a different pass. +/// +/// Default (empty `all_override_networks`, `None` pass) = single-pass mode, every test runs. +#[derive(Clone, Debug, Default)] +pub struct MultiNetworkConfig { + /// All networks explicitly referenced in inline config annotations across the whole suite. + /// Empty means single-pass mode (no per-test network overrides present). + pub all_override_networks: Vec, + /// The network this pass is responsible for. + /// `None` = default pass: runs tests *without* an explicit network annotation (or annotated + /// with a network not in `all_override_networks`). + /// `Some(v)` = override pass: runs only tests annotated with exactly `v`. + pub pass_network: Option, +} + /// Configuration for the test runner. /// /// This is modified after instantiation through inline config. @@ -311,6 +331,9 @@ pub struct TestRunnerConfig { pub isolation: bool, /// Whether to exit early on test failure or if test run interrupted. pub early_exit: EarlyExit, + + /// Multi-network pass configuration. Default = single-pass mode. + pub multi_network: MultiNetworkConfig, } impl TestRunnerConfig { @@ -423,6 +446,8 @@ pub struct MultiContractRunnerBuilder { pub isolation: bool, /// Whether to exit early on test failure. pub fail_fast: bool, + /// Multi-network pass configuration. + pub multi_network: MultiNetworkConfig, } impl MultiContractRunnerBuilder { @@ -437,6 +462,7 @@ impl MultiContractRunnerBuilder { isolation: Default::default(), decode_internal: Default::default(), fail_fast: false, + multi_network: Default::default(), } } @@ -470,6 +496,11 @@ impl MultiContractRunnerBuilder { self } + pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self { + self.multi_network = multi_network; + self + } + pub const fn fail_fast(mut self, fail_fast: bool) -> Self { self.fail_fast = fail_fast; self @@ -594,6 +625,7 @@ impl MultiContractRunnerBuilder { inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?), isolation: self.isolation, early_exit: EarlyExit::new(self.fail_fast), + multi_network: self.multi_network, config: self.config, }, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index d924c416759a2..7feaf35254636 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -109,6 +109,25 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { } } + /// Returns `true` if `func` should run in the current multi-network pass. + /// + /// In single-pass mode (no inline network overrides) every function passes. + /// In multi-pass mode: + /// - Default pass (`pass_network = None`): includes functions *without* an override annotation. + /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`. + fn function_matches_network_pass(&self, func: &Function) -> bool { + let multi = &self.mcr.tcfg.multi_network; + if multi.all_override_networks.is_empty() { + return true; + } + let profile = &self.tcfg.config.profile; + let func_network = self.mcr.inline_config.network_for(profile, self.name, &func.name); + match &multi.pass_network { + None => func_network.is_none_or(|n| !multi.all_override_networks.contains(&n)), + Some(target) => func_network.as_ref() == Some(target), + } + } + /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -380,6 +399,7 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { .abi .functions() .filter(|func| filter.matches_test_function(func)) + .filter(|func| self.function_matches_network_pass(func)) .collect::>(); debug!( "Found {} test functions out of {} in {:?}", @@ -826,7 +846,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { ); if let Some(ref progress) = progress { - progress.set_prefix(format!("{}\n{warn}\n", &func.name)); + progress.set_prefix(format!("{}\n{warn}\n", func.name)); } else { let _ = sh_warn!("{warn}"); } @@ -1052,7 +1072,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { self.cr.name, &func.name, fuzz_config.timeout, - fuzz_config.runs, + if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs }, ); let state = self.build_fuzz_state(false); diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 6e0acebc67225..5d8378c50c9ac 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3000,7 +3000,7 @@ Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] +=====================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 132459 | 396 | | | | | +| 132471 | 396 | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -3023,7 +3023,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/FallbackWithCalldataTest.sol:CounterWithFallback", "deployment": { - "gas": 132459, + "gas": 132471, "size": 396 }, "functions": { diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 0eeb3757982e9..3eebca475a781 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -577,6 +577,32 @@ forgetest_init!(can_get_evm_opts, |prj, _cmd| { } }); +// Regression test for : +// the bare `ETH_RPC_URL` env var must NOT cause `forge` commands to set +// `eth_rpc_url` (which would silently fork all `forge test` runs). +// Only `--rpc-url`, `foundry.toml`, the `FOUNDRY_ETH_RPC_URL` env var, or +// cheatcodes should configure forking. +forgetest_init!(eth_rpc_url_env_does_not_set_fork_url, |prj, _cmd| { + prj.initialize_default_contracts(); + let url = "http://127.0.0.1:8545"; + + let mut cmd = prj.forge_bin(); + cmd.arg("config") + .arg("--root") + .arg(prj.root()) + .arg("--json") + .env("ETH_RPC_URL", url) + // Make sure the figment-style env var is not set in the test environment. + .env_remove("FOUNDRY_ETH_RPC_URL"); + let output = cmd.output().unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let config: Config = serde_json::from_str(stdout.as_ref()).unwrap(); + assert_eq!( + config.eth_rpc_url, None, + "bare ETH_RPC_URL must not propagate to forge config (regression #14538)" + ); +}); + // checks that we can set various config values forgetest_init!(can_set_config_values, |prj, _cmd| { prj.initialize_default_contracts(); @@ -1269,6 +1295,8 @@ forgetest_init!(test_default_config, |prj, cmd| { "show_progress": false, "fuzz": { "runs": 256, + "run": null, + "worker": null, "fail_on_revert": true, "max_test_rejects": 65536, "seed": null, diff --git a/crates/forge/tests/cli/failure_assertions.rs b/crates/forge/tests/cli/failure_assertions.rs index 48a17c723b261..77d5a5e84cfbb 100644 --- a/crates/forge/tests/cli/failure_assertions.rs +++ b/crates/forge/tests/cli/failure_assertions.rs @@ -70,8 +70,13 @@ Suite result: FAILED. 0 passed; 7 failed; 0 skipped; [ELAPSED] .stdout_eq( r#"No files changed, compilation skipped ... +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != [..]] testShouldFailExpectRevertNestedCreateInnerAddress() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterNestedCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterTopLevelCreate() ([GAS]) [FAIL: next call did not revert as expected] testShouldFailExpectRevertsNotOnImmediateNextCall() ([GAS]) -Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +Suite result: FAILED. 0 passed; 6 failed; 0 skipped; [ELAPSED] ... "#, ); diff --git a/crates/forge/tests/cli/inline_config.rs b/crates/forge/tests/cli/inline_config.rs index 04fb2369d83b0..ba01767d58b26 100644 --- a/crates/forge/tests/cli/inline_config.rs +++ b/crates/forge/tests/cli/inline_config.rs @@ -425,3 +425,107 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); + +// Checks that tests annotated with `forge-config: default.networks.network` run on the correct +// EVM network, and that unannotated tests run on the globally configured network. +// +// Each test makes a real call to the Tempo `TipFeeManager` precompile at +// `0xfeec000000000000000000000000000000000000` (a Tempo-only contract that exists on the +// Moderato testnet and is auto-injected by the in-memory Tempo EVM): +// +// * The default-network test asserts the precompile has no code (it does not exist on Ethereum). +// * The Tempo-network test asserts the precompile has code and `userTokens(address)` returns the +// unset zero-address sentinel, proving the Tempo network was actually selected for that test and +// the Tempo genesis state was loaded. +forgetest!(per_test_network_routing, |prj, cmd| { + prj.add_test( + "inline.sol", + r#" + address constant TIP_FEE_MANAGER = 0xfeEC000000000000000000000000000000000000; + + contract DefaultNetwork { + // No annotation -> runs on the globally selected network (Ethereum by default). + // The Tempo FeeManager precompile must NOT exist here. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + } + + contract TempoNetwork { + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + // Sentinel bytecode (0xef) is injected at every Tempo precompile address. + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + // Call a Tempo-only method: `userTokens(address)` returns the user's preferred + // fee token, or the zero address when none is set. + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + + // Mixed contract: one function annotated with Tempo, one unannotated (runs on Ethereum). + contract MixedNetwork { + // No annotation -> runs on Ethereum; precompile must be absent. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + "#, + ); + + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 3 test suites [ELAPSED]: 4 tests passed, 0 failed, 0 skipped (4 total tests) + +"#]]); +}); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index fd69907be2f09..8420e24eb3df7 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -1,4 +1,7 @@ -use forge_lint::{linter::Lint, sol::med::REGISTERED_LINTS}; +use forge_lint::{ + linter::Lint, + sol::{self, SolLint}, +}; use foundry_config::{ DenyLevel, LintSeverity, LinterConfig, SolidityErrorCode, lint::LintSpecificConfig, }; @@ -203,7 +206,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -230,7 +233,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -610,7 +613,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -637,7 +640,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -665,7 +668,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect 13 │ uint256 result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift "# @@ -694,7 +697,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]).stdout_eq(str![[r#" @@ -855,7 +858,7 @@ note[unused-import]: unused imports should be removed 8 │ import { _PascalCaseInfo } from "./ContractWithLints.sol"; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import "#]]); @@ -887,7 +890,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase 6 │ uint256 public CounterB_Fail_Lint; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `counterBFailLint` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable "#]]); @@ -992,7 +995,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { ], "children": [ { - "message": "https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic", + "message": "https://getfoundry.sh/forge/linting/unwrapped-modifier-logic", "code": null, "level": "help", "spans": [], @@ -1048,7 +1051,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { "rendered": null } ], - "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" + "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" } "#]], ); @@ -1129,46 +1132,46 @@ Warning: Key `deny_warnings` is being deprecated in favor of `deny = warnings`. #[tokio::test] async fn ensure_lint_rule_docs() { - const FOUNDRY_BOOK_LINT_PAGE_URL: &str = "https://book.getfoundry.sh/forge/linting"; - - // Fetch the content of the lint reference - let content = match reqwest::get(FOUNDRY_BOOK_LINT_PAGE_URL).await { - Ok(resp) => { - assert!( - resp.status().is_success(), - "Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}). Status: {status}", - status = resp.status() - ); - match resp.text().await { - Ok(text) => text, - Err(e) => { - panic!("Failed to read response text: {e}"); - } + let client = reqwest::Client::new(); + let mut failures = Vec::new(); + + for lint in registered_lints() { + let url = lint.help(); + let response = match client.get(url).send().await { + Ok(response) => response, + Err(err) => { + failures.push(format!("{} ({url}) could not be fetched: {err}", lint.id())); + continue; } + }; + + if !response.status().is_success() { + failures.push(format!("{} ({url}) returned HTTP {}", lint.id(), response.status())); + continue; } - Err(e) => { - panic!("Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}): {e}",); - } - }; - // Ensure no missing lints - let mut missing_lints = Vec::new(); - for lint in REGISTERED_LINTS { + let content = match response.text().await { + Ok(content) => content.to_lowercase(), + Err(err) => { + failures + .push(format!("{} ({url}) response body could not be read: {err}", lint.id())); + continue; + } + }; + let selector = lint.id().to_lowercase(); let selector_with_space = selector.replace('-', " "); - if !content.to_lowercase().contains(&selector) - && !content.to_lowercase().contains(&selector_with_space) - { - missing_lints.push(lint.id()); + if !content.contains(&selector) && !content.contains(&selector_with_space) { + failures.push(format!("{} ({url}) did not mention the lint id", lint.id())); } } - if !missing_lints.is_empty() { + if !failures.is_empty() { let mut msg = String::from( - "Foundry Book lint validation failed. The following lints must be added to the docs:\n", + "Foundry Book lint validation failed. The following lint pages are missing or invalid:\n", ); - for lint in missing_lints { - msg.push_str(&format!(" - {lint}\n")); + for failure in failures { + msg.push_str(&format!(" - {failure}\n")); } msg.push_str("Please open a PR: https://github.com/foundry-rs/book"); panic!("{msg}"); @@ -1177,11 +1180,21 @@ async fn ensure_lint_rule_docs() { #[test] fn ensure_no_privileged_lint_id() { - for lint in REGISTERED_LINTS { + for lint in registered_lints() { assert_ne!(lint.id(), "all", "lint-id 'all' is reserved. Please use a different id"); } } +fn registered_lints() -> impl Iterator { + sol::high::REGISTERED_LINTS + .iter() + .chain(sol::med::REGISTERED_LINTS) + .chain(sol::low::REGISTERED_LINTS) + .chain(sol::info::REGISTERED_LINTS) + .chain(sol::gas::REGISTERED_LINTS) + .chain(sol::codesize::REGISTERED_LINTS) +} + // forgetest!(dependency_warnings_do_not_affect_lint_exit_code, |prj, cmd| { // Library with code that triggers a solc warning (unused local variable) @@ -1265,3 +1278,195 @@ contract OldContract { "# ]]); }); + +const PRAGMA_INCONSISTENT_ALPHA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Alpha {} +"#; + +const PRAGMA_INCONSISTENT_BETA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract Beta {} +"#; + +forgetest!(pragma_inconsistent_cross_file, |prj, cmd| { + prj.add_source("Alpha", PRAGMA_INCONSISTENT_ALPHA); + prj.add_source("Beta", PRAGMA_INCONSISTENT_BETA); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +const PRAGMA_EXACT_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract A {} +"#; + +const PRAGMA_EXACT_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract B {} +"#; + +const PRAGMA_EXACT_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract C {} +"#; + +const PRAGMA_CARET_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract A {} +"#; + +const PRAGMA_CARET_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract B {} +"#; + +const PRAGMA_CARET_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract C {} +"#; + +const NO_PRAGMA_C: &str = r#" +// SPDX-License-Identifier: MIT + +contract C {} +"#; + +// Multiple files all using the exact same pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_exact_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_EXACT_C); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Multiple files all using the exact same caret pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_caret_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + prj.add_source("B", PRAGMA_CARET_B); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// A single file in the project cannot conflict with itself. +forgetest!(pragma_inconsistent_single_file_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Even files that share a requirement still emit when ANY other variant exists. +// Two files with `0.8.20` plus one file with `^0.8.20` => 3 emits total. +forgetest!(pragma_inconsistent_duplicates_among_conflict, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_CARET_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +// Files without a `pragma solidity` directive must not affect the conflict computation. +// Note: `add_raw_source` is used here to bypass the helper that would otherwise inject a default +// `pragma solidity =;` for files that omit one. +forgetest!(pragma_inconsistent_files_without_pragma, |prj, cmd| { + prj.add_raw_source("A", PRAGMA_EXACT_A); + prj.add_raw_source("B", PRAGMA_CARET_B); + // C has no pragma at all; should be ignored by the cross-file check. + prj.add_raw_source("C", NO_PRAGMA_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); diff --git a/crates/forge/tests/cli/lint/geiger.rs b/crates/forge/tests/cli/lint/geiger.rs index faecfb212fb90..202866e83e35f 100644 --- a/crates/forge/tests/cli/lint/geiger.rs +++ b/crates/forge/tests/cli/lint/geiger.rs @@ -21,7 +21,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ bytes memory stuff = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:10:20 @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 10 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:11:20 @@ -100,7 +100,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 11 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 3 linter note(s) ... diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 242a0ebb4267f..031d80f0cf071 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3241,7 +3241,7 @@ contract CounterScript is Script { error: the following required arguments were not provided: --broadcast -Usage: [..] script --broadcast --verify --rpc-url [ARGS]... +Usage: [..] script --broadcast --verify --rpc-url [ARGS]... For more information, try '--help'. diff --git a/crates/forge/tests/cli/test_cmd/fuzz.rs b/crates/forge/tests/cli/test_cmd/fuzz.rs index fefefb30d9b15..454b014a6e1bc 100644 --- a/crates/forge/tests/cli/test_cmd/fuzz.rs +++ b/crates/forge/tests/cli/test_cmd/fuzz.rs @@ -1,4 +1,5 @@ use alloy_primitives::U256; +use foundry_evm::fuzz::BaseCounterExample; use foundry_test_utils::{TestCommand, forgetest_init, str}; use regex::Regex; @@ -845,6 +846,8 @@ forgetest_init!(test_fuzz_random_uint_varies_across_runs, |prj, cmd| { prj.add_test( "RandomFuzzTest.t.sol", r#" +pragma solidity >=0.8.0; + import {Test} from "forge-std/Test.sol"; contract RandomFuzzTest is Test { @@ -868,3 +871,145 @@ Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) ... "#]]); }); + +forgetest_init!(test_fuzz_run_replays_random_uint_failure, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + function testFuzz_randomUint_shouldFail(uint256) public { + uint256 rand = vm.randomUint(0, 4); + assertTrue(rand != 0, "hit value 0"); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: hit value 0; counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +"#]]; + + cmd.args(["test", "--fuzz-seed", "1", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + assert_eq!(persisted_failure.fuzz.seed, Some(U256::from(1))); + assert_eq!(persisted_failure.fuzz.worker, Some(0)); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + cmd.forge_fuse() + .args([ + "test", + "--fuzz-seed", + "1", + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure().stdout_eq(expected_output); +}); + +forgetest_init!(test_fuzz_rerun_replays_random_uint_failure_without_seed, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + error Random(uint256 value); + + function testFuzz_randomUint_shouldFail(uint256) public { + revert Random(vm.randomUint()); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: Random([..]); counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +Tip: Run `forge test --rerun` to retry only the 1 failed test + +[SEED] (use `--fuzz-seed` to reproduce) + +"#]]; + + let assert = cmd + .args(["test", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + let reason = random_failure_reason(&stdout); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + let fuzz_seed = format!("{:#x}", persisted_failure.fuzz.seed.unwrap()); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + let assert = cmd + .forge_fuse() + .args([ + "test", + "--fuzz-seed", + &fuzz_seed, + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd + .forge_fuse() + .args(["test", "--rerun", "-j1"]) + .assert_failure() + .stdout_eq(expected_output); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure(); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); +}); + +fn random_failure_reason(stdout: &str) -> String { + Regex::new(r"\[FAIL: (Random\([^)]+\))") + .unwrap() + .captures(stdout) + .unwrap_or_else(|| panic!("{stdout}"))[1] + .to_string() +} diff --git a/crates/forge/tests/cli/test_cmd/repros.rs b/crates/forge/tests/cli/test_cmd/repros.rs index 32bfe6a98a9fd..3803385b496ab 100644 --- a/crates/forge/tests/cli/test_cmd/repros.rs +++ b/crates/forge/tests/cli/test_cmd/repros.rs @@ -783,6 +783,66 @@ ParserError: Source "Missing.sol" not found: File not found. Searched the follow "#]]); }); +// https://github.com/foundry-rs/foundry/issues/10463 +forgetest_init!(issue_10463, |prj, cmd| { + prj.add_test( + "Issue10463.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract Issue10463Test is Test { + event Foo(); + + error CustomError(uint256 code); + + function revertingBefore(bool shouldRevert) external { + if (shouldRevert) revert(); + emit Foo(); + } + + function revertingWithReason() external pure { + revert("revert reason"); + } + + function revertingWithCustomError() external pure { + revert CustomError(42); + } + + function testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() public { + vm.expectEmit(); + emit Foo(); + + this.revertingBefore(true); + } + + function testExpectEmitPreservesRevertReason() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithReason(); + } + + function testExpectEmitPreservesCustomError() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithCustomError(); + } +} +"#, + ); + + cmd.arg("test").assert_failure().stdout_eq(str![[r#" +... +Ran 3 tests for test/Issue10463.t.sol:Issue10463Test +[FAIL: CustomError(42)] testExpectEmitPreservesCustomError() ([GAS]) +[FAIL: revert reason] testExpectEmitPreservesRevertReason() ([GAS]) +[FAIL: EvmError: Revert] testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() ([GAS]) +Suite result: FAILED. 0 passed; 3 failed; 0 skipped; [ELAPSED] +... +"#]]); +}); + // https://github.com/foundry-rs/foundry/issues/12803 // Test gas underflow prevention on Cancun (no EIP-7702 gas floor) forgetest_init!(issue_12803_cancun, |prj, cmd| { diff --git a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol index 7e482c6673155..838183a1b0b5e 100644 --- a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol +++ b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol @@ -233,6 +233,63 @@ contract ExpectRevertWithReverterFailureTest is DSTest { aContract.doNotRevert(); aContract.callAndRevert(); } + + // + // Regression: must fail because 0xdead is not the actual reverter when a + // top-level CREATE constructor reverts directly. + function testShouldFailExpectRevertWrongReverterTopLevelCreate() public { + vm.expectRevert(address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // even when an exact-bytes pattern is also supplied for a top-level CREATE. + function testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() public { + vm.expectRevert(abi.encodePacked("Reverted by DContract"), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // for `expectPartialRevert(bytes4, address)` against a top-level CREATE. + function testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() public { + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail when the innermost reverting frame is a nested + // CREATE and the reverter address argument does not match the would-be + // deployed address of the failed deployment. + function testShouldFailExpectRevertWrongReverterNestedCreate() public { + vm.expectRevert(address(0xdead)); + new NestedDContractCreator(); + } + + // + // Regression: documents the intended semantics for nested CREATEs — the + // matched reverter is the *outer* would-be-deployed address (the contract + // whose deployment failed), NOT the innermost reverting CREATE's address. + // Supplying the inner address must fail. + function testShouldFailExpectRevertNestedCreateInnerAddress() public { + // Outer = NestedDContractCreator at this contract's next nonce. + // Inner = DContract created from inside the outer constructor (deployer + // is the outer, nonce 1). + address outer = + vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + address inner = vm.computeCreateAddress(outer, 1); + vm.expectRevert(inner); + new NestedDContractCreator(); + } +} + +// Used by `testShouldFailExpectRevertWrongReverterNestedCreate`: a contract whose +// constructor directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } } contract ExpectRevertCountFailureTest is DSTest { diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 87864721432d9..589a0a5069e37 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -24,3 +24,7 @@ eyre.workspace = true heck.workspace = true rayon.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/lint/README.md b/crates/lint/README.md index e7c6471555da3..9d5dad0c0272e 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -17,11 +17,14 @@ It helps enforce best practices and improve code quality within Foundry projects - `divide-before-multiply`: Warns against performing division before multiplication in the same expression, which can cause precision loss. - `incorrect-erc20-interface`: Flags ERC20 interfaces and implementations with non-compliant function signatures. - `incorrect-erc721-interface`: Flags ERC721 interfaces and implementations with non-compliant function signatures. + - `tx-origin`: Flags use of `tx.origin` in authorization-like predicates. - `unsafe-typecast`: Typecasts that can truncate values should be checked. - **Low Severity:** - `block-timestamp`: Warns when `block.timestamp` is used in a comparison, as it may be manipulated by validators. + - `missing-zero-check`: Address parameter is used in a state write or value transfer without a zero-address check. - **Informational / Style Guide:** - `boolean-equal`: Boolean comparisons to constants should be simplified. + - `too-many-digits`: Numeric literals with 5+ consecutive zeros are error-prone. - `pascal-case-struct`: Flags for struct names not adhering to `PascalCase`. - `mixed-case-function`: Flags for function names not adhering to `mixedCase`. - `mixed-case-variable`: Flags for mutable variable names not adhering to `mixedCase`. @@ -31,10 +34,15 @@ It helps enforce best practices and improve code quality within Foundry projects - `unaliased-plain-import`: Use named imports `{A, B}` or alias `import ".." as X`. - `named-struct-fields`: Prefer initializing structs with named fields. - `unsafe-cheatcode`: Usage of unsafe cheatcodes that can perform dangerous operations. + - `multi-contract-file`: Prefer having only one contract, interface, or library per file. + - `interface-file-naming`: Interface file names should be prefixed with `I`. + - `interface-naming`: Interface names should be prefixed with `I`. + - `pragma-inconsistent`: Flags projects whose source files declare different Solidity pragma version requirements. - **Gas Optimizations:** - `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings. - `could-be-immutable`: Recommends declaring constructor-only state variables as `immutable`. - `custom-errors`: Recommends using custom errors instead of strings and plain reverts for potential gas savings. + - `unused-state-variables`: State variables that are never used should be removed. - **Code Size:** - `unwrapped-modifier-logic`: Recommends wrapping modifier logic to reduce contract code size. diff --git a/crates/lint/docs/README.md b/crates/lint/docs/README.md new file mode 100644 index 0000000000000..5eb4110a92e57 --- /dev/null +++ b/crates/lint/docs/README.md @@ -0,0 +1,52 @@ +# Forge lint documentation + +This directory contains one markdown file per registered `forge-lint` rule. Each file is referenced +by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the +[Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. + +## Adding a new lint + +When you add a new lint with `declare_forge_lint!`, you **must** also add a documentation file at +`crates/lint/docs/.md`. The presence of the file is enforced by the +`registered_lints_have_docs` unit test in [`crates/lint/src/sol/mod.rs`](../src/sol/mod.rs). + +Use [`_template.md`](./_template.md) as a starting point. + +## File structure + +Each lint doc file should follow this structure: + +```markdown +# + +**Severity**: `` +**ID**: `` + +A one-paragraph description of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` + +## Configuration + +Document any inline-config or `foundry.toml` options that affect this lint, if any. +``` diff --git a/crates/lint/docs/_template.md b/crates/lint/docs/_template.md new file mode 100644 index 0000000000000..41c735a0ba579 --- /dev/null +++ b/crates/lint/docs/_template.md @@ -0,0 +1,28 @@ +# + +**Severity**: `` +**ID**: `` + +One-paragraph summary of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` diff --git a/crates/lint/docs/asm-keccak256.md b/crates/lint/docs/asm-keccak256.md new file mode 100644 index 0000000000000..4678cfe9f8d12 --- /dev/null +++ b/crates/lint/docs/asm-keccak256.md @@ -0,0 +1,42 @@ +# Inefficient keccak256 call + +**Severity**: `Gas` +**ID**: `asm-keccak256` + +Flags calls to the high-level `keccak256(...)` builtin that can be cheaply rewritten with inline +assembly. + +## What it does + +Reports `keccak256(arg)` calls and (when possible) emits a fix suggestion that uses inline +assembly to compute the hash directly, avoiding the overhead of the high-level call. + +## Why is this bad? + +The high-level `keccak256` call performs additional memory management and ABI encoding compared +to a direct `keccak256(ptr, len)` opcode invocation. In hot paths the difference is visible in +gas reports. + +## Example + +### Bad + +```solidity +bytes32 h = keccak256(abi.encodePacked(a, b)); +``` + +### Good + +```solidity +bytes32 h; +assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, a) + mstore(add(m, 0x20), b) + h := keccak256(m, 0x40) +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/block-timestamp.md b/crates/lint/docs/block-timestamp.md new file mode 100644 index 0000000000000..a51b55ff5d8cc --- /dev/null +++ b/crates/lint/docs/block-timestamp.md @@ -0,0 +1,44 @@ +# Use of block.timestamp in comparisons + +**Severity**: `Low` +**ID**: `block-timestamp` + +Flags use of `block.timestamp` as an operand of a comparison, where its value can be slightly +manipulated by the block proposer. + +## What it does + +Reports any comparison expression (`<`, `<=`, `>`, `>=`, `==`, `!=`) that directly or +transitively reads `block.timestamp`. + +## Why is this bad? + +Block proposers can adjust `block.timestamp` within a small window (a few seconds). This is +usually harmless, but for short-window logic — auctions ending, randomness, time-locked +withdrawals — a few seconds of manipulation can be enough for an attacker to capture value. + +Using `block.timestamp` for general scheduling (hours/days) is fine; what's risky is fine-grained +timing and treating timestamps as a source of randomness. + +## Example + +### Bad + +```solidity +function settle() external { + require(block.timestamp >= auctionEnd, "auction ongoing"); + // ... +} +``` + +### Good + +```solidity +// Prefer block numbers for tight windows, or accept a clearly large grace period. +require(block.number >= endBlock, "auction ongoing"); +``` + +## Notes + +This lint is intentionally conservative: not every flagged comparison is exploitable. Review +each occurrence in context. diff --git a/crates/lint/docs/boolean-cst.md b/crates/lint/docs/boolean-cst.md new file mode 100644 index 0000000000000..f5c65dfec2789 --- /dev/null +++ b/crates/lint/docs/boolean-cst.md @@ -0,0 +1,37 @@ +# Misuse of a boolean constant + +**Severity**: `Med` +**ID**: `boolean-cst` + +Flags expressions where a boolean constant (`true`/`false`) is used as a control-flow condition +or operand of a boolean operator, which usually indicates dead code or a leftover debug toggle. + +## What it does + +Reports `if (true)`, `if (false)`, `while (true)` outside of intentional infinite loops, and +boolean operators (`&&`, `||`) where one side is a literal `true`/`false`. + +## Why is this bad? + +A literal boolean as a condition makes the surrounding branch dead, hides logic errors, or +preserves a forgotten debug shortcut that bypasses real checks. + +## Example + +### Bad + +```solidity +if (true) { // always taken + doSomething(); +} +require(condition && true, "unreachable"); // 'true' is redundant +``` + +### Good + +```solidity +if (condition) { + doSomething(); +} +require(condition, "..."); +``` diff --git a/crates/lint/docs/boolean-equal.md b/crates/lint/docs/boolean-equal.md new file mode 100644 index 0000000000000..9397003b039b4 --- /dev/null +++ b/crates/lint/docs/boolean-equal.md @@ -0,0 +1,34 @@ +# Boolean comparison to a constant + +**Severity**: `Info` +**ID**: `boolean-equal` + +Flags expressions of the form `x == true`, `x == false`, `x != true`, `x != false`, which can be +simplified. + +## What it does + +Reports any equality comparison between a boolean expression and a literal `true` or `false`. + +## Why is this bad? + +Comparing a boolean to a boolean literal is redundant and harms readability. Use the boolean +expression directly (or its negation). + +## Example + +### Bad + +```solidity +if (paused == true) revert(); +if (paused == false) doSomething(); +require(ok != false, "fail"); +``` + +### Good + +```solidity +if (paused) revert(); +if (!paused) doSomething(); +require(ok, "fail"); +``` diff --git a/crates/lint/docs/could-be-immutable.md b/crates/lint/docs/could-be-immutable.md new file mode 100644 index 0000000000000..bda1de6379955 --- /dev/null +++ b/crates/lint/docs/could-be-immutable.md @@ -0,0 +1,42 @@ +# State variable could be immutable + +**Severity**: `Gas` +**ID**: `could-be-immutable` + +Flags state variables that are assigned only in the constructor and never written to afterward — +making them eligible to be declared `immutable`. + +## What it does + +Reports each non-`constant`, non-`immutable` state variable whose only writes occur in the +constructor (or in initialization at declaration time). + +## Why is this bad? + +`immutable` state variables are stored in the deployed bytecode rather than in storage, eliminating +an `SLOAD` per access and saving substantial gas across the contract's lifetime. Declaring such +variables `immutable` also expresses intent and prevents future writes. + +## Example + +### Bad + +```solidity +contract C { + address owner; + constructor() { owner = msg.sender; } +} +``` + +### Good + +```solidity +contract C { + address immutable OWNER; + constructor() { OWNER = msg.sender; } +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/custom-errors.md b/crates/lint/docs/custom-errors.md new file mode 100644 index 0000000000000..9e01e01d593e9 --- /dev/null +++ b/crates/lint/docs/custom-errors.md @@ -0,0 +1,45 @@ +# Prefer custom errors over revert strings + +**Severity**: `Gas` +**ID**: `custom-errors` + +Flags `require(cond, "message")`, `revert("message")`, and `revert()` calls; suggests replacing +them with a `revert CustomError(...)`. + +## What it does + +Reports `require` calls whose second argument is a string literal, and `revert(...)` calls that +are either bare or have a string-literal argument. + +## Why is this bad? + +Custom errors: +- cost less gas than encoding/decoding a string, +- can carry typed parameters for richer diagnostics, +- shrink contract bytecode (string constants live in code). + +Solidity 0.8.4+ supports custom errors natively. + +## Example + +### Bad + +```solidity +require(amount > 0, "amount must be > 0"); +revert("not authorized"); +revert(); +``` + +### Good + +```solidity +error AmountZero(); +error NotAuthorized(); + +if (amount == 0) revert AmountZero(); +if (!authorized) revert NotAuthorized(); +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/divide-before-multiply.md b/crates/lint/docs/divide-before-multiply.md new file mode 100644 index 0000000000000..f082bef19a1bd --- /dev/null +++ b/crates/lint/docs/divide-before-multiply.md @@ -0,0 +1,32 @@ +# Divide before multiply + +**Severity**: `Med` +**ID**: `divide-before-multiply` + +Flags arithmetic expressions where division is performed before multiplication, which can cause +unintended precision loss in integer arithmetic. + +## What it does + +Warns on expressions of the form `(a / b) * c` (or equivalent shapes), where the integer division +truncates before the result is multiplied. + +## Why is this bad? + +Solidity's integer division truncates toward zero. Performing `(a / b) * c` discards the remainder +of `a / b` before scaling, while `(a * c) / b` preserves precision. This pattern frequently +manifests as fee/share/yield miscalculations. + +## Example + +### Bad + +```solidity +uint256 share = (amount / total) * weight; // truncates first, then scales +``` + +### Good + +```solidity +uint256 share = (amount * weight) / total; // preserves precision +``` diff --git a/crates/lint/docs/erc20-unchecked-transfer.md b/crates/lint/docs/erc20-unchecked-transfer.md new file mode 100644 index 0000000000000..d7d053e020cca --- /dev/null +++ b/crates/lint/docs/erc20-unchecked-transfer.md @@ -0,0 +1,43 @@ +# Unchecked ERC20 transfer return value + +**Severity**: `High` +**ID**: `erc20-unchecked-transfer` + +Flags calls to ERC20 `transfer` and `transferFrom` where the boolean return value is ignored. + +## What it does + +Warns when a function with the same signature as +`transfer(address,uint256)` or `transferFrom(address,address,uint256)` and a `bool` return type is +invoked but the result is not checked. + +## Why is this bad? + +The ERC20 spec allows tokens to signal failure by returning `false` instead of reverting. Ignoring +the return value lets a "failed" transfer go unnoticed, allowing accounting to drift and creating +common DeFi exploits. Use a wrapper such as OpenZeppelin's `SafeERC20` or check the boolean +explicitly. + +## Example + +### Bad + +```solidity +token.transfer(to, amount); +token.transferFrom(from, to, amount); +``` + +### Good + +```solidity +require(token.transfer(to, amount), "transfer failed"); +require(token.transferFrom(from, to, amount), "transferFrom failed"); + +// or use SafeERC20 +SafeERC20.safeTransfer(token, to, amount); +``` + +## Notes + +This lint can produce false positives when the callee does not strictly conform to the ERC20 +interface (e.g. tokens that revert on failure rather than returning `false`). diff --git a/crates/lint/docs/incorrect-erc20-interface.md b/crates/lint/docs/incorrect-erc20-interface.md new file mode 100644 index 0000000000000..65fb8313c205f --- /dev/null +++ b/crates/lint/docs/incorrect-erc20-interface.md @@ -0,0 +1,42 @@ +# Incorrect ERC20 interface + +**Severity**: `Med` +**ID**: `incorrect-erc20-interface` + +Flags interfaces or contracts whose function signatures match an ERC20 method by name and +parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC20 method +(`totalSupply`, `balanceOf`, `transfer`, `transferFrom`, `approve`, `allowance`), the lint checks +that the return type matches the spec. A mismatch is reported. + +## Why is this bad? + +Tokens that diverge from the ERC20 spec break composability with the wider ecosystem (DEXes, +lending protocols, multisigs) and are a common source of integration bugs and exploits. + +## Example + +### Bad + +```solidity +interface IBadERC20 { + function balanceOf(address) external view returns (bool); // should be uint256 + function transfer(address, uint256) external; // should return bool +} +``` + +### Good + +```solidity +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-erc721-interface.md b/crates/lint/docs/incorrect-erc721-interface.md new file mode 100644 index 0000000000000..4803afdde7cc1 --- /dev/null +++ b/crates/lint/docs/incorrect-erc721-interface.md @@ -0,0 +1,48 @@ +# Incorrect ERC721 interface + +**Severity**: `Med` +**ID**: `incorrect-erc721-interface` + +Flags interfaces or contracts whose function signatures match an ERC721 (or ERC165) method by +name and parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC721/ERC165 method +(`balanceOf`, `ownerOf`, `safeTransferFrom`, `transferFrom`, `approve`, `setApprovalForAll`, +`getApproved`, `isApprovedForAll`, `supportsInterface`), the lint checks that the return type +matches the spec. A mismatch is reported. + +## Why is this bad? + +Non-conforming NFT contracts break marketplaces, indexers, and any protocol that relies on the +ERC721 spec. A wrong return type often compiles and deploys silently but causes integration +failures at runtime. + +## Example + +### Bad + +```solidity +interface IBadERC721 { + function balanceOf(address) external view returns (bool); // should be uint256 + function ownerOf(uint256) external view returns (bool); // should be address + function supportsInterface(bytes4) external view returns (uint256); // should be bool +} +``` + +### Good + +```solidity +interface IERC721 { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-shift.md b/crates/lint/docs/incorrect-shift.md new file mode 100644 index 0000000000000..a9a70c7f93128 --- /dev/null +++ b/crates/lint/docs/incorrect-shift.md @@ -0,0 +1,37 @@ +# Incorrect shift order + +**Severity**: `High` +**ID**: `incorrect-shift` + +Flags shift operations where a literal appears on the left and a non-literal on the right, which +is almost always the wrong operand order. + +## What it does + +Warns when the left-hand operand of `<<` or `>>` is a numeric literal and the right-hand operand +is a non-literal expression (e.g. a variable, function call, or composite expression). + +## Why is this bad? + +Shift expressions like `2 << x` are usually a typo for `x << 2`. In the former, the *value being +shifted* is a tiny constant and the *shift amount* is dynamic — almost never the intended +behavior, and a known source of bugs in production contracts. + +## Example + +### Bad + +```solidity +result = 2 << stateValue; // shift amount comes from state +result = 8 >> localValue; // shift amount comes from a local +result = 16 << (stateValue + 1); // shift amount is a dynamic expression +``` + +### Good + +```solidity +result = stateValue << 2; +result = localValue >> 3; +result = stateValue << localShiftAmount; +result = 1 << 8; // both literals — fine +``` diff --git a/crates/lint/docs/inline-assembly.md b/crates/lint/docs/inline-assembly.md new file mode 100644 index 0000000000000..bba61148b84c5 --- /dev/null +++ b/crates/lint/docs/inline-assembly.md @@ -0,0 +1,69 @@ +# Inline assembly + +**Severity**: `Info` +**ID**: `inline-assembly` + +Flags every `assembly { ... }` block. Inline assembly bypasses many of Solidity's safety +features (type checks, overflow checks, memory layout invariants) and is a common source of +high-impact bugs, so each occurrence should be reviewed deliberately. + +## What it does + +Reports every inline assembly statement, including blocks declared with the `"evmasm"` dialect +and/or the `("memory-safe")` flag. Blocks declared as memory-safe — either via the modern +`("memory-safe")` flag or the legacy `/// @solidity memory-safe-assembly` NatSpec marker — are +still reported, but with a softer message acknowledging the developer attestation: review +focuses on business logic and side effects rather than memory layout. + +## Why is this bad? + +Assembly skips Solidity's compile-time checks and many of its runtime guarantees. Mistakes +inside an `assembly` block can corrupt memory, break the free memory pointer, leak storage, +escalate privileges via `delegatecall`, or destroy the contract via `selfdestruct`. Even when +required for gas or features unavailable in high-level Solidity, assembly should be small, +documented, and reviewed. + +## When inline assembly is reasonable + +Some idioms are widely used and generally safe: + +- Reading transaction/chain context: `chainid()`, `gas()`, `returndatasize()`. +- Probing code: `codesize()`, `extcodesize(addr)`, `extcodehash(addr)`. +- Reading the free memory pointer: `mload(0x40)`. +- Cheap hashing of a known memory layout, when paired with `("memory-safe")`. + +If you must use assembly: + +1. Keep the block minimal and well-commented. +2. Add the `("memory-safe")` flag when the block does not violate Solidity's memory model, so + the optimizer (and reviewers) can rely on it. The legacy + `/// @solidity memory-safe-assembly` NatSpec marker on the line directly above the block is + also recognized for compatibility with older codebases. +3. Suppress the lint locally to mark the block as audited: + ```solidity + // forge-lint: disable-next-line(inline-assembly) + assembly ("memory-safe") { /* reviewed: ... */ } + ``` + +## Example + +### Bad + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory) { + assembly { + let ok := call(gas(), target, 0, add(data.offset, 0), data.length, 0, 0) + // ... + } +} +``` + +### Good + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory result) { + bool ok; + (ok, result) = target.call(data); + require(ok, "call failed"); +} +``` diff --git a/crates/lint/docs/interface-file-naming.md b/crates/lint/docs/interface-file-naming.md new file mode 100644 index 0000000000000..ff72a0c175e8e --- /dev/null +++ b/crates/lint/docs/interface-file-naming.md @@ -0,0 +1,31 @@ +# Interface file naming + +**Severity**: `Info` +**ID**: `interface-file-naming` + +Flags Solidity files whose only top-level declaration is an interface but whose filename is not +prefixed with `I`. + +## What it does + +Reports interface-only files whose path basename does not start with `I` (e.g. `IERC20.sol`). + +## Why is this bad? + +Prefixing interface filenames with `I` is the prevailing convention in the Solidity ecosystem. +Following it makes import paths predictable and lets reviewers tell at a glance whether they are +looking at an interface or an implementation. + +## Example + +### Bad + +```text +contracts/Token.sol // file contains only `interface Token { ... }` +``` + +### Good + +```text +contracts/IToken.sol // file contains only `interface IToken { ... }` +``` diff --git a/crates/lint/docs/interface-naming.md b/crates/lint/docs/interface-naming.md new file mode 100644 index 0000000000000..5c6b12b946091 --- /dev/null +++ b/crates/lint/docs/interface-naming.md @@ -0,0 +1,31 @@ +# Interface name should be prefixed with 'I' + +**Severity**: `Info` +**ID**: `interface-naming` + +Flags `interface` declarations whose names are not prefixed with `I`. + +## What it does + +Reports `interface Foo` where `Foo` does not start with `I` (e.g. `IFoo`). + +## Why is this bad? + +Prefixing interfaces with `I` is the prevailing convention in Solidity codebases (`IERC20`, +`IERC721`, `IUniswapV2Pair`, ...). Following it makes the role of each type unambiguous at use +sites and aligns with the matching +[`interface-file-naming`](https://getfoundry.sh/forge/linting/interface-file-naming) lint. + +## Example + +### Bad + +```solidity +interface ERC20 { /* ... */ } +``` + +### Good + +```solidity +interface IERC20 { /* ... */ } +``` diff --git a/crates/lint/docs/missing-zero-check.md b/crates/lint/docs/missing-zero-check.md new file mode 100644 index 0000000000000..7eab1f3a00117 --- /dev/null +++ b/crates/lint/docs/missing-zero-check.md @@ -0,0 +1,39 @@ +# Missing zero-address check + +**Severity**: `Low` +**ID**: `missing-zero-check` + +Flags entry-point functions and constructors where an `address` parameter flows into a state write +or value transfer without a zero-address guard. + +## What it does + +Performs a taint analysis from each `address` parameter of an externally callable, state-mutating +function (or constructor) and reports a parameter that reaches a sink (state write, `transfer`, +`call{value: ...}`, etc.) without first being compared against `address(0)` in an `if`/`require`/ +`assert` predicate. + +## Why is this bad? + +Forgetting a zero-address check is a common source of value loss: tokens become permanently +unrecoverable, ownership is renounced unintentionally, or upgrades are bricked. Adding an explicit +guard is cheap and removes an entire class of operational mistakes. + +## Example + +### Bad + +```solidity +function setOwner(address newOwner) external onlyOwner { + owner = newOwner; // no zero-address check +} +``` + +### Good + +```solidity +function setOwner(address newOwner) external onlyOwner { + require(newOwner != address(0), "zero address"); + owner = newOwner; +} +``` diff --git a/crates/lint/docs/mixed-case-function.md b/crates/lint/docs/mixed-case-function.md new file mode 100644 index 0000000000000..9997dcb5691c7 --- /dev/null +++ b/crates/lint/docs/mixed-case-function.md @@ -0,0 +1,32 @@ +# Function names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-function` + +Flags function names that do not follow `mixedCase`. + +## What it does + +Reports functions whose names contain underscores, start with an uppercase letter, or otherwise +deviate from `mixedCase`. Test functions starting with `test`, `invariant_`, or `statefulFuzz` +and user-defined patterns (e.g. `ERC20`) are exempted. + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for function names. Consistent style makes call +sites uniform, helps editor tooling, and reduces friction in code review. + +## Example + +### Bad + +```solidity +function get_balance() external view returns (uint256); +function GetBalance() external view returns (uint256); +``` + +### Good + +```solidity +function getBalance() external view returns (uint256); +``` diff --git a/crates/lint/docs/mixed-case-variable.md b/crates/lint/docs/mixed-case-variable.md new file mode 100644 index 0000000000000..3341e1a0c48ad --- /dev/null +++ b/crates/lint/docs/mixed-case-variable.md @@ -0,0 +1,36 @@ +# Mutable variable names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-variable` + +Flags mutable variable names (locals, parameters, mutable state) that do not follow `mixedCase`. + +## What it does + +Reports mutable variable identifiers that contain underscores, start with an uppercase letter, +or otherwise deviate from `mixedCase`. + +`constant` and `immutable` state variables are not flagged by this lint — see +[`screaming-snake-case-const`](https://getfoundry.sh/forge/linting/screaming-snake-case-const) and +[`screaming-snake-case-immutable`](https://getfoundry.sh/forge/linting/screaming-snake-case-immutable). + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for mutable variables. Consistent style makes +code easier to scan and review. + +## Example + +### Bad + +```solidity +uint256 public total_supply; +address Owner; +``` + +### Good + +```solidity +uint256 public totalSupply; +address owner; +``` diff --git a/crates/lint/docs/multi-contract-file.md b/crates/lint/docs/multi-contract-file.md new file mode 100644 index 0000000000000..beabc827e4ea6 --- /dev/null +++ b/crates/lint/docs/multi-contract-file.md @@ -0,0 +1,37 @@ +# Multiple contracts in one file + +**Severity**: `Info` +**ID**: `multi-contract-file` + +Flags source files that declare more than one top-level contract, interface, or library. + +## What it does + +Reports each top-level `contract`, `interface`, or `library` definition (after the first) in a +file that contains more than one such declaration. + +## Why is this bad? + +Keeping one contract per file improves discoverability (`grep`, IDE jump-to-file), simplifies +import paths, and avoids unintentional bytecode bloat from artifacts that bundle unrelated +contracts. + +## Example + +### Bad + +```solidity +// File: Token.sol +contract TokenA { /* ... */ } +contract TokenB { /* ... */ } +``` + +### Good + +```solidity +// File: TokenA.sol +contract TokenA { /* ... */ } + +// File: TokenB.sol +contract TokenB { /* ... */ } +``` diff --git a/crates/lint/docs/named-struct-fields.md b/crates/lint/docs/named-struct-fields.md new file mode 100644 index 0000000000000..45713e2555ddc --- /dev/null +++ b/crates/lint/docs/named-struct-fields.md @@ -0,0 +1,31 @@ +# Prefer named struct fields + +**Severity**: `Info` +**ID**: `named-struct-fields` + +Flags struct construction expressions that pass fields positionally instead of by name. + +## What it does + +Reports `Struct(a, b, c)` style struct construction; suggests `Struct({ field1: a, field2: b, +field3: c })` instead. + +## Why is this bad? + +Positional struct construction is fragile: adding or reordering fields silently changes the +meaning of every existing call site. Named-field construction is self-documenting and resilient +to struct changes. + +## Example + +### Bad + +```solidity +User memory u = User(addr, 100, true); +``` + +### Good + +```solidity +User memory u = User({ wallet: addr, balance: 100, active: true }); +``` diff --git a/crates/lint/docs/pascal-case-struct.md b/crates/lint/docs/pascal-case-struct.md new file mode 100644 index 0000000000000..02a243bd56bf4 --- /dev/null +++ b/crates/lint/docs/pascal-case-struct.md @@ -0,0 +1,31 @@ +# Struct names should use PascalCase + +**Severity**: `Info` +**ID**: `pascal-case-struct` + +Flags struct definitions whose names do not follow `PascalCase`. + +## What it does + +Reports any `struct` whose identifier does not match the `PascalCase` convention. + +## Why is this bad? + +The Solidity style guide recommends `PascalCase` for type-like names (contracts, structs, +enums, libraries). Consistent casing makes code easier to scan and integrates with editor +features and external tooling. + +## Example + +### Bad + +```solidity +struct user_info { uint256 balance; } +struct USERINFO { uint256 balance; } +``` + +### Good + +```solidity +struct UserInfo { uint256 balance; } +``` diff --git a/crates/lint/docs/pragma-inconsistent.md b/crates/lint/docs/pragma-inconsistent.md new file mode 100644 index 0000000000000..095f45783773d --- /dev/null +++ b/crates/lint/docs/pragma-inconsistent.md @@ -0,0 +1,41 @@ +# Inconsistent pragma directives + +**Severity**: `Info` +**ID**: `pragma-inconsistent` + +Flags projects whose source files declare incompatible or differently-shaped Solidity version +pragmas. + +## What it does + +Inspects every `pragma solidity ...;` directive across all input source files and reports when +their version requirements are inconsistent (different exact versions, mixed caret/tilde/range +shapes, etc.). + +## Why is this bad? + +A project compiled under multiple Solidity versions can subtly change behavior between files +(e.g. checked arithmetic, default visibility, ABI encoding). Aligning pragmas across the project +removes a hidden source of integration bugs and makes upgrades coordinated. + +## Example + +### Bad + +```solidity +// A.sol +pragma solidity 0.8.18; + +// B.sol +pragma solidity ^0.8.20; + +// C.sol +pragma solidity >=0.7.0 <0.9.0; +``` + +### Good + +```solidity +// All files +pragma solidity 0.8.20; +``` diff --git a/crates/lint/docs/rtlo.md b/crates/lint/docs/rtlo.md new file mode 100644 index 0000000000000..58ce648752c6f --- /dev/null +++ b/crates/lint/docs/rtlo.md @@ -0,0 +1,32 @@ +# Right-to-left override character + +**Severity**: `High` +**ID**: `rtlo` + +Flags the presence of Unicode bidirectional override characters in source code, which can be used +to hide malicious behavior ("Trojan Source", [CVE-2021-42574](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574)). + +## What it does + +Detects the right-to-left override codepoint (`U+202E`) and other bidirectional control characters +embedded in identifiers, strings, and comments. + +## Why is this bad? + +These characters render source code in a different visual order than how the compiler reads it, +allowing an attacker to make malicious code look benign on review. Solidity contracts are public +and frequently audited visually; this attack vector must not be ignored. + +## Example + +### Bad + +```solidity +// transfer(victim‮, attacker)/* // U+202E hidden between args +``` + +### Good + +```solidity +// Avoid bidirectional override characters in code and comments. +``` diff --git a/crates/lint/docs/screaming-snake-case-const.md b/crates/lint/docs/screaming-snake-case-const.md new file mode 100644 index 0000000000000..72a16c5875fae --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-const.md @@ -0,0 +1,30 @@ +# Constants should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-const` + +Flags `constant` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `constant` whose identifier deviates from `SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for constants so they stand out from +mutable state and immutables at call sites. + +## Example + +### Bad + +```solidity +uint256 constant maxSupply = 1_000_000; +uint256 constant Max_Supply = 1_000_000; +``` + +### Good + +```solidity +uint256 constant MAX_SUPPLY = 1_000_000; +``` diff --git a/crates/lint/docs/screaming-snake-case-immutable.md b/crates/lint/docs/screaming-snake-case-immutable.md new file mode 100644 index 0000000000000..cee5590e16d27 --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-immutable.md @@ -0,0 +1,31 @@ +# Immutables should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-immutable` + +Flags `immutable` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `immutable` whose identifier deviates from +`SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for `immutable` variables so they +visually align with `constant` ones and stand out from mutable state at call sites. + +## Example + +### Bad + +```solidity +address immutable owner; +address immutable Owner; +``` + +### Good + +```solidity +address immutable OWNER; +``` diff --git a/crates/lint/docs/too-many-digits.md b/crates/lint/docs/too-many-digits.md new file mode 100644 index 0000000000000..5decb67bec9c3 --- /dev/null +++ b/crates/lint/docs/too-many-digits.md @@ -0,0 +1,32 @@ +# Numeric literal with too many digits + +**Severity**: `Info` +**ID**: `too-many-digits` + +Flags numeric literals containing five or more consecutive zeros, which are easy to misread. + +## What it does + +Reports decimal numeric literals that contain a run of 5 or more `0` characters. + +## Why is this bad? + +Long sequences of zeros are difficult to count visually, and an off-by-one zero is a common bug +(e.g. funding `1_000_000` instead of `10_000_000`). Use scientific notation, sub-denominations, or +underscore separators to make the magnitude obvious. + +## Example + +### Bad + +```solidity +uint256 amount = 1000000000000000000; +``` + +### Good + +```solidity +uint256 amount = 1e18; +uint256 amount2 = 1 ether; +uint256 amount3 = 1_000_000_000_000_000_000; +``` diff --git a/crates/lint/docs/tx-origin.md b/crates/lint/docs/tx-origin.md new file mode 100644 index 0000000000000..26877cf9c0116 --- /dev/null +++ b/crates/lint/docs/tx-origin.md @@ -0,0 +1,34 @@ +# Use of tx.origin for authorization + +**Severity**: `Med` +**ID**: `tx-origin` + +Flags use of `tx.origin` inside authorization-like predicates such as `require`, `assert`, `if`, +`while`, and `for` conditions. + +## What it does + +Reports `tx.origin` reads when they are used as part of a guard condition. Plain reads outside of +guard predicates are not reported. + +## Why is this bad? + +`tx.origin` is the original externally owned account that started the whole transaction, not the +immediate caller. If authorization checks rely on `tx.origin`, a malicious contract can call the +protected contract while the legitimate owner is the transaction origin. + +Use `msg.sender` for authorization checks instead. + +## Example + +### Bad + +```solidity +require(tx.origin == owner, "not owner"); +``` + +### Good + +```solidity +require(msg.sender == owner, "not owner"); +``` diff --git a/crates/lint/docs/unaliased-plain-import.md b/crates/lint/docs/unaliased-plain-import.md new file mode 100644 index 0000000000000..be8c5120028d6 --- /dev/null +++ b/crates/lint/docs/unaliased-plain-import.md @@ -0,0 +1,34 @@ +# Unaliased plain import + +**Severity**: `Info` +**ID**: `unaliased-plain-import` + +Flags `import "path";` statements that pull in every top-level symbol from another file without +an alias. + +## What it does + +Reports plain imports of the form `import "path";`. Suggests using either named imports +(`import { A, B } from "path"`) or an aliased import (`import "path" as X`). + +## Why is this bad? + +Plain imports pollute the importing file's namespace and make the source of each symbol +non-obvious. Named or aliased imports make the dependency surface explicit and reduce the chance +of accidental name collisions. + +## Example + +### Bad + +```solidity +import "./Lib.sol"; +``` + +### Good + +```solidity +import { Foo, Bar } from "./Lib.sol"; +// or +import "./Lib.sol" as Lib; +``` diff --git a/crates/lint/docs/unchecked-call.md b/crates/lint/docs/unchecked-call.md new file mode 100644 index 0000000000000..9a0a4143a0e0e --- /dev/null +++ b/crates/lint/docs/unchecked-call.md @@ -0,0 +1,34 @@ +# Unchecked low-level call + +**Severity**: `High` +**ID**: `unchecked-call` + +Flags low-level calls (`call`, `delegatecall`, `staticcall`, `callcode`) whose `success` return +value is ignored. + +## What it does + +Warns when the boolean returned by a low-level call is discarded — either because the return value +is not assigned or because only the `bytes memory` payload is used. + +## Why is this bad? + +Low-level calls do **not** revert when the callee fails; they silently return `false`. Ignoring +the success flag means a failed call is indistinguishable from a successful one, leading to bugs +where state is updated on the assumption that an external interaction succeeded. + +## Example + +### Bad + +```solidity +target.call(data); // success ignored +(, bytes memory ret) = target.call(data); // only payload kept +``` + +### Good + +```solidity +(bool ok, ) = target.call(data); +require(ok, "call failed"); +``` diff --git a/crates/lint/docs/unsafe-cheatcode.md b/crates/lint/docs/unsafe-cheatcode.md new file mode 100644 index 0000000000000..0aef657b0b7be --- /dev/null +++ b/crates/lint/docs/unsafe-cheatcode.md @@ -0,0 +1,35 @@ +# Usage of unsafe cheatcodes + +**Severity**: `Info` +**ID**: `unsafe-cheatcode` + +Flags use of Foundry cheatcodes that perform dangerous side effects (filesystem access, network +activity, environment variable reads, etc.) so they cannot slip into production code unnoticed. + +## What it does + +Reports calls to cheatcodes whose effects extend beyond the EVM sandbox or that bypass typical +test invariants. The flagged set follows the cheatcode's +[`Safety::Unsafe`](https://book.getfoundry.sh/cheatcodes) classification. + +## Why is this bad? + +Unsafe cheatcodes can read/write files, hit the network, or fork external state. They are +appropriate in tests with explicit intent but should not be added without review, and must +never end up in shipped contract code. + +## Example + +### Bad + +```solidity +vm.writeFile("./out.txt", data); // unsafe — writes to host filesystem +vm.envString("PRIVATE_KEY"); // unsafe — reads host environment +``` + +### Good + +```solidity +// Use safe cheatcodes (vm.expectRevert, vm.prank, vm.warp, ...) and explicit +// inputs/fixtures instead of pulling state from the host environment. +``` diff --git a/crates/lint/docs/unsafe-typecast.md b/crates/lint/docs/unsafe-typecast.md new file mode 100644 index 0000000000000..89d493eec3c3f --- /dev/null +++ b/crates/lint/docs/unsafe-typecast.md @@ -0,0 +1,40 @@ +# Unsafe typecast + +**Severity**: `Med` +**ID**: `unsafe-typecast` + +Flags explicit numeric typecasts that can silently truncate or alter the value. + +## What it does + +Reports casts where the source value's type is wider than the target type +(e.g. `uint256 → uint128`, `int256 → uint128`), unless the cast is preceded by a check that +guarantees the value fits in the target. + +## Why is this bad? + +Solidity does **not** revert on narrowing casts; it silently keeps the lowest bits, which can +cause severe accounting bugs (e.g. amount overflows, wrong fees, broken invariants). Use a checked +cast helper such as OpenZeppelin's `SafeCast` whenever the source value is not provably bounded. + +## Example + +### Bad + +```solidity +function setAmount(uint256 amount) external { + smallAmount = uint128(amount); // silent truncation if amount >= 2**128 +} +``` + +### Good + +```solidity +function setAmount(uint256 amount) external { + require(amount <= type(uint128).max, "overflow"); + smallAmount = uint128(amount); +} + +// or +smallAmount = SafeCast.toUint128(amount); +``` diff --git a/crates/lint/docs/unused-import.md b/crates/lint/docs/unused-import.md new file mode 100644 index 0000000000000..08f2545a36587 --- /dev/null +++ b/crates/lint/docs/unused-import.md @@ -0,0 +1,40 @@ +# Unused import + +**Severity**: `Info` +**ID**: `unused-import` + +Flags imported symbols (or whole import statements) whose imported names are not referenced +anywhere in the source unit. + +## What it does + +Reports `import "..."`, `import "..." as X`, and `import { A, B } from "..."` statements where one +or more imported names are never used. Symbols brought in via `import * as X` are tracked through +`X.member` accesses. + +## Why is this bad? + +Unused imports add noise, slow down compilation, can cause name collisions, and frequently +indicate dead code or stale refactors. + +## Example + +### Bad + +```solidity +import { A, B } from "./Lib.sol"; // B is never used + +contract C { + A internal a; +} +``` + +### Good + +```solidity +import { A } from "./Lib.sol"; + +contract C { + A internal a; +} +``` diff --git a/crates/lint/docs/unused-state-variables.md b/crates/lint/docs/unused-state-variables.md new file mode 100644 index 0000000000000..758c6e58b911b --- /dev/null +++ b/crates/lint/docs/unused-state-variables.md @@ -0,0 +1,39 @@ +# Unused state variable + +**Severity**: `Gas` +**ID**: `unused-state-variables` + +Flags state variables that are declared but never read or written anywhere in the contract or its +descendants. + +## What it does + +Reports each state variable that has no read or write site across the project. + +## Why is this bad? + +Unused state variables waste storage slots, inflate deployment cost, and are a strong signal of +dead or stale code that should be removed. + +## Example + +### Bad + +```solidity +contract C { + uint256 unused; // never read or written + uint256 public total; // used elsewhere +} +``` + +### Good + +```solidity +contract C { + uint256 public total; +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/unwrapped-modifier-logic.md b/crates/lint/docs/unwrapped-modifier-logic.md new file mode 100644 index 0000000000000..985c79962af07 --- /dev/null +++ b/crates/lint/docs/unwrapped-modifier-logic.md @@ -0,0 +1,51 @@ +# Unwrapped modifier logic + +**Severity**: `CodeSize` +**ID**: `unwrapped-modifier-logic` + +Flags modifiers whose body contains non-trivial logic that should be moved into a helper function +to reduce contract code size. + +## What it does + +Reports modifiers whose body contains statements other than a single placeholder, simple builtin +calls (`require`/`assert`), or a single library function call. Modifiers that use inline assembly +are exempted. + +## Why is this bad? + +Solidity inlines a modifier's body at every call site, so any non-trivial logic is duplicated +across all functions that use the modifier. Wrapping the logic in an internal function and calling +it from the modifier keeps the bytecode small while preserving behavior. + +## Example + +### Bad + +```solidity +modifier onlyAuth() { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; + _; +} +``` + +### Good + +```solidity +modifier onlyAuth() { + _checkAuth(); + _; +} + +function _checkAuth() internal { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; +} +``` + +## Notes + +This is a `CodeSize`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/src/linter/mod.rs b/crates/lint/src/linter/mod.rs index 0a5b4a40a5118..3e38a02726605 100644 --- a/crates/lint/src/linter/mod.rs +++ b/crates/lint/src/linter/mod.rs @@ -1,8 +1,10 @@ mod early; mod late; +mod project; pub use early::{EarlyLintPass, EarlyLintVisitor}; pub use late::{LateLintPass, LateLintVisitor}; +pub use project::{ProjectLintEmitter, ProjectLintPass, ProjectSource}; use foundry_common::comments::inline_config::InlineConfig; use foundry_compilers::Language; diff --git a/crates/lint/src/linter/project.rs b/crates/lint/src/linter/project.rs new file mode 100644 index 0000000000000..38fc1ad1ba59f --- /dev/null +++ b/crates/lint/src/linter/project.rs @@ -0,0 +1,92 @@ +use super::{Lint, LintContext, LinterConfig}; +use foundry_common::comments::inline_config::InlineConfig; +use foundry_config::lint::LintSpecificConfig; +use solar::{ + ast, + interface::{Session, Span, diagnostics::DiagMsg, source_map::SourceFile}, +}; +use std::{path::PathBuf, sync::Arc}; + +/// A single source unit visible to a project-wide lint pass, pre-loaded with its inline config so +/// emits respect `// forge-lint: disable-*` markers without rebuilding it per emit. +pub struct ProjectSource<'ast> { + pub path: PathBuf, + pub file: Arc, + pub ast: &'ast ast::SourceUnit<'ast>, + pub inline_config: InlineConfig>, +} + +/// Trait for lints that need to inspect every input source at once (e.g. cross-file checks). +/// +/// `check_project` runs once after all per-file [`super::EarlyLintPass`] / +/// [`super::LateLintPass`] passes have completed. +pub trait ProjectLintPass<'ast>: Send + Sync { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]); +} + +/// Helper passed to [`ProjectLintPass::check_project`] for emitting diagnostics against a specific +/// source. +pub struct ProjectLintEmitter<'s, 'c> { + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, +} + +impl<'s, 'c> ProjectLintEmitter<'s, 'c> { + pub const fn new( + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, + ) -> Self { + Self { sess, with_description, with_json_emitter, lint_specific, active_lints } + } + + /// Returns `true` if the given lint id is enabled for this run. Project passes that perform + /// expensive analysis should guard their work behind this check. + pub fn is_lint_enabled(&self, id: &'static str) -> bool { + self.active_lints.contains(&id) + } + + /// Emits a diagnostic with the lint's default description as the message. + pub fn emit<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + ) where + 'c: 'a, + { + self.build_ctx(source).emit(lint, span); + } + + /// Emits a diagnostic with a caller-provided message. + pub fn emit_with_msg<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + msg: impl Into, + ) where + 'c: 'a, + { + self.build_ctx(source).emit_with_msg(lint, span, msg); + } + + fn build_ctx<'a, 'ast>(&'a self, source: &'a ProjectSource<'ast>) -> LintContext<'s, 'a> + where + 'c: 'a, + { + LintContext::new( + self.sess, + self.with_description, + self.with_json_emitter, + LinterConfig { inline: &source.inline_config, lint_specific: self.lint_specific }, + self.active_lints.clone(), + Some(source.file.clone()), + ) + } +} diff --git a/crates/lint/src/sol/info/inline_assembly.rs b/crates/lint/src/sol/info/inline_assembly.rs new file mode 100644 index 0000000000000..1111129dada34 --- /dev/null +++ b/crates/lint/src/sol/info/inline_assembly.rs @@ -0,0 +1,71 @@ +use super::InlineAssembly; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Stmt, StmtKind}, + interface::{BytePos, Span}, +}; + +declare_forge_lint!( + INLINE_ASSEMBLY, + Severity::Info, + "inline-assembly", + "usage of inline assembly; assembly bypasses Solidity safety features and should be reviewed" +); + +const ASSEMBLY_KW_LEN: u32 = 8; +const NATSPEC_MEMORY_SAFE_MARKER: &str = "@solidity memory-safe-assembly"; + +impl<'ast> EarlyLintPass<'ast> for InlineAssembly { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + let StmtKind::Assembly(asm) = &stmt.kind else { return }; + + let kw_span = assembly_keyword_span(stmt.span); + + let memory_safe = asm.flags.iter().any(|f| f.value.as_str() == "memory-safe") + || has_memory_safe_natspec(ctx, stmt.span.lo()); + + let msg = if memory_safe { + "inline assembly (declared memory-safe); review business logic and side effects" + } else { + "inline assembly used; review for memory safety and side effects" + }; + + ctx.emit_with_msg(&INLINE_ASSEMBLY, kw_span, msg); + } +} + +/// Narrows a span to the leading `assembly` keyword to keep diagnostics readable. +fn assembly_keyword_span(span: Span) -> Span { + span.with_hi(span.lo() + BytePos(ASSEMBLY_KW_LEN)) +} + +/// Returns `true` when the lines immediately preceding `stmt_lo` form a `///` NatSpec block +/// containing `@solidity memory-safe-assembly`. +fn has_memory_safe_natspec(ctx: &LintContext, stmt_lo: BytePos) -> bool { + let Some(source_file) = ctx.source_file() else { return false }; + let src = source_file.src.as_str(); + let start_pos = source_file.start_pos.to_u32(); + let lo_abs = stmt_lo.to_u32(); + if lo_abs < start_pos { + return false; + } + let offset = (lo_abs - start_pos) as usize; + if offset > src.len() { + return false; + } + + for line in src[..offset].lines().rev() { + let trimmed = line.trim_start(); + if trimmed.is_empty() { + continue; + } + let Some(rest) = trimmed.strip_prefix("///") else { return false }; + if rest.trim_start().starts_with(NATSPEC_MEMORY_SAFE_MARKER) { + return true; + } + } + false +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index c7800a417bafc..913c5d2ea9da3 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -30,6 +30,15 @@ use multi_contract_file::MULTI_CONTRACT_FILE; mod interface_naming; use interface_naming::{INTERFACE_FILE_NAMING, INTERFACE_NAMING}; +mod too_many_digits; +use too_many_digits::TOO_MANY_DIGITS; + +mod pragma_directive; +use pragma_directive::PRAGMA_INCONSISTENT; + +mod inline_assembly; +use inline_assembly::INLINE_ASSEMBLY; + register_lints!( (BooleanCst, early, (BOOLEAN_CST)), (BooleanEqual, early, (BOOLEAN_EQUAL)), @@ -42,4 +51,7 @@ register_lints!( (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)), (MultiContractFile, early, (MULTI_CONTRACT_FILE)), (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)), + (TooManyDigits, early, (TOO_MANY_DIGITS)), + (PragmaDirective, project, (PRAGMA_INCONSISTENT)), + (InlineAssembly, early, (INLINE_ASSEMBLY)), ); diff --git a/crates/lint/src/sol/info/pragma_directive.rs b/crates/lint/src/sol/info/pragma_directive.rs new file mode 100644 index 0000000000000..b66b6bcff6ade --- /dev/null +++ b/crates/lint/src/sol/info/pragma_directive.rs @@ -0,0 +1,71 @@ +use crate::{ + linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource}, + sol::{Severity, SolLint, info::PragmaDirective}, +}; +use solar::{ast, interface::Span}; + +declare_forge_lint!( + PRAGMA_INCONSISTENT, + Severity::Info, + "pragma-inconsistent", + "inconsistent Solidity pragma version requirements across the project" +); + +impl<'ast> ProjectLintPass<'ast> for PragmaDirective { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) { + if !ctx.is_lint_enabled(PRAGMA_INCONSISTENT.id()) { + return; + } + + // Collect every `pragma solidity` directive across input sources, with its rendered + // version-requirement string for grouping. Stores source index to avoid lifetime + // invariance issues with `&ProjectSource<'ast>`. + let mut entries: Vec<(usize, Span, String)> = Vec::new(); + for (idx, source) in sources.iter().enumerate() { + for (span, req) in solidity_pragmas(source.ast) { + entries.push((idx, span, req.to_string())); + } + } + + // Stable order for snapshots and JSON output. + entries.sort_by(|a, b| { + sources[a.0].path.cmp(&sources[b.0].path).then(a.1.lo().cmp(&b.1.lo())) + }); + + // Build the distinct list once and bail if all sources agree. + let mut distinct: Vec<&str> = entries.iter().map(|(_, _, s)| s.as_str()).collect(); + distinct.sort_unstable(); + distinct.dedup(); + if distinct.len() < 2 { + return; + } + + for (idx, span, req_str) in &entries { + let others = distinct + .iter() + .filter(|v| **v != req_str.as_str()) + .copied() + .collect::>() + .join(", "); + let msg = format!( + "'pragma solidity {req_str};' conflicts with other version requirements in the project: {others}" + ); + ctx.emit_with_msg(&sources[*idx], &PRAGMA_INCONSISTENT, *span, msg); + } + } +} + +/// Yields every top-level `pragma solidity ...;` directive in `unit`. +fn solidity_pragmas<'ast>( + unit: &'ast ast::SourceUnit<'ast>, +) -> impl Iterator)> + 'ast { + unit.items.iter().filter_map(|item| match &item.kind { + ast::ItemKind::Pragma(p) => match &p.tokens { + ast::PragmaTokens::Version(ident, req) if ident.as_str() == "solidity" => { + Some((item.span, req)) + } + _ => None, + }, + _ => None, + }) +} diff --git a/crates/lint/src/sol/info/too_many_digits.rs b/crates/lint/src/sol/info/too_many_digits.rs new file mode 100644 index 0000000000000..3ba9e8abba2de --- /dev/null +++ b/crates/lint/src/sol/info/too_many_digits.rs @@ -0,0 +1,50 @@ +use super::TooManyDigits; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::ast::{Expr, ExprKind, LitKind}; + +declare_forge_lint!( + TOO_MANY_DIGITS, + Severity::Info, + "too-many-digits", + "numeric literal with many digits is error-prone; \ + use scientific notation, sub-denominations, or underscore separators" +); + +impl<'ast> EarlyLintPass<'ast> for TooManyDigits { + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + let ExprKind::Lit(lit, sub_denom) = &expr.kind else { return }; + + // Only plain integer literals. `LitKind::Address` (40-hex-digit address) is a + // distinct variant and is therefore skipped automatically. + if !matches!(lit.kind, LitKind::Number(_)) { + return; + } + + // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`. + if sub_denom.is_some() { + return; + } + + let s = lit.symbol.as_str(); + + // Skip hex literals — long zero runs in hex are usually intentional (masks, + // selectors, bit patterns) and there is no scientific-notation alternative. + if s.starts_with("0x") || s.starts_with("0X") { + return; + } + + // Skip if the user already used scientific notation (`1e18`). + if s.contains('e') || s.contains('E') { + return; + } + + // 5+ consecutive zeros in the literal as written. Underscores are + // preserved, so `1_000_000` passes while `1_000000` is flagged. + if s.contains("00000") { + ctx.emit(&TOO_MANY_DIGITS, lit.span); + } + } +} diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 00d764770374a..8540ab8b95b8f 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -9,9 +9,11 @@ /// - `$desc`: A short description of the lint. /// /// # Note -/// Each lint must have a `help` section in the foundry book. This help field is auto-generated by -/// the macro. Because of that, to ensure that new lint rules have their corresponding docs in the -/// book, the existence of the lint rule's help section is validated with a unit test. +/// Each lint must have a corresponding markdown documentation file at +/// `crates/lint/docs/.md`. The `help` URL is auto-generated by the macro and points to +/// the per-lint page on the Foundry docs site (`getfoundry.sh/forge/linting/`). To +/// ensure that new lint rules have their corresponding docs, the existence of every registered +/// lint's markdown file is validated by a unit test (see `crates/lint/src/sol/mod.rs`). #[macro_export] macro_rules! declare_forge_lint { ($id:ident, $severity:expr, $str_id:expr, $desc:expr) => { @@ -20,7 +22,7 @@ macro_rules! declare_forge_lint { id: $str_id, severity: $severity, description: $desc, - help: concat!("https://book.getfoundry.sh/reference/forge/forge-lint#", $str_id), + help: concat!("https://getfoundry.sh/forge/linting/", $str_id), }; }; } @@ -53,6 +55,7 @@ macro_rules! register_lints { register_lints!(@early_impl $pass_id, $pass_type); register_lints!(@late_impl $pass_id, $pass_type); + register_lints!(@project_impl $pass_id, $pass_type); } )* }; @@ -89,10 +92,22 @@ macro_rules! register_lints { .flatten() .collect() } + + pub fn create_project_lint_passes<'ast>() -> Vec<(Box>, &'static [SolLint])> { + [ + $( + register_lints!(@project_create $pass_id, $pass_type), + )* + ] + .into_iter() + .flatten() + .collect() + } }; // --- HELPERS ------------------------------------------------------------ (@early_impl $_pass_id:ident, late) => {}; + (@early_impl $_pass_id:ident, project) => {}; (@early_impl $pass_id:ident, $other:ident) => { pub fn as_early_lint_pass<'a>() -> Box> { Box::new(Self::default()) @@ -100,22 +115,41 @@ macro_rules! register_lints { }; (@late_impl $_pass_id:ident, early) => {}; + (@late_impl $_pass_id:ident, project) => {}; (@late_impl $pass_id:ident, $other:ident) => { pub fn as_late_lint_pass<'hir>() -> Box> { Box::new(Self::default()) } }; + (@project_impl $_pass_id:ident, early) => {}; + (@project_impl $_pass_id:ident, late) => {}; + (@project_impl $_pass_id:ident, both) => {}; + (@project_impl $pass_id:ident, $other:ident) => { + pub fn as_project_lint_pass<'ast>() -> Box> { + Box::new(Self::default()) + } + }; + (@early_create $_pass_id:ident, late) => { None }; + (@early_create $_pass_id:ident, project) => { None }; (@early_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_early_lint_pass(), $pass_id::LINTS)) }; (@late_create $_pass_id:ident, early) => { None }; + (@late_create $_pass_id:ident, project) => { None }; (@late_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_late_lint_pass(), $pass_id::LINTS)) }; + (@project_create $_pass_id:ident, early) => { None }; + (@project_create $_pass_id:ident, late) => { None }; + (@project_create $_pass_id:ident, both) => { None }; + (@project_create $pass_id:ident, $_other:ident) => { + Some(($pass_id::as_project_lint_pass(), $pass_id::LINTS)) + }; + // --- ENTRY POINT --------------------------------------------------------- ( $($tokens:tt)* ) => { register_lints! { @declare_structs $($tokens)* } diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs index ba7a09b0e9bac..2673ba23d3252 100644 --- a/crates/lint/src/sol/med/mod.rs +++ b/crates/lint/src/sol/med/mod.rs @@ -9,6 +9,9 @@ use incorrect_erc20_interface::INCORRECT_ERC20_INTERFACE; mod incorrect_erc721_interface; use incorrect_erc721_interface::INCORRECT_ERC721_INTERFACE; +mod tx_origin; +use tx_origin::TX_ORIGIN; + mod unsafe_typecast; use unsafe_typecast::UNSAFE_TYPECAST; @@ -16,5 +19,6 @@ register_lints!( (DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)), (IncorrectERC20Interface, late, (INCORRECT_ERC20_INTERFACE)), (IncorrectERC721Interface, late, (INCORRECT_ERC721_INTERFACE)), + (TxOrigin, early, (TX_ORIGIN)), (UnsafeTypecast, late, (UNSAFE_TYPECAST)) ); diff --git a/crates/lint/src/sol/med/tx_origin.rs b/crates/lint/src/sol/med/tx_origin.rs new file mode 100644 index 0000000000000..00ff5f939ebfb --- /dev/null +++ b/crates/lint/src/sol/med/tx_origin.rs @@ -0,0 +1,101 @@ +use super::TxOrigin; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Expr, ExprKind, IndexKind, Stmt, StmtKind}, + interface::SpannedOption, +}; + +declare_forge_lint!( + TX_ORIGIN, + Severity::Med, + "tx-origin", + "`tx.origin` should not be used for authorization" +); + +impl<'ast> EarlyLintPass<'ast> for TxOrigin { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + match &stmt.kind { + StmtKind::If(cond, ..) | StmtKind::DoWhile(_, cond) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::While(cond, _) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::For { cond: Some(cond), .. } => { + emit_if_contains_tx_origin(ctx, cond); + } + _ => {} + } + } + + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + if let ExprKind::Call(callee, args) = &expr.kind + && is_require_or_assert_call(callee) + && let Some(cond) = args.exprs().next() + { + emit_if_contains_tx_origin(ctx, cond); + } + } +} + +fn emit_if_contains_tx_origin(ctx: &LintContext, expr: &Expr<'_>) { + if contains_tx_origin(expr) { + ctx.emit(&TX_ORIGIN, expr.span); + } +} + +fn contains_tx_origin(expr: &Expr<'_>) -> bool { + if is_tx_origin(expr) { + return true; + } + match &expr.kind { + ExprKind::Unary(_, inner) => contains_tx_origin(inner), + ExprKind::Binary(lhs, _, rhs) => contains_tx_origin(lhs) || contains_tx_origin(rhs), + ExprKind::Index(base, index) => { + contains_tx_origin(base) + || match index { + IndexKind::Index(Some(index)) => contains_tx_origin(index), + IndexKind::Range(start, end) => { + start.as_ref().is_some_and(|start| contains_tx_origin(start)) + || end.as_ref().is_some_and(|end| contains_tx_origin(end)) + } + _ => false, + } + } + ExprKind::Tuple(elems) => elems.iter().any(|elem| { + if let SpannedOption::Some(inner) = elem.as_ref() { + contains_tx_origin(inner) + } else { + false + } + }), + ExprKind::Call(callee, args) => { + contains_tx_origin(callee) || args.exprs().any(contains_tx_origin) + } + ExprKind::Ternary(cond, then_expr, else_expr) => { + contains_tx_origin(cond) + || contains_tx_origin(then_expr) + || contains_tx_origin(else_expr) + } + _ => false, + } +} + +fn is_tx_origin(expr: &Expr<'_>) -> bool { + matches!( + &expr.kind, + ExprKind::Member(base, member) + if member.as_str() == "origin" + && matches!(&base.kind, ExprKind::Ident(ident) if ident.as_str() == "tx") + ) +} + +fn is_require_or_assert_call(callee: &Expr<'_>) -> bool { + matches!( + &callee.kind, + ExprKind::Ident(ident) if matches!(ident.as_str(), "require" | "assert") + ) +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 1f7c515585fb4..7ae073f2ea20b 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,6 +1,6 @@ use crate::linter::{ EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter, - LinterConfig, + LinterConfig, ProjectLintEmitter, ProjectLintPass, ProjectSource, }; use foundry_common::{ comments::{ @@ -179,6 +179,62 @@ impl<'a> SolidityLinter<'a> { Ok(()) } + /// Runs all enabled project-wide lint passes against the given input sources. + fn process_project<'gcx>(&self, gcx: Gcx<'gcx>, input: &[PathBuf]) { + // Gather enabled project passes from every severity bucket. + let mut passes_and_lints: Vec<(Box>, &'static [SolLint])> = + Vec::new(); + passes_and_lints.extend(high::create_project_lint_passes()); + passes_and_lints.extend(med::create_project_lint_passes()); + passes_and_lints.extend(low::create_project_lint_passes()); + passes_and_lints.extend(info::create_project_lint_passes()); + passes_and_lints.extend(gas::create_project_lint_passes()); + passes_and_lints.extend(codesize::create_project_lint_passes()); + + let (mut passes, lint_ids): (Vec>>, Vec<_>) = passes_and_lints + .into_iter() + .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| { + let included: Vec<_> = lints + .iter() + .filter_map(|lint| self.include_lint(*lint).then_some(lint.id)) + .collect(); + if !included.is_empty() { + passes.push(pass); + ids.extend(included); + } + (passes, ids) + }); + + if passes.is_empty() { + return; + } + + // Pre-load every input source with its inline config, in input order. + let sources: Vec> = input + .iter() + .filter_map(|path| { + let path = self.path_config.root.join(path); + let (_, source) = gcx.get_ast_source(&path)?; + let ast = source.ast.as_ref()?; + let comments = + Comments::new(&source.file, gcx.sess.source_map(), false, false, None); + let inline_config = parse_inline_config(gcx.sess, &comments, ast); + Some(ProjectSource { path, file: source.file.clone(), ast, inline_config }) + }) + .collect(); + + let emitter = ProjectLintEmitter::new( + gcx.sess, + self.with_description, + self.with_json_emitter, + self.lint_specific, + lint_ids, + ); + for pass in &mut passes { + pass.check_project(&emitter, &sources); + } + } + fn process_source_hir<'gcx>( &self, gcx: Gcx<'gcx>, @@ -314,6 +370,9 @@ impl<'a> Linter for SolidityLinter<'a> { ); }); + // Project-wide lints, run once after all per-file passes. + self.process_project(gcx, input); + convert_solar_errors(compiler.dcx()) })?; @@ -453,3 +512,75 @@ impl<'a> TryFrom<&'a str> for SolLint { Err(SolLintError::InvalidId(value.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every registered lint must have a markdown documentation file at + /// `crates/lint/docs/.md`. This test enforces that contract so that the `help` URL + /// generated by `declare_forge_lint!` always resolves to real documentation. + /// + /// When this test fails, add a new file at `crates/lint/docs/.md` describing the + /// lint. See [`crates/lint/docs/_template.md`](../../docs/_template.md) for the expected + /// structure. + #[test] + fn registered_lints_have_docs() { + let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs"); + assert!(docs_dir.is_dir(), "missing docs directory at {}", docs_dir.display()); + + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + let mut missing: Vec<&'static str> = Vec::new(); + let mut empty: Vec<&'static str> = Vec::new(); + for lint in &all_lints { + let path = docs_dir.join(format!("{}.md", lint.id())); + match std::fs::read_to_string(&path) { + Ok(content) => { + // Basic sanity: file should be non-trivial and reference the lint id. + if content.trim().is_empty() || !content.contains(lint.id()) { + empty.push(lint.id()); + } + } + Err(_) => missing.push(lint.id()), + } + } + + assert!( + missing.is_empty(), + "the following registered lints are missing a docs file at \ + `crates/lint/docs/.md`: {missing:?}\n\ + See `crates/lint/docs/_template.md` for the expected structure." + ); + assert!( + empty.is_empty(), + "the following lint docs files are empty or do not reference the lint id: {empty:?}" + ); + } + + /// The auto-generated `help` URL must point at the canonical Foundry docs site so that the + /// link printed in diagnostics resolves correctly. + #[test] + fn registered_lints_have_canonical_help_url() { + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + for lint in all_lints { + let expected = format!("https://getfoundry.sh/forge/linting/{}", lint.id()); + assert_eq!(lint.help(), expected, "lint `{}` has a non-canonical help URL", lint.id()); + } + } +} diff --git a/crates/lint/testdata/BlockTimestamp.stderr b/crates/lint/testdata/BlockTimestamp.stderr index 016f8fa2bdb2d..62ab588ae7340 100644 --- a/crates/lint/testdata/BlockTimestamp.stderr +++ b/crates/lint/testdata/BlockTimestamp.stderr @@ -4,7 +4,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -12,7 +12,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp == 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -20,7 +20,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp != 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -28,7 +28,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp <= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -36,7 +36,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp >= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -44,7 +44,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp < deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -52,7 +52,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return deadline > block.timestamp; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -60,7 +60,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp + 1 > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -68,7 +68,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return (block.timestamp / 3600) == 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -76,7 +76,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ require(block.timestamp > deadline); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -84,7 +84,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ if (block.timestamp > deadline) { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -92,5 +92,5 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return foo(block.timestamp) > 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp diff --git a/crates/lint/testdata/BooleanCst.stderr b/crates/lint/testdata/BooleanCst.stderr index 53b89fcb11735..75fdb0b57cea7 100644 --- a/crates/lint/testdata/BooleanCst.stderr +++ b/crates/lint/testdata/BooleanCst.stderr @@ -4,7 +4,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (false) {} │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -12,7 +12,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag || true) {} │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -20,7 +20,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag ? true : false) {} │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -28,7 +28,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag ? true : false) {} │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -36,5 +36,5 @@ warning[boolean-cst]: misuse of a boolean constant LL │ return assigned && false; │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst diff --git a/crates/lint/testdata/BooleanEqual.stderr b/crates/lint/testdata/BooleanEqual.stderr index 11749698f5714..590a85b806fcf 100644 --- a/crates/lint/testdata/BooleanEqual.stderr +++ b/crates/lint/testdata/BooleanEqual.stderr @@ -4,7 +4,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (enabled == true) {} │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -12,7 +12,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (paused == false) {} │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `!paused` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -20,7 +20,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (true != ready) {} │ ━━━━━━━━━━━━━ help: consider simplifying to: `!ready` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -28,7 +28,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ while (done != false) { │ ━━━━━━━━━━━━━ help: consider simplifying to: `done` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -36,7 +36,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ for (; enabled == true && paused != false;) { │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -44,7 +44,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ for (; enabled == true && paused != false;) { │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `paused` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -52,5 +52,5 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ return enabled == true; │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal diff --git a/crates/lint/testdata/CouldBeImmutable.stderr b/crates/lint/testdata/CouldBeImmutable.stderr index 2858b2311cd95..170682baf89d3 100644 --- a/crates/lint/testdata/CouldBeImmutable.stderr +++ b/crates/lint/testdata/CouldBeImmutable.stderr @@ -4,7 +4,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address public owner; │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -12,7 +12,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address public deployer = msg.sender; │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -20,7 +20,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 private configured; │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -28,7 +28,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ bytes32 internal salt = keccak256(abi.encodePacked(block.timestamp)); │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -36,7 +36,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ CouldBeImmutable private peer; │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -44,7 +44,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 internal inheritedBase; │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -52,5 +52,5 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 internal baseConfigured; │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable diff --git a/crates/lint/testdata/CustomErrors.stderr b/crates/lint/testdata/CustomErrors.stderr index 66b3c11bc183c..286a649aee269 100644 --- a/crates/lint/testdata/CustomErrors.stderr +++ b/crates/lint/testdata/CustomErrors.stderr @@ -4,7 +4,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ require(a > 0, "Value must be greater than zero"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -12,7 +12,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ … require(a >= 0 && a <= 100 || b == 50, "Complex condition should be linted"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -20,7 +20,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert("Something went wrong"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -28,7 +28,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(""); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -36,5 +36,5 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors diff --git a/crates/lint/testdata/DivideBeforeMultiply.stderr b/crates/lint/testdata/DivideBeforeMultiply.stderr index c0e5ef78e2e1c..95022f65db874 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.stderr +++ b/crates/lint/testdata/DivideBeforeMultiply.stderr @@ -4,7 +4,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -12,7 +12,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 / 2) * 3) * 4; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -20,7 +20,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 * 2) / 3) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -28,7 +28,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2 / 3) * 4; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -36,7 +36,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / (2 + 3)) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -44,5 +44,5 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ 1 / ((2 / 3) * 3); │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply diff --git a/crates/lint/testdata/Imports.stderr b/crates/lint/testdata/Imports.stderr index 8fa9800b27ded..1031f4f6f8ca0 100644 --- a/crates/lint/testdata/Imports.stderr +++ b/crates/lint/testdata/Imports.stderr @@ -4,7 +4,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsSomeFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." as X' ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -12,7 +12,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsAnotherFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -20,7 +20,7 @@ note[unused-import]: unused imports should be removed LL │ symbol2 as notUsed, │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -28,7 +28,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol, │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -36,7 +36,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol2, │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -44,7 +44,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbolWrongTag, │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -52,7 +52,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed, │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -60,7 +60,7 @@ note[unused-import]: unused imports should be removed LL │ IContractNotUsed │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -68,7 +68,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed3 │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -76,7 +76,7 @@ note[unused-import]: unused imports should be removed LL │ CONSTANT_1 │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -84,7 +84,7 @@ note[unused-import]: unused imports should be removed LL │ YetAnotherType │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -92,7 +92,7 @@ note[unused-import]: unused imports should be removed LL │ import "./auxiliary/ImportsAnotherFile2.sol" as AnotherFile2; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -100,5 +100,5 @@ note[unused-import]: unused imports should be removed LL │ import * as OtherUtils from "./auxiliary/ImportsUtils2.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import diff --git a/crates/lint/testdata/IncorrectERC20Interface.stderr b/crates/lint/testdata/IncorrectERC20Interface.stderr index 3bb60ecce8320..33e2f1ca27d22 100644 --- a/crates/lint/testdata/IncorrectERC20Interface.stderr +++ b/crates/lint/testdata/IncorrectERC20Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 {} │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Incorrect is IERC20 { │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Correct is IERC20 { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20NamedCorrect { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC20 { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transferFrom(address from, address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function allowance(address owner, address spender) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function balanceOf(address account) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -116,5 +116,5 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function totalSupply() external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface diff --git a/crates/lint/testdata/IncorrectERC721Interface.stderr b/crates/lint/testdata/IncorrectERC721Interface.stderr index a88db93e39b10..2e68084c1cec1 100644 --- a/crates/lint/testdata/IncorrectERC721Interface.stderr +++ b/crates/lint/testdata/IncorrectERC721Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721 {} │ ━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Incorrect is IERC721 { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Correct is IERC721 { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721NamedCorrect { │ ━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC721 { │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function transferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -116,7 +116,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function approve(address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -124,7 +124,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function setApprovalForAll(address operator, bool approved) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -132,7 +132,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function getApproved(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -140,7 +140,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function isApprovedForAll(address owner, address operator) external view returns (address); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -148,5 +148,5 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function supportsInterface(bytes4 interfaceId) external view returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface diff --git a/crates/lint/testdata/IncorrectShift.stderr b/crates/lint/testdata/IncorrectShift.stderr index bce84c98df432..dfff32db897bb 100644 --- a/crates/lint/testdata/IncorrectShift.stderr +++ b/crates/lint/testdata/IncorrectShift.stderr @@ -4,7 +4,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 2 << stateValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -12,7 +12,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -20,7 +20,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 16 << (stateValue + 1); │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -28,7 +28,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 32 >> getAmount(); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -36,5 +36,5 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ … result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift diff --git a/crates/lint/testdata/InlineAssembly.sol b/crates/lint/testdata/InlineAssembly.sol new file mode 100644 index 0000000000000..05917ea22784c --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.sol @@ -0,0 +1,110 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract InlineAssembly { + function bare() public view returns (uint256 id) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + id := chainid() + } + } + + function withMemorySafe() public view returns (uint256 size) { + assembly ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + size := extcodesize(address()) + } + } + + function withDialectAndMemorySafe() public view returns (uint256 ptr) { + assembly "evmasm" ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + ptr := mload(0x40) + } + } + + function withNatspecMemorySafe() public view returns (uint256 v) { + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := chainid() + } + } + + function withNatspecMemorySafeAndOtherDocs() public view returns (uint256 v) { + /// @notice does a thing + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := gas() + } + } + + function plainCommentDoesNotCount() public view returns (uint256 v) { + // solidity memory-safe-assembly + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := chainid() + } + } + + function nestedInControlFlow(bool flag) public view returns (uint256 v) { + if (flag) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := gas() + } + } + + for (uint256 i = 0; i < 1; ++i) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInUnchecked(uint256 x) public pure returns (uint256 v) { + unchecked { + v = x + 1; + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInTryCatch() public returns (uint256 v) { + try this.bare() returns (uint256) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 1 + } + } catch { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 2 + } + } + } + + function suppressed() public view returns (uint256 id) { + // forge-lint: disable-next-line(inline-assembly) + assembly { + id := chainid() + } + } + + modifier guarded() { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + if iszero(caller()) { revert(0, 0) } + } + _; + } + + function suppressedRegion() public view returns (uint256 a, uint256 b) { + // forge-lint: disable-start(inline-assembly) + assembly { + a := chainid() + } + assembly ("memory-safe") { + b := gas() + } + // forge-lint: disable-end(inline-assembly) + } + + function noAssembly() public pure returns (uint256) { + return 42; + } +} diff --git a/crates/lint/testdata/InlineAssembly.stderr b/crates/lint/testdata/InlineAssembly.stderr new file mode 100644 index 0000000000000..12f8bcbacd14e --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.stderr @@ -0,0 +1,96 @@ +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly "evmasm" ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index 41f856336b5b3..2457aed96d601 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -52,6 +52,7 @@ contract AsmKeccak256 { function assemblyHash(uint256 a, uint256 b) public pure returns (bytes32) { //optimized + // forge-lint: disable-next-line(inline-assembly) assembly { mstore(0x00, a) mstore(0x20, b) diff --git a/crates/lint/testdata/Keccak256.stderr b/crates/lint/testdata/Keccak256.stderr index 4203d950d9cba..a81e429e389a1 100644 --- a/crates/lint/testdata/Keccak256.stderr +++ b/crates/lint/testdata/Keccak256.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `mixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Another_MixedCase = 2; │ ━━━━━━━━━━━━━━━━━ help: consider using: `anotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 YetAnother_MixedCase = 3; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `yetAnotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract AsmKeccak256 { │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract OtherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -60,7 +60,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract YetAnotherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -68,7 +68,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 hash = keccak256(abi.encodePacked(a, b, bytes32(bytes20(c)))); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -76,7 +76,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 afterDisabledBlock = keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -84,7 +84,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromCalldata = keccak256(z); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -92,7 +92,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromMemory = keccak256(y); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -100,7 +100,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 lintWithoutFix = keccak256(abi.encodePacked(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -108,7 +108,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -116,7 +116,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 Enabled_MixedCase_Variable; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -124,7 +124,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesNotUseScratchSpace = keccak256(abi.encode(x, y, x, y, x, y)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -132,7 +132,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesUseScratchSpace = keccak256(abi.encode(x)); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -140,5 +140,5 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(doesUseScratchSpace, doesNotUseScratchSpace)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 diff --git a/crates/lint/testdata/MissingZeroCheck.stderr b/crates/lint/testdata/MissingZeroCheck.stderr index b55a902547fcf..81a9179e79c94 100644 --- a/crates/lint/testdata/MissingZeroCheck.stderr +++ b/crates/lint/testdata/MissingZeroCheck.stderr @@ -4,7 +4,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwner(address newOwner) external { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -12,7 +12,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ constructor(address initialOwner) { │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -20,7 +20,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function pay(address payable to) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -28,7 +28,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function lowLevel(address payable to, bytes calldata data) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -36,7 +36,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function withUselessModifier(address a) external doesNothing(a) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -44,7 +44,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaAlias(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -52,7 +52,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaReassign(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -60,7 +60,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaCast(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -68,7 +68,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payViaAlias(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -76,7 +76,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function mixedParams(address a, address b) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -84,7 +84,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function bothSinks(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -92,7 +92,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function ternaryAlias(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -100,7 +100,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payableWrap(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -108,7 +108,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function modifierWithExpr(address a) external nonZero(addrIdentity(a)) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -116,7 +116,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function delegateCallSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -124,7 +124,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkStmt(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -132,7 +132,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkDecl(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -140,7 +140,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function multiHopTaint(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -148,7 +148,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardAfterSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -156,7 +156,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardOnOneBranch(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -164,7 +164,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInForLoop(address a, uint256 n) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -172,7 +172,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInWhileLoop(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -180,5 +180,5 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInTryClause(address a, address payable target) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check diff --git a/crates/lint/testdata/MixedCase.stderr b/crates/lint/testdata/MixedCase.stderr index d290af5cdb5a8..2db30559ba5a6 100644 --- a/crates/lint/testdata/MixedCase.stderr +++ b/crates/lint/testdata/MixedCase.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Variablemixedcase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variablemixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VARIABLE_MIXED_CASE; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VariableMixedCase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 testVAL; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TestVal; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -44,7 +44,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TESTVAL; │ ━━━━━━━ help: consider using: `testval` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -52,7 +52,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function Functionmixedcase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionmixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -60,7 +60,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FUNCTION_MIXED_CASE() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -68,7 +68,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FunctionMixedCase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -76,7 +76,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function function_mixed_case() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -84,7 +84,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantBalance_MixedCase_Enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantBalanceMixedCaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -92,7 +92,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantbalance_mixedcase_enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantbalanceMixedcaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -100,7 +100,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function ERC20_DoSomething() public {} // invalid because of the underscore │ ━━━━━━━━━━━━━━━━━ help: consider using: `erc20DoSomething` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -108,7 +108,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_PARAMS(address addr) external view returns (uint256) {} │ ━━━━━━━━━━ help: consider using: `hasParams` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -116,7 +116,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_NO_RETURN() external view {} │ ━━━━━━━━━━━━━ help: consider using: `hasNoReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -124,7 +124,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_MORE_THAN_ONE_RETURN() external view returns (uint256, uint256) {} │ ━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `hasMoreThanOneReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -132,7 +132,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function NOT_ELEMENTARY_RETURN() external view returns (uint256[] memory) {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `notElementaryReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -140,7 +140,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -148,5 +148,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract MixedCaseTest { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile.stderr b/crates/lint/testdata/MultiContractFile.stderr index c6e4e32a2df55..e25f3d72ad01a 100644 --- a/crates/lint/testdata/MultiContractFile.stderr +++ b/crates/lint/testdata/MultiContractFile.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract A {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract B {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -36,5 +36,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr index 41fc439ea7d1b..1912f16863712 100644 --- a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr +++ b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -20,5 +20,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/NamedStructFields.stderr b/crates/lint/testdata/NamedStructFields.stderr index 6ee2160791cd2..cfb35637176bd 100644 --- a/crates/lint/testdata/NamedStructFields.stderr +++ b/crates/lint/testdata/NamedStructFields.stderr @@ -4,5 +4,5 @@ note[named-struct-fields]: prefer initializing structs with named fields LL │ Person memory person = Person("Alice", 25, address(0)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using named fields: `Person({ name: "Alice", age: 25, wallet: address(0) })` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#named-struct-fields + ╰ help: https://getfoundry.sh/forge/linting/named-struct-fields diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol new file mode 100644 index 0000000000000..bfc993baab794 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 +pragma solidity 0.8.18; //~NOTE: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr new file mode 100644 index 0000000000000..c2c967dee792f --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.18; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol new file mode 100644 index 0000000000000..75bc17988accc --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr new file mode 100644 index 0000000000000..f60361718ba9b --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol new file mode 100644 index 0000000000000..37b06040c33a6 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 +pragma solidity ~0.8.20; //~NOTE: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr new file mode 100644 index 0000000000000..6c46f2478208d --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ~0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.sol b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol new file mode 100644 index 0000000000000..f85a477cc8744 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20 || 0.8.21; //~NOTE: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr new file mode 100644 index 0000000000000..acf6bd7c2d6e0 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20 || 0.8.21; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol new file mode 100644 index 0000000000000..d8fcb7a0eb4b1 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; //~NOTE: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr new file mode 100644 index 0000000000000..5ac221b924c9a --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0 <0.9.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol new file mode 100644 index 0000000000000..fe208e15efb63 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol @@ -0,0 +1,8 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; //~NOTE: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 +pragma solidity ~0.8.0; //~NOTE: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr new file mode 100644 index 0000000000000..e1e5ad7333fb2 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr @@ -0,0 +1,24 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ~0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/Rtlo.stderr b/crates/lint/testdata/Rtlo.stderr index 2c2b53df646e1..93f5bb191532f 100644 --- a/crates/lint/testdata/Rtlo.stderr +++ b/crates/lint/testdata/Rtlo.stderr @@ -4,7 +4,7 @@ warning[rtlo]: U+202A (Left-to-Right Embedding) detected LL │ string public lre = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -12,7 +12,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public lre = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202B (Right-to-Left Embedding) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -20,7 +20,7 @@ warning[rtlo]: U+202B (Right-to-Left Embedding) detected LL │ string public rle = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -28,7 +28,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public rle = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202A (Left-to-Right Embedding) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -36,7 +36,7 @@ warning[rtlo]: U+202A (Left-to-Right Embedding) detected LL │ string public pdf = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -44,7 +44,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public pdf = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202D (Left-to-Right Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -52,7 +52,7 @@ warning[rtlo]: U+202D (Left-to-Right Override) detected LL │ string public lro = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -60,7 +60,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public lro = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -68,7 +68,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ string public rlo = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -76,7 +76,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public rlo = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2066 (Left-to-Right Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -84,7 +84,7 @@ warning[rtlo]: U+2066 (Left-to-Right Isolate) detected LL │ string public lri = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -92,7 +92,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public lri = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2067 (Right-to-Left Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -100,7 +100,7 @@ warning[rtlo]: U+2067 (Right-to-Left Isolate) detected LL │ string public rli = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -108,7 +108,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public rli = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2068 (First Strong Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -116,7 +116,7 @@ warning[rtlo]: U+2068 (First Strong Isolate) detected LL │ string public fsi = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -124,7 +124,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public fsi = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2066 (Left-to-Right Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -132,7 +132,7 @@ warning[rtlo]: U+2066 (Left-to-Right Isolate) detected LL │ string public pdi = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -140,7 +140,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public pdi = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -148,7 +148,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ /* hidden� /* text � */ uint256 inBlockComment; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -156,7 +156,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ /* hidden� /* text � */ uint256 inBlockComment; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -164,7 +164,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ // sneaky� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -172,7 +172,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ // sneaky� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+200E (Left-to-Right Mark) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -180,7 +180,7 @@ warning[rtlo]: U+200E (Left-to-Right Mark) detected LL │ string public marks = unicode"left‎right‏end"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+200F (Right-to-Left Mark) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -188,5 +188,5 @@ warning[rtlo]: U+200F (Right-to-Left Mark) detected LL │ string public marks = unicode"left‎right‏end"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo diff --git a/crates/lint/testdata/RtloCommentsOnly.stderr b/crates/lint/testdata/RtloCommentsOnly.stderr index 88a354432867e..5a7ec9ee6e69d 100644 --- a/crates/lint/testdata/RtloCommentsOnly.stderr +++ b/crates/lint/testdata/RtloCommentsOnly.stderr @@ -4,7 +4,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ // hidden� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -12,7 +12,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ // hidden� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -20,7 +20,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ /* block� comment � end */ │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -28,5 +28,5 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ /* block� comment � end */ │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo diff --git a/crates/lint/testdata/ScreamingSnakeCase.stderr b/crates/lint/testdata/ScreamingSnakeCase.stderr index a740506ed74d8..36305bb268d9a 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.stderr +++ b/crates/lint/testdata/ScreamingSnakeCase.stderr @@ -4,7 +4,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -12,7 +12,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screaming_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -20,7 +20,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant ScreamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -28,7 +28,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant SCREAMING_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -36,7 +36,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -44,7 +44,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screaming_snake_case0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -52,7 +52,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable ScreamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -60,5 +60,5 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable SCREAMING_snake_case_0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE_0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable diff --git a/crates/lint/testdata/StructPascalCase.stderr b/crates/lint/testdata/StructPascalCase.stderr index 1c7bfa13ba84b..255c1c4d5d74b 100644 --- a/crates/lint/testdata/StructPascalCase.stderr +++ b/crates/lint/testdata/StructPascalCase.stderr @@ -4,7 +4,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct _PascalCase { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -12,7 +12,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalCase { │ ━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -20,7 +20,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalcase { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -28,7 +28,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascal_case { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -36,7 +36,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCAL_CASE { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -44,5 +44,5 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCALCASE { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct diff --git a/crates/lint/testdata/TooManyDigits.sol b/crates/lint/testdata/TooManyDigits.sol new file mode 100644 index 0000000000000..a56ad67fe379e --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.sol @@ -0,0 +1,73 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TooManyDigits { + // SHOULD FAIL: plain decimal integer literals with 5+ consecutive zeros. + + uint256 stateA = 1000000000000000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + uint256 stateB = 100000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + function asReturn() public pure returns (uint256) { + return 10000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asComparison(uint256 x) public pure returns (bool) { + return x == 1000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArg(address to) public { + _send(to, 50000000000); //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArraySize() public pure { + uint256[100000] memory _arr; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + // Zero-run in the middle (not just trailing). + uint256 middleZeros = 123000007; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscores that don't actually break up the zero run. + uint256 badGrouping = 1_000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscore right after a single digit, leaving a 5-zero group. + uint256 badGrouping2 = 1_00000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // SHOULD PASS: + + // Boundary: 4 consecutive zeros (one short of the threshold). + uint256 fourZeros = 10000; + + // Uppercase scientific notation. + uint256 sciUpper = 1E18; + + // Scientific notation. + uint256 sci = 1e18; + + // Underscore-separated digit groups. + uint256 grouped = 1_000_000_000_000_000_000; + + // Sub-denominations. + uint256 oneEther = 1 ether; + uint256 oneGwei = 1 gwei; + uint256 fiveMin = 5 minutes; + + // Address literal (distinct AST kind, not flagged). + address adr = 0x1234567890123456789012345678901234567890; + + // Hex literal — intentional zero patterns (mask / padded value). + bytes32 mask = 0x0000000000000000000000000000000000000000000000000000000000000001; + uint256 hexNum = 0x100000; + + // Small literals (< 5 consecutive zeros). + uint256 small1 = 100; + uint256 small2 = 9999; + uint256 small3 = 1234; + uint256 spread = 101010; + + // Boolean literal. + bool flag = true; + + function _send(address, uint256) internal pure {} +} diff --git a/crates/lint/testdata/TooManyDigits.stderr b/crates/lint/testdata/TooManyDigits.stderr new file mode 100644 index 0000000000000..7e21a530776c2 --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.stderr @@ -0,0 +1,72 @@ +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateA = 1000000000000000000; + │ ━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateB = 100000; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return 10000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return x == 1000000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … _send(to, 50000000000); + │ ━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … uint256[100000] memory _arr; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 middleZeros = 123000007; + │ ━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping = 1_000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping2 = 1_00000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + diff --git a/crates/lint/testdata/TxOrigin.sol b/crates/lint/testdata/TxOrigin.sol new file mode 100644 index 0000000000000..9728a7e528e5b --- /dev/null +++ b/crates/lint/testdata/TxOrigin.sol @@ -0,0 +1,65 @@ +//@compile-flags: --only-lint tx-origin +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TxOrigin { + address public owner; + mapping(address => bool) public allowed; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(tx.origin == owner, "not owner"); //~WARN: `tx.origin` should not be used for authorization + _; + } + + function guardedByIf() external view { + if (tx.origin != owner) { //~WARN: `tx.origin` should not be used for authorization + revert("not owner"); + } + } + + function guardedByPredicate() external view { + assert(isOwner(tx.origin)); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByWhile() external view { + while (tx.origin == owner) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByFor() external view { + for (; tx.origin == owner;) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByDoWhile() external view { + do { + } while (tx.origin == owner); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByMapping() external view { + require(allowed[tx.origin], "not allowed"); //~WARN: `tx.origin` should not be used for authorization + require(allowed[tx.origin] == true, "not allowed"); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByTernary() external view { + require(tx.origin == owner ? true : false, "not owner"); //~WARN: `tx.origin` should not be used for authorization + } + + function readForLogging() external view returns (address) { + return tx.origin; + } + + function explicitSenderCheck() external view { + require(msg.sender == owner, "not owner"); + } + + function isOwner(address account) internal view returns (bool) { + return account == owner; + } +} diff --git a/crates/lint/testdata/TxOrigin.stderr b/crates/lint/testdata/TxOrigin.stderr new file mode 100644 index 0000000000000..7c2e70225b76d --- /dev/null +++ b/crates/lint/testdata/TxOrigin.stderr @@ -0,0 +1,72 @@ +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner, "not owner"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ if (tx.origin != owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ assert(isOwner(tx.origin)); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ while (tx.origin == owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ for (; tx.origin == owner;) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ } while (tx.origin == owner); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin], "not allowed"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin] == true, "not allowed"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner ? true : false, "not owner"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + diff --git a/crates/lint/testdata/UncheckedCall.stderr b/crates/lint/testdata/UncheckedCall.stderr index afb8ade4ea89b..8a8a9fa9b5e17 100644 --- a/crates/lint/testdata/UncheckedCall.stderr +++ b/crates/lint/testdata/UncheckedCall.stderr @@ -4,7 +4,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call(data); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -12,7 +12,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call{value: value}(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -20,7 +20,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.delegatecall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -28,7 +28,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.staticcall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -36,7 +36,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target1.call(""); │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -44,7 +44,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target2.delegatecall(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -52,7 +52,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, bytes memory data) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -60,5 +60,5 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, existingData) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call diff --git a/crates/lint/testdata/UncheckedTransferERC20.stderr b/crates/lint/testdata/UncheckedTransferERC20.stderr index 733d22ce610d1..2c2caa69e7215 100644 --- a/crates/lint/testdata/UncheckedTransferERC20.stderr +++ b/crates/lint/testdata/UncheckedTransferERC20.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Wrapper { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransfer { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library Currency { │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransferUsingCurrencyLib { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -44,7 +44,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ IERC20(address(token)).transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -52,7 +52,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -60,7 +60,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -68,7 +68,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -76,7 +76,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -84,5 +84,5 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer diff --git a/crates/lint/testdata/UnsafeCheatcodes.stderr b/crates/lint/testdata/UnsafeCheatcodes.stderr index e66a4d72c70de..5b8b429942e80 100644 --- a/crates/lint/testdata/UnsafeCheatcodes.stderr +++ b/crates/lint/testdata/UnsafeCheatcodes.stderr @@ -4,7 +4,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -12,7 +12,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -20,7 +20,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readLine("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -28,7 +28,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeFile("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -36,7 +36,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeLine("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -44,7 +44,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.removeFile("test.txt"); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.closeFile("test.txt"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -60,7 +60,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -68,7 +68,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.deriveKey("mnemonic", 0); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -76,7 +76,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ bytes memory result = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(new string[](1)); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -100,5 +100,5 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode diff --git a/crates/lint/testdata/UnsafeTypecast.stderr b/crates/lint/testdata/UnsafeTypecast.stderr index b3e0334d63d43..d909b90973e00 100644 --- a/crates/lint/testdata/UnsafeTypecast.stderr +++ b/crates/lint/testdata/UnsafeTypecast.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UnsafeTypecast { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract Repros { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -26,7 +26,7 @@ LL │ uint248 b = uint248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -40,7 +40,7 @@ LL │ uint240 c = uint240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -54,7 +54,7 @@ LL │ uint232 d = uint232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -68,7 +68,7 @@ LL │ uint224 e = uint224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -82,7 +82,7 @@ LL │ uint216 f = uint216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -96,7 +96,7 @@ LL │ uint208 g = uint208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -110,7 +110,7 @@ LL │ uint200 h = uint200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -124,7 +124,7 @@ LL │ uint192 i = uint192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -138,7 +138,7 @@ LL │ uint184 j = uint184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -152,7 +152,7 @@ LL │ uint176 k = uint176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -166,7 +166,7 @@ LL │ uint168 l = uint168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -180,7 +180,7 @@ LL │ uint160 m = uint160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -194,7 +194,7 @@ LL │ uint152 n = uint152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -208,7 +208,7 @@ LL │ uint144 o = uint144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -222,7 +222,7 @@ LL │ uint136 p = uint136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -236,7 +236,7 @@ LL │ uint128 q = uint128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -250,7 +250,7 @@ LL │ uint120 r = uint120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -264,7 +264,7 @@ LL │ uint112 s = uint112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -278,7 +278,7 @@ LL │ uint104 t = uint104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -292,7 +292,7 @@ LL │ uint96 u = uint96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -306,7 +306,7 @@ LL │ uint88 v = uint88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -320,7 +320,7 @@ LL │ uint80 w = uint80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -334,7 +334,7 @@ LL │ uint72 x = uint72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -348,7 +348,7 @@ LL │ uint64 y = uint64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -362,7 +362,7 @@ LL │ uint56 z = uint56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -376,7 +376,7 @@ LL │ uint48 A = uint48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -390,7 +390,7 @@ LL │ uint40 B = uint40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -404,7 +404,7 @@ LL │ uint32 C = uint32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -418,7 +418,7 @@ LL │ uint24 D = uint24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -432,7 +432,7 @@ LL │ uint16 E = uint16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -446,7 +446,7 @@ LL │ uint8 F = uint8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -460,7 +460,7 @@ LL │ int248 b = int248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -474,7 +474,7 @@ LL │ int240 c = int240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -488,7 +488,7 @@ LL │ int232 d = int232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -502,7 +502,7 @@ LL │ int224 e = int224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -516,7 +516,7 @@ LL │ int216 f = int216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -530,7 +530,7 @@ LL │ int208 g = int208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -544,7 +544,7 @@ LL │ int200 h = int200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -558,7 +558,7 @@ LL │ int192 i = int192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -572,7 +572,7 @@ LL │ int184 j = int184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -586,7 +586,7 @@ LL │ int176 k = int176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -600,7 +600,7 @@ LL │ int168 l = int168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -614,7 +614,7 @@ LL │ int160 m = int160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -628,7 +628,7 @@ LL │ int152 n = int152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -642,7 +642,7 @@ LL │ int144 o = int144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -656,7 +656,7 @@ LL │ int136 p = int136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -670,7 +670,7 @@ LL │ int128 q = int128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -684,7 +684,7 @@ LL │ int120 r = int120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -698,7 +698,7 @@ LL │ int112 s = int112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -712,7 +712,7 @@ LL │ int104 t = int104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -726,7 +726,7 @@ LL │ int96 u = int96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -740,7 +740,7 @@ LL │ int88 v = int88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -754,7 +754,7 @@ LL │ int80 w = int80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -768,7 +768,7 @@ LL │ int72 x = int72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -782,7 +782,7 @@ LL │ int64 y = int64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -796,7 +796,7 @@ LL │ int56 z = int56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -810,7 +810,7 @@ LL │ int48 A = int48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -824,7 +824,7 @@ LL │ int40 B = int40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -838,7 +838,7 @@ LL │ int32 C = int32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -852,7 +852,7 @@ LL │ int24 D = int24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -866,7 +866,7 @@ LL │ int16 E = int16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -880,7 +880,7 @@ LL │ int8 F = int8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -894,7 +894,7 @@ LL │ bytes31 b = bytes31(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -908,7 +908,7 @@ LL │ bytes30 c = bytes30(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -922,7 +922,7 @@ LL │ bytes29 d = bytes29(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -936,7 +936,7 @@ LL │ bytes28 e = bytes28(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -950,7 +950,7 @@ LL │ bytes27 f = bytes27(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -964,7 +964,7 @@ LL │ bytes26 g = bytes26(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -978,7 +978,7 @@ LL │ bytes25 h = bytes25(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -992,7 +992,7 @@ LL │ bytes24 i = bytes24(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1006,7 +1006,7 @@ LL │ bytes23 j = bytes23(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1020,7 +1020,7 @@ LL │ bytes22 k = bytes22(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1034,7 +1034,7 @@ LL │ bytes21 l = bytes21(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1048,7 +1048,7 @@ LL │ bytes20 m = bytes20(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1062,7 +1062,7 @@ LL │ bytes19 n = bytes19(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1076,7 +1076,7 @@ LL │ bytes18 o = bytes18(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1090,7 +1090,7 @@ LL │ bytes17 p = bytes17(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1104,7 +1104,7 @@ LL │ bytes16 q = bytes16(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1118,7 +1118,7 @@ LL │ bytes15 r = bytes15(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1132,7 +1132,7 @@ LL │ bytes14 s = bytes14(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1146,7 +1146,7 @@ LL │ bytes13 t = bytes13(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1160,7 +1160,7 @@ LL │ bytes12 u = bytes12(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1174,7 +1174,7 @@ LL │ bytes11 v = bytes11(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1188,7 +1188,7 @@ LL │ bytes10 w = bytes10(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1202,7 +1202,7 @@ LL │ bytes9 x = bytes9(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1216,7 +1216,7 @@ LL │ bytes8 y = bytes8(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1230,7 +1230,7 @@ LL │ bytes7 z = bytes7(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1244,7 +1244,7 @@ LL │ bytes6 A = bytes6(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1258,7 +1258,7 @@ LL │ bytes5 B = bytes5(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1272,7 +1272,7 @@ LL │ bytes4 C = bytes4(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1286,7 +1286,7 @@ LL │ bytes3 D = bytes3(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1300,7 +1300,7 @@ LL │ bytes2 E = bytes2(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1314,7 +1314,7 @@ LL │ bytes1 F = bytes1(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1328,7 +1328,7 @@ LL │ int256 b = int256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1342,7 +1342,7 @@ LL │ int248 d = int248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1356,7 +1356,7 @@ LL │ int240 f = int240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1370,7 +1370,7 @@ LL │ int232 h = int232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1384,7 +1384,7 @@ LL │ int224 j = int224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1398,7 +1398,7 @@ LL │ int216 l = int216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1412,7 +1412,7 @@ LL │ int208 n = int208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1426,7 +1426,7 @@ LL │ int200 p = int200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1440,7 +1440,7 @@ LL │ int192 r = int192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1454,7 +1454,7 @@ LL │ int184 t = int184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1468,7 +1468,7 @@ LL │ int176 v = int176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1482,7 +1482,7 @@ LL │ int168 x = int168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1496,7 +1496,7 @@ LL │ int160 z = int160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1510,7 +1510,7 @@ LL │ int152 B = int152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1524,7 +1524,7 @@ LL │ int144 D = int144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1538,7 +1538,7 @@ LL │ int136 F = int136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1552,7 +1552,7 @@ LL │ int128 H = int128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1566,7 +1566,7 @@ LL │ int120 J = int120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1580,7 +1580,7 @@ LL │ int112 L = int112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1594,7 +1594,7 @@ LL │ int104 N = int104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1608,7 +1608,7 @@ LL │ int96 P = int96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1622,7 +1622,7 @@ LL │ int88 R = int88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1636,7 +1636,7 @@ LL │ int80 T = int80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1650,7 +1650,7 @@ LL │ int72 V = int72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1664,7 +1664,7 @@ LL │ int64 X = int64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1678,7 +1678,7 @@ LL │ int56 Z = int56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1692,7 +1692,7 @@ LL │ int48 BB = int48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1706,7 +1706,7 @@ LL │ int40 DD = int40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1720,7 +1720,7 @@ LL │ int32 FF = int32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1734,7 +1734,7 @@ LL │ int24 HH = int24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1748,7 +1748,7 @@ LL │ int16 JJ = int16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1762,7 +1762,7 @@ LL │ int8 LL = int8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1776,7 +1776,7 @@ LL │ uint256 b = uint256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1790,7 +1790,7 @@ LL │ uint248 d = uint248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1804,7 +1804,7 @@ LL │ uint240 f = uint240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1818,7 +1818,7 @@ LL │ uint232 h = uint232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1832,7 +1832,7 @@ LL │ uint224 j = uint224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1846,7 +1846,7 @@ LL │ uint216 l = uint216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1860,7 +1860,7 @@ LL │ uint208 n = uint208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1874,7 +1874,7 @@ LL │ uint200 p = uint200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1888,7 +1888,7 @@ LL │ uint192 r = uint192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1902,7 +1902,7 @@ LL │ uint184 t = uint184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1916,7 +1916,7 @@ LL │ uint176 v = uint176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1930,7 +1930,7 @@ LL │ uint168 x = uint168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1944,7 +1944,7 @@ LL │ uint160 z = uint160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1958,7 +1958,7 @@ LL │ uint152 B = uint152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1972,7 +1972,7 @@ LL │ uint144 D = uint144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1986,7 +1986,7 @@ LL │ uint136 F = uint136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2000,7 +2000,7 @@ LL │ uint128 H = uint128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2014,7 +2014,7 @@ LL │ uint120 J = uint120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2028,7 +2028,7 @@ LL │ uint112 L = uint112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2042,7 +2042,7 @@ LL │ uint104 N = uint104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2056,7 +2056,7 @@ LL │ uint96 P = uint96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2070,7 +2070,7 @@ LL │ uint88 R = uint88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2084,7 +2084,7 @@ LL │ uint80 T = uint80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2098,7 +2098,7 @@ LL │ uint72 V = uint72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2112,7 +2112,7 @@ LL │ uint64 X = uint64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2126,7 +2126,7 @@ LL │ uint56 Z = uint56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2140,7 +2140,7 @@ LL │ uint48 BB = uint48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2154,7 +2154,7 @@ LL │ uint40 DD = uint40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2168,7 +2168,7 @@ LL │ uint32 FF = uint32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2182,7 +2182,7 @@ LL │ uint24 HH = uint24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2196,7 +2196,7 @@ LL │ uint16 JJ = uint16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2210,7 +2210,7 @@ LL │ uint8 LL = uint8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2224,7 +2224,7 @@ LL │ bytes32 dataSlice = bytes32(data); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2238,7 +2238,7 @@ LL │ bytes32 strSlice = bytes32(bytes(str)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2252,7 +2252,7 @@ LL │ uint128 aPlusB = uint128(int128(uint128(a)) + b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2266,7 +2266,7 @@ LL │ uint64 unsafe = uint64(aPlusB); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2280,7 +2280,7 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2294,5 +2294,5 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast diff --git a/crates/lint/testdata/UnusedStateVariables.stderr b/crates/lint/testdata/UnusedStateVariables.stderr index ecc9e87efc105..92a0082bb8293 100644 --- a/crates/lint/testdata/UnusedStateVariables.stderr +++ b/crates/lint/testdata/UnusedStateVariables.stderr @@ -4,7 +4,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address usedInBoth; │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -12,7 +12,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 unused; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -20,7 +20,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 unused; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -28,7 +28,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 firstUnused; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -36,5 +36,5 @@ note[unused-state-variables]: state variable is never used LL │ uint256 secondUnused; │ ━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables diff --git a/crates/lint/testdata/UnwrappedModifierLogic.stderr b/crates/lint/testdata/UnwrappedModifierLogic.stderr index 5e1bf754e60e4..dc5514c2d5e98 100644 --- a/crates/lint/testdata/UnwrappedModifierLogic.stderr +++ b/crates/lint/testdata/UnwrappedModifierLogic.stderr @@ -9,7 +9,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforePlaceholder() { @@ -35,7 +35,7 @@ LL │ ┃ checkInternal(msg.sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleAfterPlaceholder() { @@ -62,7 +62,7 @@ LL │ ┃ checkPublic(sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforeAfterPlaceholder(address sender) { @@ -91,7 +91,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwner() { @@ -113,7 +113,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRole(bytes32 role) { @@ -135,7 +135,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrOpenRole(bytes32 role) { @@ -157,7 +157,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrAdmin(bytes32 role, address admin) { @@ -180,7 +180,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier assign(address sender) { @@ -204,7 +204,7 @@ LL │ ┃ sender; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier uncheckedBlock(address sender) { @@ -228,7 +228,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier emitEvent(address sender) { @@ -250,7 +250,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwnerContract(address sender) { diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 9970616900e6b..15363a6a40bb0 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -23,10 +23,10 @@ alloy-rpc-types-eth.workspace = true alloy-serde.workspace = true alloy-signer.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"] } -op-alloy-rpc-types.workspace = true -alloy-op-evm.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +alloy-op-evm = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } revm.workspace = true serde_json.workspace = true serde = { version = "1.0", features = ["derive"] } @@ -34,3 +34,12 @@ derive_more.workspace = true tempo-primitives.workspace = true tempo-alloy.workspace = true tempo-revm.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", +] diff --git a/crates/primitives/src/network/mod.rs b/crates/primitives/src/network/mod.rs index 7000b580b6a73..0b840185b0383 100644 --- a/crates/primitives/src/network/mod.rs +++ b/crates/primitives/src/network/mod.rs @@ -1,12 +1,20 @@ use alloy_network::Network; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; use alloy_provider::fillers::{ BlobGasFiller, ChainIdFiller, GasFiller, JoinFill, NonceFiller, RecommendedFillers, }; +#[cfg(feature = "optimism")] +pub use optimism::FoundryTransactionResponse; pub use receipt::*; +/// Default JSON-RPC transaction response when the `optimism` feature is disabled. +#[cfg(not(feature = "optimism"))] +pub type FoundryTransactionResponse = alloy_rpc_types_eth::Transaction; + /// Foundry network type. /// /// This network type supports Foundry-specific transaction types, including @@ -36,7 +44,7 @@ impl Network for FoundryNetwork { type TransactionRequest = crate::FoundryTransactionRequest; - type TransactionResponse = op_alloy_rpc_types::Transaction; + type TransactionResponse = FoundryTransactionResponse; type ReceiptResponse = crate::FoundryTxReceipt; diff --git a/crates/primitives/src/network/optimism.rs b/crates/primitives/src/network/optimism.rs new file mode 100644 index 0000000000000..aff30a755663f --- /dev/null +++ b/crates/primitives/src/network/optimism.rs @@ -0,0 +1,47 @@ +//! OP-stack-specific helpers and type aliases used by [`super::FoundryNetwork`] and +//! [`super::FoundryTxReceipt`]. + +use alloy_consensus::{Receipt, ReceiptWithBloom, TxReceipt}; +use alloy_primitives::U64; +use alloy_rpc_types::Log; +use alloy_serde::OtherFields; +use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; + +use crate::FoundryReceiptEnvelope; + +/// JSON-RPC transaction response type used by [`super::FoundryNetwork`]. +pub type FoundryTransactionResponse = op_alloy_rpc_types::Transaction; + +/// Build a [`FoundryReceiptEnvelope::Deposit`] from a `ReceiptWithBloom` plus the OP +/// deposit-specific fields decoded from the [`OtherFields`] of an `AnyTransactionReceipt`. +pub(super) fn build_deposit_receipt_envelope( + receipt_with_bloom: ReceiptWithBloom>, + other: &OtherFields, +) -> FoundryReceiptEnvelope { + // These fields may not be present in all receipts, so missing/invalid values are None. + let deposit_nonce = other + .get_deserialized::("depositNonce") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + let deposit_receipt_version = other + .get_deserialized::("depositReceiptVersion") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + + FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { + status: alloy_consensus::Eip658Value::Eip658(receipt_with_bloom.status()), + cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), + logs: receipt_with_bloom.receipt.logs, + }, + deposit_nonce, + deposit_receipt_version, + }, + logs_bloom: receipt_with_bloom.logs_bloom, + }) +} diff --git a/crates/primitives/src/network/receipt.rs b/crates/primitives/src/network/receipt.rs index b727b4c39b345..6b01f9eaa9ee9 100644 --- a/crates/primitives/src/network/receipt.rs +++ b/crates/primitives/src/network/receipt.rs @@ -1,13 +1,13 @@ -use alloy_consensus::{Receipt, TxReceipt}; use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt, ReceiptResponse}; -use alloy_primitives::{Address, B256, BlockHash, TxHash, U64}; +use alloy_primitives::{Address, B256, BlockHash, TxHash}; use alloy_rpc_types::{ConversionError, Log, TransactionReceipt}; use alloy_serde::WithOtherFields; use derive_more::AsRef; -use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; use serde::{Deserialize, Serialize}; use tempo_primitives::TEMPO_TX_TYPE_ID; +#[cfg(feature = "optimism")] +use super::optimism::build_deposit_receipt_envelope; use crate::FoundryReceiptEnvelope; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AsRef)] @@ -144,38 +144,8 @@ impl TryFrom for FoundryTxReceipt { 0x03 => FoundryReceiptEnvelope::Eip4844(receipt_with_bloom), 0x04 => FoundryReceiptEnvelope::Eip7702(receipt_with_bloom), TEMPO_TX_TYPE_ID => FoundryReceiptEnvelope::Tempo(receipt_with_bloom), - 0x7E => { - // Construct the deposit receipt, extracting optional deposit fields - // These fields may not be present in all receipts, so missing/invalid - // values are None - let deposit_nonce = other - .get_deserialized::("depositNonce") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - let deposit_receipt_version = other - .get_deserialized::("depositReceiptVersion") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - - FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { - receipt: OpDepositReceipt { - inner: Receipt { - status: alloy_consensus::Eip658Value::Eip658( - receipt_with_bloom.status(), - ), - cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), - logs: receipt_with_bloom.receipt.logs, - }, - deposit_nonce, - deposit_receipt_version, - }, - logs_bloom: receipt_with_bloom.logs_bloom, - }) - } + #[cfg(feature = "optimism")] + 0x7E => build_deposit_receipt_envelope(receipt_with_bloom, &other), _ => { let tx_type = r#type; return Err(ConversionError::Custom(format!( diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index ab4aafcc294c0..0a009a1931f06 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,6 +1,7 @@ +#[cfg(feature = "optimism")] +use alloy_consensus::{Sealed, Transaction as _}; use alloy_consensus::{ - Sealed, Signed, Transaction as _, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, - TxLegacy, TxType, Typed2718, + Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType, Typed2718, crypto::RecoveryError, transaction::{ SignerRecoverable, TxEip7702, TxHashRef, @@ -9,14 +10,10 @@ use alloy_consensus::{ }; use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, TxHash}; use alloy_rpc_types::ConversionError; -use op_alloy_consensus::{ - DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, POST_EXEC_TX_TYPE_ID, TxDeposit, - TxPostExec, -}; -use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +#[cfg(feature = "optimism")] +use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit, TxPostExec}; use revm::context::TxEnv; use tempo_primitives::{AASigned, TempoTransaction}; use tempo_revm::TempoTxEnv; @@ -57,9 +54,11 @@ pub enum FoundryTxEnvelope { /// OP stack deposit transaction. /// /// See . + #[cfg(feature = "optimism")] #[envelope(ty = 126)] Deposit(Sealed), /// OP stack post-execution synthetic transaction. + #[cfg(feature = "optimism")] #[envelope(ty = 0x7D)] PostExec(Sealed), /// Tempo transaction type. @@ -80,7 +79,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)), Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)), Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)), + #[cfg(feature = "optimism")] Self::Deposit(_) => Err(self), + #[cfg(feature = "optimism")] Self::PostExec(_) => Err(self), Self::Tempo(_) => Err(self), } @@ -109,7 +110,9 @@ impl FoundryTxEnvelope { Self::Eip1559(t) => *t.hash(), Self::Eip4844(t) => *t.hash(), Self::Eip7702(t) => *t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.tx_hash(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.tx_hash(), Self::Tempo(t) => *t.hash(), } @@ -128,7 +131,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => tx.recover_signer()?, Self::Eip4844(tx) => tx.recover_signer()?, Self::Eip7702(tx) => tx.recover_signer()?, + #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.from, + #[cfg(feature = "optimism")] Self::PostExec(tx) => tx.inner().signer_address(), Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?, }) @@ -143,7 +148,9 @@ impl TxHashRef for FoundryTxEnvelope { Self::Eip1559(t) => t.hash(), Self::Eip4844(t) => t.hash(), Self::Eip7702(t) => t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.hash_ref(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.hash_ref(), Self::Tempo(t) => t.hash(), } @@ -160,23 +167,6 @@ impl SignerRecoverable for FoundryTxEnvelope { } } -impl OpTransactionTrait for FoundryTxEnvelope { - fn is_deposit(&self) -> bool { - matches!(self, Self::Deposit(_)) - } - - fn as_deposit(&self) -> Option<&Sealed> { - match self { - Self::Deposit(tx) => Some(tx), - _ => None, - } - } - - fn as_post_exec(&self) -> Option<&Sealed> { - if let Self::PostExec(tx) = self { Some(tx) } else { None } - } -} - impl TryFrom for TxEnvelope { type Error = FoundryTxEnvelope; @@ -197,19 +187,6 @@ impl From for FoundryTxEnvelope { } } -impl From for FoundryTxEnvelope { - fn from(tx: op_alloy_consensus::OpTxEnvelope) -> Self { - match tx { - op_alloy_consensus::OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), - op_alloy_consensus::OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), - op_alloy_consensus::OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), - op_alloy_consensus::OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), - op_alloy_consensus::OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), - op_alloy_consensus::OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), - } - } -} - impl From for FoundryTxEnvelope { fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self { match tx { @@ -236,33 +213,50 @@ impl TryFrom for FoundryTxEnvelope { TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)), TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)), }, - AnyTxEnvelope::Unknown(mut tx) => { - // Try to convert to deposit transaction - if tx.ty() == DEPOSIT_TX_TYPE_ID { - tx.inner.fields.insert("from".to_string(), serde_json::to_value(from).unwrap()); - let deposit_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize deposit tx: {e}" - )) - })?; - - return Ok(Self::Deposit(Sealed::new(deposit_tx))); + AnyTxEnvelope::Unknown(tx) => { + #[cfg(feature = "optimism")] + { + let mut tx = tx; + let _ = from; + // Try to convert to deposit transaction + if tx.ty() == DEPOSIT_TX_TYPE_ID { + tx.inner + .fields + .insert("from".to_string(), serde_json::to_value(from).unwrap()); + let deposit_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize deposit tx: {e}" + )) + })?; + + return Ok(Self::Deposit(Sealed::new(deposit_tx))); + } + + if tx.ty() == POST_EXEC_TX_TYPE_ID { + let post_exec_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize post-exec tx: {e}" + )) + })?; + + return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + } + + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - if tx.ty() == POST_EXEC_TX_TYPE_ID { - let post_exec_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize post-exec tx: {e}" - )) - })?; - - return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + #[cfg(not(feature = "optimism"))] + { + let _ = from; + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - let tx_type = tx.ty(); - Err(ConversionError::Custom(format!("Unknown transaction type: 0x{tx_type:02X}"))) } } } @@ -276,6 +270,7 @@ impl FromRecoveredTx for TxEnv { FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -288,6 +283,7 @@ impl FromRecoveredTx for TxEnv { ..Default::default() } } + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -303,63 +299,6 @@ impl FromRecoveredTx for TxEnv { } } -impl FromRecoveredTx for OpTransaction { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: None, deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl FromTxWithEncoded for TxEnv { fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self { Self::from_recovered_tx(tx, sender) @@ -384,7 +323,9 @@ impl FromRecoveredTx for TempoTxEnv { FoundryTxEnvelope::Eip7702(signed_tx) => { Self::from(TxEnv::from_recovered_tx(signed_tx, caller)) } + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"), FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller), } @@ -397,75 +338,6 @@ impl FromTxWithEncoded for TempoTxEnv { } } -impl FromRecoveredTx for OpTx { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - Self(OpTransaction::::from_recovered_tx(tx, caller)) - } -} - -impl FromTxWithEncoded for OpTx { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) - } -} - -impl FromTxWithEncoded for OpTransaction { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: Some(encoded), deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl std::fmt::Display for FoundryTxType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -474,7 +346,9 @@ impl std::fmt::Display for FoundryTxType { Self::Eip1559 => write!(f, "eip1559"), Self::Eip4844 => write!(f, "eip4844"), Self::Eip7702 => write!(f, "eip7702"), + #[cfg(feature = "optimism")] Self::Deposit => write!(f, "deposit"), + #[cfg(feature = "optimism")] Self::PostExec => write!(f, "post-exec"), Self::Tempo => write!(f, "tempo"), } @@ -501,7 +375,9 @@ impl From for FoundryTypedTx { FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()), FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()), FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.into_inner()), FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()), } @@ -609,28 +485,6 @@ mod tests { assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2")); } - #[test] - fn test_decode_encode_deposit_tx() { - // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" - .parse::() - .unwrap(); - - // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let raw_tx = alloy_primitives::hex::decode( - "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", - ) - .unwrap(); - let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); - - let mut encoded = Vec::new(); - dep_tx.encode_2718(&mut encoded); - - assert_eq!(raw_tx, encoded); - - assert_eq!(tx_hash, dep_tx.hash()); - } - #[test] fn can_recover_sender_not_normalized() { let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap(); @@ -707,11 +561,6 @@ mod tests { assert_eq!(tx_env.caller, sender); assert_eq!(tx_env.gas_limit, 0x5208); assert_eq!(tx_env.gas_price, 1); - - // Test OpTransaction conversion via FromRecoveredTx trait - let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); - assert_eq!(op_tx.base.caller, sender); - assert_eq!(op_tx.base.gas_limit, 0x5208); } // Test vector from Tempo testnet: diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 7dccf3e30752f..18f39c437bfbc 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1,7 +1,11 @@ mod envelope; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; mod request; pub use envelope::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; +#[cfg(feature = "optimism")] +pub use optimism::get_deposit_tx_parts; pub use receipt::FoundryReceiptEnvelope; -pub use request::{FoundryTransactionRequest, get_deposit_tx_parts}; +pub use request::FoundryTransactionRequest; diff --git a/crates/primitives/src/transaction/optimism.rs b/crates/primitives/src/transaction/optimism.rs new file mode 100644 index 0000000000000..5952b5d6a9020 --- /dev/null +++ b/crates/primitives/src/transaction/optimism.rs @@ -0,0 +1,300 @@ +//! OP-stack-specific impls for [`FoundryTxEnvelope`] and [`FoundryTransactionRequest`]. + +use alloy_consensus::{Sealed, Transaction as _, Typed2718}; +use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::OpTx; +use alloy_primitives::{Address, B256, Bytes, U256}; +use alloy_serde::OtherFields; +use op_alloy_consensus::{ + OpDepositReceipt, OpDepositReceiptWithBloom, OpTransaction as OpTransactionTrait, OpTxEnvelope, + TxDeposit, TxPostExec, +}; +use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +use revm::context::TxEnv; + +use super::{FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope}; + +impl OpTransactionTrait for FoundryTxEnvelope { + fn is_deposit(&self) -> bool { + matches!(self, Self::Deposit(_)) + } + + fn as_deposit(&self) -> Option<&Sealed> { + match self { + Self::Deposit(tx) => Some(tx), + _ => None, + } + } + + fn as_post_exec(&self) -> Option<&Sealed> { + if let Self::PostExec(tx) = self { Some(tx) } else { None } + } +} + +impl From for FoundryTxEnvelope { + fn from(tx: OpTxEnvelope) -> Self { + match tx { + OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), + OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), + OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), + OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), + OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), + OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), + } + } +} + +impl FromRecoveredTx for OpTransaction { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: None, deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl FromRecoveredTx for OpTx { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + Self(OpTransaction::::from_recovered_tx(tx, caller)) + } +} + +impl FromTxWithEncoded for OpTx { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) + } +} + +impl FromTxWithEncoded for OpTransaction { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: Some(encoded), deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl From> for FoundryTransactionRequest { + fn from(tx: op_alloy_rpc_types::Transaction) -> Self { + tx.inner.into_inner().into() + } +} + +/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields. +pub fn get_deposit_tx_parts( + other: &OtherFields, +) -> Result> { + let mut missing = Vec::new(); + let source_hash = + other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("sourceHash"); + Default::default() + }, + ); + let mint = other + .get_deserialized::("mint") + .transpose() + .unwrap_or_else(|_| { + missing.push("mint"); + Default::default() + }) + .map(|value| value.to::()); + let is_system_transaction = + other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("isSystemTx"); + Default::default() + }, + ); + if missing.is_empty() { + Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) + } else { + Err(missing) + } +} + +/// OP-stack-specific accessors on [`FoundryReceiptEnvelope`]. +impl FoundryReceiptEnvelope { + /// Return the receipt's deposit_nonce if it is a deposit receipt. + pub fn deposit_nonce(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_nonce) + } + + /// Return the receipt's deposit version if it is a deposit receipt. + pub fn deposit_receipt_version(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { + match self { + Self::Deposit(t) => Some(t), + _ => None, + } + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { + match self { + Self::Deposit(t) => Some(&t.receipt), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use alloy_network::eip2718::Encodable2718; + use alloy_primitives::TxHash; + use alloy_rlp::Decodable; + + use super::*; + + #[test] + fn test_from_recovered_tx_legacy_op() { + use alloy_consensus::transaction::SignerRecoverable; + + let tx = r#" + { + "type": "0x0", + "chainId": "0x1", + "nonce": "0x0", + "gas": "0x5208", + "gasPrice": "0x1", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "value": "0x1", + "input": "0x", + "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0", + "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd", + "v": "0x1b", + "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515" + }"#; + + let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap(); + let sender = typed_tx.recover_signer().unwrap(); + + // Test OpTransaction conversion via FromRecoveredTx trait + let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); + assert_eq!(op_tx.base.caller, sender); + assert_eq!(op_tx.base.gas_limit, 0x5208); + } + + #[test] + fn test_decode_encode_deposit_tx() { + // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" + .parse::() + .unwrap(); + + // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let raw_tx = alloy_primitives::hex::decode( + "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", + ) + .unwrap(); + let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); + + let mut encoded = Vec::new(); + dep_tx.encode_2718(&mut encoded); + + assert_eq!(raw_tx, encoded); + + assert_eq!(tx_hash, dep_tx.hash()); + } +} diff --git a/crates/primitives/src/transaction/receipt.rs b/crates/primitives/src/transaction/receipt.rs index fe209fc72c907..78bbcd1efa2fb 100644 --- a/crates/primitives/src/transaction/receipt.rs +++ b/crates/primitives/src/transaction/receipt.rs @@ -8,6 +8,7 @@ use alloy_network::eip2718::{ use alloy_primitives::{Bloom, Log, TxHash, logs_bloom}; use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes}; use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{ DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID, }; @@ -29,8 +30,10 @@ pub enum FoundryReceiptEnvelope { Eip4844(ReceiptWithBloom>), #[serde(rename = "0x4", alias = "0x04")] Eip7702(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7D", alias = "0x7d")] PostExec(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7E", alias = "0x7e")] Deposit(OpDepositReceiptWithBloom), #[serde(rename = "0x76")] @@ -44,7 +47,8 @@ impl FoundryReceiptEnvelope { cumulative_gas_used: u64, logs: impl IntoIterator, tx_type: FoundryTxType, - deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_receipt_version: Option, ) -> Self { let logs = logs.into_iter().collect::>(); @@ -67,9 +71,11 @@ impl FoundryReceiptEnvelope { FoundryTxType::Eip7702 => { Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => { Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { let inner = OpDepositReceiptWithBloom { receipt: OpDepositReceipt { @@ -112,13 +118,18 @@ impl FoundryReceiptEnvelope { removed: false, }) .collect::>(); + #[cfg(feature = "optimism")] + let (deposit_nonce, deposit_receipt_version) = + (self.deposit_nonce(), self.deposit_receipt_version()); + #[cfg(not(feature = "optimism"))] + let (deposit_nonce, deposit_receipt_version) = (None, None); FoundryReceiptEnvelope::::from_parts( self.status(), self.cumulative_gas_used(), logs, self.tx_type(), - self.deposit_nonce(), - self.deposit_receipt_version(), + deposit_nonce, + deposit_receipt_version, ) } } @@ -132,7 +143,9 @@ impl FoundryReceiptEnvelope { Self::Eip1559(_) => FoundryTxType::Eip1559, Self::Eip4844(_) => FoundryTxType::Eip4844, Self::Eip7702(_) => FoundryTxType::Eip7702, + #[cfg(feature = "optimism")] Self::PostExec(_) => FoundryTxType::PostExec, + #[cfg(feature = "optimism")] Self::Deposit(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -158,8 +171,12 @@ impl FoundryReceiptEnvelope { Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)), Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)), Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)), + #[cfg(feature = "optimism")] Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)), - Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(r.map_receipt(|r| r.map_logs(f))), + #[cfg(feature = "optimism")] + Self::Deposit(r) => FoundryReceiptEnvelope::Deposit( + r.map_receipt(|r: OpDepositReceipt| r.map_logs(f)), + ), Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)), } } @@ -182,38 +199,14 @@ impl FoundryReceiptEnvelope { Self::Eip1559(t) => &t.logs_bloom, Self::Eip4844(t) => &t.logs_bloom, Self::Eip7702(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::PostExec(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.logs_bloom, Self::Tempo(t) => &t.logs_bloom, } } - /// Return the receipt's deposit_nonce if it is a deposit receipt. - pub fn deposit_nonce(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_nonce) - } - - /// Return the receipt's deposit version if it is a deposit receipt. - pub fn deposit_receipt_version(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { - match self { - Self::Deposit(t) => Some(t), - _ => None, - } - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { - match self { - Self::Deposit(t) => Some(&t.receipt), - _ => None, - } - } - /// Consumes the type and returns the underlying [`Receipt`]. pub fn into_receipt(self) -> Receipt { match self { @@ -222,8 +215,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => t.receipt.into_inner(), } } @@ -236,8 +231,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => &t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => &t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.receipt.inner, } } @@ -287,7 +284,9 @@ impl Encodable for FoundryReceiptEnvelope { Self::Eip1559(r) => r.length() + 1, Self::Eip4844(r) => r.length() + 1, Self::Eip7702(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::PostExec(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::Deposit(r) => r.length() + 1, Self::Tempo(r) => r.length() + 1, _ => unreachable!("receipt already matched"), @@ -314,11 +313,13 @@ impl Encodable for FoundryReceiptEnvelope { EIP7702_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::PostExec(r) => { Header { list: true, payload_length: payload_len }.encode(out); POST_EXEC_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::Deposit(r) => { Header { list: true, payload_length: payload_len }.encode(out); DEPOSIT_TX_TYPE_ID.encode(out); @@ -371,18 +372,23 @@ impl Decodable for FoundryReceiptEnvelope { buf.advance(1); ::decode(buf) .map(FoundryReceiptEnvelope::Eip7702) - } else if receipt_type == POST_EXEC_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::PostExec) - } else if receipt_type == DEPOSIT_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::Deposit) } else if receipt_type == TEMPO_TX_TYPE_ID { buf.advance(1); ::decode(buf).map(FoundryReceiptEnvelope::Tempo) } else { + #[cfg(feature = "optimism")] + { + if receipt_type == POST_EXEC_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::PostExec); + } + if receipt_type == DEPOSIT_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::Deposit); + } + } Err(alloy_rlp::Error::Custom("invalid receipt type")) } } @@ -404,7 +410,9 @@ impl Typed2718 for FoundryReceiptEnvelope { Self::Eip1559(_) => EIP1559_TX_TYPE_ID, Self::Eip4844(_) => EIP4844_TX_TYPE_ID, Self::Eip7702(_) => EIP7702_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::PostExec(_) => POST_EXEC_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::Deposit(_) => DEPOSIT_TX_TYPE_ID, Self::Tempo(_) => TEMPO_TX_TYPE_ID, } @@ -419,7 +427,9 @@ impl Encodable2718 for FoundryReceiptEnvelope { Self::Eip1559(r) => 1 + r.length(), Self::Eip4844(r) => 1 + r.length(), Self::Eip7702(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::PostExec(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::Deposit(r) => 1 + r.length(), Self::Tempo(r) => 1 + r.length(), } @@ -435,8 +445,10 @@ impl Encodable2718 for FoundryReceiptEnvelope { | Self::Eip1559(r) | Self::Eip4844(r) | Self::Eip7702(r) - | Self::PostExec(r) | Self::Tempo(r) => r.encode(out), + #[cfg(feature = "optimism")] + Self::PostExec(r) => r.encode(out), + #[cfg(feature = "optimism")] Self::Deposit(r) => r.encode(out), } } @@ -444,15 +456,18 @@ impl Encodable2718 for FoundryReceiptEnvelope { impl Decodable2718 for FoundryReceiptEnvelope { fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { - if ty == DEPOSIT_TX_TYPE_ID { - return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + #[cfg(feature = "optimism")] + { + if ty == DEPOSIT_TX_TYPE_ID { + return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + } + if ty == POST_EXEC_TX_TYPE_ID { + return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); + } } if ty == TEMPO_TX_TYPE_ID { return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?)); } - if ty == POST_EXEC_TX_TYPE_ID { - return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); - } match ReceiptEnvelope::typed_decode(ty, buf)? { ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)), ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)), @@ -646,8 +661,11 @@ mod tests { assert!(receipt.status()); assert_eq!(receipt.cumulative_gas_used(), 100000); assert!(receipt.logs().is_empty()); - assert!(receipt.deposit_nonce().is_none()); - assert!(receipt.deposit_receipt_version().is_none()); + #[cfg(feature = "optimism")] + { + assert!(receipt.deposit_nonce().is_none()); + assert!(receipt.deposit_receipt_version().is_none()); + } } #[test] diff --git a/crates/primitives/src/transaction/request.rs b/crates/primitives/src/transaction/request.rs index 2c4dbae8fdcd4..8ae31efbd5cb1 100644 --- a/crates/primitives/src/transaction/request.rs +++ b/crates/primitives/src/transaction/request.rs @@ -3,15 +3,19 @@ use alloy_network::{ BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder, TransactionBuilder4844, TransactionBuilderError, }; -use alloy_primitives::{Address, B256, ChainId, TxKind, U256}; +use alloy_primitives::{Address, ChainId, TxKind, U256}; use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest}; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit}; +#[cfg(feature = "optimism")] use op_revm::transaction::deposit::DepositTransactionParts; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionRequest; use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType}; +#[cfg(feature = "optimism")] +use super::optimism::get_deposit_tx_parts; use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; use crate::FoundryNetwork; @@ -28,6 +32,7 @@ use crate::FoundryNetwork; #[derive(Clone, Debug, PartialEq, Eq)] pub enum FoundryTransactionRequest { Ethereum(TransactionRequest), + #[cfg(feature = "optimism")] Op(WithOtherFields), Tempo(Box), } @@ -44,6 +49,7 @@ impl FoundryTransactionRequest { pub fn into_inner(self) -> TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx.inner, Self::Tempo(tx) => tx.inner, } @@ -55,6 +61,7 @@ impl FoundryTransactionRequest { /// # Returns /// - Ok(deposit_tx_parts) if all necessary keys are present to build a deposit transaction. /// - Err(missing) if some keys are missing to build a deposit transaction. + #[cfg(feature = "optimism")] pub fn get_deposit_tx_parts(&self) -> Result> { match self { Self::Op(tx) => get_deposit_tx_parts(&tx.other), @@ -69,9 +76,11 @@ impl FoundryTransactionRequest { pub fn preferred_type(&self) -> FoundryTxType { match self { Self::Ethereum(tx) => tx.preferred_type().into(), + #[cfg(feature = "optimism")] Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => { FoundryTxType::PostExec } + #[cfg(feature = "optimism")] Self::Op(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -95,6 +104,7 @@ impl FoundryTransactionRequest { /// Check if all necessary keys are present to build a Deposit transaction, returning a list of /// keys that are missing. + #[cfg(feature = "optimism")] pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> { self.get_deposit_tx_parts().map(|_| ()) } @@ -123,7 +133,9 @@ impl FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } { @@ -138,9 +150,10 @@ impl FoundryTransactionRequest { /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction /// types. pub fn build_typed_tx(self) -> Result { + #[cfg(feature = "optimism")] if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() { // Build deposit transaction - Ok(FoundryTypedTx::Deposit(TxDeposit { + return Ok(FoundryTypedTx::Deposit(TxDeposit { from: self.from().unwrap_or_default(), source_hash: deposit_tx_parts.source_hash, to: self.kind().unwrap_or_default(), @@ -149,8 +162,9 @@ impl FoundryTransactionRequest { gas_limit: self.gas_limit().unwrap_or_default(), is_system_transaction: deposit_tx_parts.is_system_transaction, input: self.input().cloned().unwrap_or_default(), - })) - } else if self.complete_tempo().is_ok() + })); + } + if self.complete_tempo().is_ok() && let Self::Tempo(tx_req) = self { // Build Tempo transaction @@ -192,6 +206,7 @@ impl Serialize for FoundryTransactionRequest { { match self { Self::Ethereum(tx) => tx.serialize(serializer), + #[cfg(feature = "optimism")] Self::Op(tx) => tx.serialize(serializer), Self::Tempo(tx) => tx.serialize(serializer), } @@ -211,6 +226,7 @@ impl AsRef for FoundryTransactionRequest { fn as_ref(&self) -> &TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_ref(), } @@ -221,6 +237,7 @@ impl AsMut for FoundryTransactionRequest { fn as_mut(&mut self) -> &mut TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_mut(), } @@ -244,15 +261,16 @@ impl From> for FoundryTransactionRequest { { tempo_tx_req.set_nonce_key(nonce_key); } - Self::Tempo(Box::new(tempo_tx_req)) - } else if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) + return Self::Tempo(Box::new(tempo_tx_req)); + } + #[cfg(feature = "optimism")] + if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID) || get_deposit_tx_parts(&tx.other).is_ok() { - Self::Op(tx) - } else { - Self::Ethereum(tx.into_inner()) + return Self::Op(tx); } + Self::Ethereum(tx.into_inner()) } } @@ -264,6 +282,7 @@ impl From for FoundryTransactionRequest { FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::::into(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => { let other = OtherFields::from_iter([ ("sourceHash", tx.source_hash.to_string().into()), @@ -272,6 +291,7 @@ impl From for FoundryTransactionRequest { ]); WithOtherFields { inner: Into::::into(tx), other }.into() } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(tx) => WithOtherFields { inner: Into::::into(tx), other: OtherFields::default(), @@ -307,8 +327,9 @@ impl From for FoundryTransactionRequest { } } -impl From> for FoundryTransactionRequest { - fn from(tx: op_alloy_rpc_types::Transaction) -> Self { +#[cfg(not(feature = "optimism"))] +impl From> for FoundryTransactionRequest { + fn from(tx: alloy_rpc_types_eth::Transaction) -> Self { tx.inner.into_inner().into() } } @@ -437,7 +458,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.as_ref().complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } @@ -448,9 +471,14 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { } fn can_build(&self) -> bool { - self.as_ref().can_build() - || self.complete_deposit().is_ok() - || self.complete_tempo().is_ok() + if self.as_ref().can_build() || self.complete_tempo().is_ok() { + return true; + } + #[cfg(feature = "optimism")] + if self.complete_deposit().is_ok() { + return true; + } + false } fn output_tx_type(&self) -> FoundryTxType { @@ -465,7 +493,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(), FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit().ok(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => self.complete_type(pref).ok(), FoundryTxType::Tempo => self.complete_tempo().ok(), }?; @@ -479,11 +509,21 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { let inner = self.as_mut(); inner.transaction_type = Some(preferred_type as u8); inner.gas.is_none().then(|| inner.set_gas_limit(Default::default())); - if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) { + let is_deposit = { + #[cfg(feature = "optimism")] + { + preferred_type == FoundryTxType::Deposit + } + #[cfg(not(feature = "optimism"))] + { + false + } + }; + if !is_deposit && preferred_type != FoundryTxType::Tempo { inner.trim_conflicting_keys(); inner.populate_blob_hashes(); } - if preferred_type != FoundryTxType::Deposit { + if !is_deposit { inner.nonce.is_none().then(|| inner.set_nonce(Default::default())); } if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) { @@ -548,42 +588,10 @@ impl TransactionBuilder4844 for FoundryTransactionRequest { } } -/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields -pub fn get_deposit_tx_parts( - other: &OtherFields, -) -> Result> { - let mut missing = Vec::new(); - let source_hash = - other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("sourceHash"); - Default::default() - }, - ); - let mint = other - .get_deserialized::("mint") - .transpose() - .unwrap_or_else(|_| { - missing.push("mint"); - Default::default() - }) - .map(|value| value.to::()); - let is_system_transaction = - other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("isSystemTx"); - Default::default() - }, - ); - if missing.is_empty() { - Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) - } else { - Err(missing) - } -} - #[cfg(test)] mod tests { + use alloy_primitives::B256; + use super::*; fn default_tx_req() -> TransactionRequest { @@ -618,6 +626,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_routing_op_by_deposit_fields() { let tx = default_tx_req(); let mut other = OtherFields::default(); @@ -669,6 +678,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_serialization_op() { let tx = default_tx_req(); let mut other = OtherFields::default(); diff --git a/crates/script-sequence/Cargo.toml b/crates/script-sequence/Cargo.toml index 7f112ce1bbda8..ce94945fb27cf 100644 --- a/crates/script-sequence/Cargo.toml +++ b/crates/script-sequence/Cargo.toml @@ -27,3 +27,7 @@ revm-inspectors.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index acdcbbdbf95ea..6de71571ac7eb 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -62,3 +62,15 @@ tempo-primitives.workspace = true [dev-dependencies] tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-cli/optimism", + "forge-script-sequence/optimism", + "forge-verify/optimism", +] diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 5ac7e4c669ade..f1bda2ccdcf55 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -1,8 +1,8 @@ -use std::{cmp::Ordering, sync::Arc, time::Duration}; +use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration}; use crate::{ - ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress, - sequence::ScriptSequenceKind, verify::BroadcastedState, + ScriptArgs, ScriptConfig, build::LinkedBuildData, needs_script_rpc_estimate, + progress::ScriptProgress, sequence::ScriptSequenceKind, verify::BroadcastedState, }; use alloy_chains::{Chain, NamedChain}; use alloy_consensus::{SignableTransaction, Signed}; @@ -21,11 +21,12 @@ use alloy_signer::Signature; use eyre::{Context, Result, bail}; use forge_verify::provider::VerificationProviderType; use foundry_cheatcodes::Wallets; -use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; +use foundry_cli::utils::has_batch_support; use foundry_common::{ FoundryTransactionBuilder, TransactionMaybeSigned, provider::{ProviderBuilder, try_get_http_provider}, shell, + tempo::TempoSponsor, }; use foundry_config::Config; use foundry_evm::core::evm::{FoundryEvmNetwork, TempoEvmNetwork}; @@ -100,7 +101,17 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result<()> { + let access_key_authorization = match self { + Self::AccessKey(_, _, access_key) => Some(( + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.clone(), + )), + _ => None, + }; + if let Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) @@ -137,11 +148,28 @@ where } } + if let Some((wallet_address, key_address, key_authorization)) = + access_key_authorization.as_ref() + { + tx.prepare_access_key_authorization( + provider, + *wallet_address, + *key_address, + key_authorization.as_ref(), + ) + .await?; + } + // Chains which use `eth_estimateGas` are being sent sequentially and require their // gas to be re-estimated right before broadcasting. if !is_fixed_gas_limit && estimate_via_rpc { estimate_gas(tx, provider, estimate_multiplier).await?; } + + if let Some(sponsor) = tempo_sponsor { + let from = tx.from().expect("no sender"); + sponsor.attach_and_print::(tx, from).await?; + } } Ok(()) @@ -211,6 +239,7 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result { self.prepare( &provider, @@ -218,6 +247,7 @@ where is_fixed_gas_limit, estimate_via_rpc, estimate_multiplier, + tempo_sponsor, ) .await?; @@ -387,6 +417,27 @@ impl BundledState { SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys } }; + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new); + if tempo_sponsor.is_some() && self.script_config.tempo.sponsor_sig.is_some() { + let remaining = self + .sequence + .sequences() + .iter() + .map(|sequence| { + sequence + .transactions() + .skip(sequence.receipts.len()) + .filter(|tx| tx.is_unsigned()) + .count() + }) + .sum::(); + if remaining > 1 { + eyre::bail!( + "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts" + ); + } + } + let progress = ScriptProgress::default(); for i in 0..self.sequence.sequences().len() { @@ -464,6 +515,11 @@ impl BundledState { let kind = match tx_with_metadata.tx().clone() { TransactionMaybeSigned::Signed { tx, .. } => { + if tempo_sponsor.is_some() { + eyre::bail!( + "cannot attach Tempo sponsor signature to an already signed script transaction" + ); + } SendTransactionKind::Signed(tx) } TransactionMaybeSigned::Unsigned(mut tx) => { @@ -487,6 +543,8 @@ impl BundledState { tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); } + self.script_config.tempo.apply::(&mut tx, None); + send_kind.for_sender(&from, tx)? } }; @@ -495,9 +553,13 @@ impl BundledState { }) .collect::>>()?; - let estimate_via_rpc = has_different_gas_calc(sequence.chain) - || self.script_config.evm_opts.networks.is_tempo() - || self.args.skip_simulation; + let estimate_via_rpc = needs_script_rpc_estimate( + sequence.chain, + self.script_config.evm_opts.networks.is_tempo(), + self.script_config.batch, + &self.script_config.tempo, + self.args.skip_simulation, + ); // We only wait for a transaction receipt before sending the next transaction, if // there is more than one signer. There would be no way of assuring @@ -525,6 +587,7 @@ impl BundledState { let pending_transactions = batch.iter().map(|(kind, is_fixed_gas_limit)| { let provider = provider.clone(); + let tempo_sponsor = tempo_sponsor.clone(); async move { let res = kind .clone() @@ -534,22 +597,36 @@ impl BundledState { *is_fixed_gas_limit, estimate_via_rpc, self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), ) .await; - (res, kind, 0, None) + (res, kind, *is_fixed_gas_limit, 0, None) } .boxed() }); let mut buffer = pending_transactions.collect::>(); - 'send: while let Some((res, kind, attempt, original_res)) = - buffer.next().await + 'send: while let Some(( + res, + kind, + is_fixed_gas_limit, + attempt, + original_res, + )) = buffer.next().await { - if res.is_err() && attempt <= 3 { + if res.is_err() + && self.script_config.tempo.sponsor_sig.is_some() + && attempt == 0 + { + debug!( + "not retrying transaction because --tempo.sponsor-sig is a static signature" + ); + } else if res.is_err() && attempt <= 3 { // Try to resubmit the transaction let provider = provider.clone(); let progress = seq_progress.inner.clone(); + let tempo_sponsor = tempo_sponsor.clone(); buffer.push(Box::pin(async move { debug!(err=?res, ?attempt, "retrying transaction "); let attempt = attempt + 1; @@ -557,8 +634,24 @@ impl BundledState { "retrying transaction {res:?} (attempt {attempt})" )); tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - let r = kind.clone().send(provider).await; - (r, kind, attempt, original_res.or(Some(res))) + let r = kind + .clone() + .prepare_and_send( + provider, + sequential_broadcast, + is_fixed_gas_limit, + estimate_via_rpc, + self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), + ) + .await; + ( + r, + kind, + is_fixed_gas_limit, + attempt, + original_res.or(Some(res)), + ) })); continue 'send; @@ -675,6 +768,7 @@ impl BundledState { let sequence = self.sequence.sequences_mut().get_mut(0).unwrap(); let provider = Arc::new(ProviderBuilder::::new(sequence.rpc_url()).build()?); + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?; // Collect sender addresses - batch mode requires single sender let senders: AddressHashSet = sequence @@ -794,16 +888,35 @@ impl BundledState { max_priority_fee_per_gas: Some(max_priority_fee_per_gas), ..Default::default() }, - fee_token: self.script_config.fee_token, + fee_token: self.script_config.tempo.common.fee_token, calls: calls.clone(), + nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX), + valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new), ..Default::default() }; + self.script_config.tempo.apply::(&mut batch_tx, None); + + if let BatchSigner::TempoKeychain(_, ak) = &batch_signer { + batch_tx.key_id = Some(ak.key_address); + batch_tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } // Estimate gas for the batch transaction estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier).await?; sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut batch_tx, sender).await?; + } + // Sign and send let tx_hash = match batch_signer { BatchSigner::Wallet(wallet) => { @@ -816,8 +929,6 @@ impl BundledState { *pending.tx_hash() } BatchSigner::TempoKeychain(signer, access_key) => { - batch_tx.key_id = Some(access_key.key_address); - let raw_tx = batch_tx .sign_with_access_key( provider.as_ref(), diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index f5e4d46de0344..ea906c9b872e1 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -28,8 +28,8 @@ use eyre::{ContextCompat, Result}; use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::{RetryArgs, VerifierArgs}; use foundry_cli::{ - opts::{BuildOpts, EvmArgs, GlobalArgs}, - utils::{LoadConfig, parse_fee_token_address}, + opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts}, + utils::{LoadConfig, has_different_gas_calc}, }; use foundry_common::{ CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN, @@ -44,11 +44,13 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ backend::Backend, core::{ Breakpoints, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, tempo::PATH_USD_ADDRESS, }, executors::ExecutorBuilder, @@ -140,9 +142,9 @@ pub struct ScriptArgs { #[arg(long, requires = "batch", default_value = "100")] pub batch_size: usize, - /// Tempo fee token address for paying transaction fees. - #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] - pub fee_token: Option
, + /// Tempo transaction options. + #[command(flatten)] + pub tempo: TempoOpts, /// Skips on-chain simulation. #[arg(long)] @@ -246,13 +248,43 @@ pub struct ScriptArgs { pub retry: RetryArgs, } +const fn should_default_tempo_fee_token( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + // Plain `--network tempo` should stay an ordinary transaction; only Tempo AA opts get defaults. + is_tempo_network && tempo.common.fee_token.is_none() && (batch || tempo.is_tempo()) +} + +const fn needs_tempo_aa_rpc_estimate( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + is_tempo_network && (batch || tempo.is_tempo()) +} + +pub(crate) fn needs_script_rpc_estimate( + chain_id: u64, + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, + skip_simulation: bool, +) -> bool { + // Tempo AA needs RPC estimation; plain Tempo scripts can use the local simulation result. + (has_different_gas_calc(chain_id) && !is_tempo_network) + || needs_tempo_aa_rpc_estimate(is_tempo_network, batch, tempo) + || skip_simulation +} + impl ScriptArgs { /// Loads config, resolves evm_opts (including network inference from fork), and returns them. async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> { let (config, mut evm_opts) = self.load_config_and_evm_opts()?; - if self.fee_token.is_some() { - // If fee token is set directly select tempo + if self.tempo.is_tempo() { + // If fee token or expiry is set directly select tempo evm_opts.networks = NetworkConfigs::with_tempo(); } else { // Auto-detect network from fork chain ID when not explicitly configured. @@ -285,13 +317,14 @@ impl ScriptArgs { } } - let fee_token = if evm_opts.networks.is_tempo() && self.fee_token.is_none() { - Some(PATH_USD_ADDRESS) - } else { - self.fee_token - }; + let mut tempo = self.tempo.clone(); + tempo.resolve_expires(); + + if should_default_tempo_fee_token(evm_opts.networks.is_tempo(), self.batch, &tempo) { + tempo.common.fee_token = Some(PATH_USD_ADDRESS); + } - let script_config = ScriptConfig::new(config, evm_opts, self.batch, fee_token).await?; + let script_config = ScriptConfig::new(config, evm_opts, self.batch, tempo).await?; Ok(PreprocessedState { args: self, script_config, script_wallets, browser_wallet }) } @@ -320,12 +353,15 @@ impl ScriptArgs { if broadcasted.args.verify { broadcasted.verify().await?; } - Ok(()) - } else if evm_opts.networks.is_optimism() { - self.run_generic_script::(config, evm_opts).await - } else { - self.run_generic_script::(config, evm_opts).await + return Ok(()); } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_generic_script::(config, evm_opts).await; + } + + self.run_generic_script::(config, evm_opts).await } /// Prepares the bundled state (compile, simulate, bundle) and returns it @@ -708,8 +744,8 @@ pub struct ScriptConfig { pub backends: HashMap>, /// Whether to batch all broadcast transactions into a single Tempo batch transaction. pub batch: bool, - /// Tempo fee token address for paying transaction fees. - pub fee_token: Option
, + /// Tempo transaction options applied to broadcast transactions. + pub tempo: TempoOpts, } impl ScriptConfig { @@ -717,7 +753,7 @@ impl ScriptConfig { config: Config, evm_opts: EvmOpts, batch: bool, - fee_token: Option
, + tempo: TempoOpts, ) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await? @@ -726,7 +762,7 @@ impl ScriptConfig { 1 }; - Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, fee_token }) + Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, tempo }) } pub async fn update_sender(&mut self, sender: Address) -> Result<()> { @@ -802,7 +838,7 @@ impl ScriptConfig { self.evm_opts.clone(), Some(known_contracts), Some(target), - self.fee_token, + self.tempo.common.fee_token, ) .into(), ) @@ -813,7 +849,7 @@ impl ScriptConfig { // Propagate fee token to the transaction environment so that internal EVM calls // (e.g. script deployment, setUp) use the correct fee token for Tempo networks. - tx_env.set_fee_token(self.fee_token); + tx_env.set_fee_token(self.tempo.common.fee_token); Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())) } @@ -823,6 +859,7 @@ impl ScriptConfig { mod tests { use super::*; use alloy_network::Ethereum; + use alloy_primitives::address; use foundry_config::{NamedChain, UnresolvedEnvVarError}; use std::fs; use tempfile::tempdir; @@ -834,6 +871,50 @@ mod tests { assert_eq!(args.sig, sig); } + #[test] + fn can_parse_shared_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.fee-token", + "1", + "--tempo.expires", + "10", + ]); + + assert_eq!( + args.tempo.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000001")) + ); + assert_eq!(args.tempo.common.expires, Some(10)); + } + + #[test] + fn can_parse_sponsor_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]); + + assert_eq!( + args.tempo.sponsor, + Some(address!("0x1111111111111111111111111111111111111111")) + ); + assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + } + + #[test] + fn can_parse_full_tempo_opts() { + let args = + ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]); + + assert_eq!(args.tempo.nonce_key, Some(U256::from(1))); + } + #[test] fn can_parse_unlocked() { let args = ScriptArgs::parse_from([ diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index e2404d60ce2b9..b085f8eaf4545 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -6,7 +6,7 @@ use alloy_network::TransactionBuilder; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; -use foundry_common::{FoundryTransactionBuilder, TransactionMaybeSigned}; +use foundry_common::TransactionMaybeSigned; use foundry_config::Config; use foundry_evm::{ constants::CALLER, @@ -84,9 +84,7 @@ impl ScriptRunner { .with_input(code.clone()) .with_nonce(sender_nonce + library_transactions.len() as u64); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), @@ -122,9 +120,7 @@ impl ScriptRunner { .with_nonce(sender_nonce + library_transactions.len() as u64) .with_to(create2_deployer); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index ef10a1ce94082..fe1e9345fa23c 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -129,7 +129,7 @@ impl VerifyBundle { path: Some(artifact.source.to_string_lossy().to_string()), name: artifact .name - .strip_suffix(&format!(".{}", &artifact.profile)) + .strip_suffix(&format!(".{}", artifact.profile)) .unwrap_or_else(|| &artifact.name) .to_string(), }; diff --git a/crates/sol-macro-gen/Cargo.toml b/crates/sol-macro-gen/Cargo.toml index 69ea952d4d040..d3ad56a96cdd2 100644 --- a/crates/sol-macro-gen/Cargo.toml +++ b/crates/sol-macro-gen/Cargo.toml @@ -27,3 +27,7 @@ prettyplease.workspace = true eyre.workspace = true heck.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index d29f6358e93d2..8dc652bcb32bd 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -44,3 +44,7 @@ idna_adapter.workspace = true [dev-dependencies] tokio.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/verify/Cargo.toml b/crates/verify/Cargo.toml index 65a202911509f..e3372a2494f34 100644 --- a/crates/verify/Cargo.toml +++ b/crates/verify/Cargo.toml @@ -48,3 +48,12 @@ url.workspace = true tokio = { workspace = true, features = ["macros"] } foundry-test-utils.workspace = true tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-cli/optimism", +] diff --git a/docs/dev/lintrules.md b/docs/dev/lintrules.md index 6f5dbbd850784..969d7effe142f 100644 --- a/docs/dev/lintrules.md +++ b/docs/dev/lintrules.md @@ -60,6 +60,8 @@ Next, choose whether you want an [early or late lint pass](#choosing-between-ear - Implement the appropriate trait logic (`EarlyLintPass` or `LateLintPass`) for your lint. Do it in a new file within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). +- Add a markdown documentation file for the lint at `crates/lint/docs/.md`. The file is referenced by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the [Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. Use [`crates/lint/docs/_template.md`](../../crates/lint/docs/_template.md) as a starting point. The presence of this file is enforced by the `registered_lints_have_docs` unit test in `crates/lint/src/sol/mod.rs`. + ### Choosing Between Early and Late Passes - **Use `EarlyLintPass`** for: diff --git a/flake.lock b/flake.lock index 27f426f491da6..343a79c0cda3d 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1777102577, - "narHash": "sha256-ycoy9svZOQgyInu/lwO7IEQtlP5liqYhEcF9m9hPRbM=", + "lastModified": 1777708550, + "narHash": "sha256-Qif3UXT0l5OQq8H9pRWt4/ia4gF48MWK2oHKL8uVx8U=", "owner": "nix-community", "repo": "fenix", - "rev": "f37403486c59376cd285f9685a8ef8ff25c09a3c", + "rev": "74c1591efaff494756b8d35ebe357c6c2bbdca96", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1777641297, + "narHash": "sha256-WNGcmeOZ8Tr9dq6ztCspYbzWFswr2mPebM9LpsfGxPk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "c6d65881c5624c9cae5ea6cedef24699b0c0a4c0", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1776800521, - "narHash": "sha256-f8YJfwAOsLFpIoqZuX3yF69UvMLrkx7iVzMH1pJU7cM=", + "lastModified": 1777639980, + "narHash": "sha256-6d7Hdurvbjc5uwJuc0YiK7rZBGj6Gs3uzfBFcTs+xCc=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "8954b66d43225e62c92e8bbcc8500191b5cceb1e", + "rev": "64cdaeb06f69b6b769a492edd88b022ae88e8ca2", "type": "github" }, "original": { diff --git a/foundryup/README.md b/foundryup/README.md index 29e91378929cc..2cf61f8725227 100644 --- a/foundryup/README.md +++ b/foundryup/README.md @@ -30,10 +30,10 @@ To install the latest **nightly** version: foundryup --install nightly ``` -To install a specific version (e.g. `v1.6.0`): +To install a specific version (e.g. `v1.7.0`): ```sh -foundryup --install v1.6.0 +foundryup --install v1.7.0 ``` To **list** all **versions** installed: diff --git a/foundryup/foundryup b/foundryup/foundryup index 7576501b4619e..5fb086a75f8c1 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -3,7 +3,7 @@ set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -FOUNDRYUP_INSTALLER_VERSION="1.8.1" +FOUNDRYUP_INSTALLER_VERSION="1.8.3" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} @@ -15,6 +15,13 @@ FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" FOUNDRYUP_JOBS="" FOUNDRYUP_IGNORE_VERIFICATION=false +# Retry/backoff settings used for `fetch` (GitHub API calls). +# Recovers from transient HTTP 403/429/5xx responses returned by +# api.github.com under heavy load or per-IP rate limiting. +FOUNDRYUP_MAX_RETRIES=5 +FOUNDRYUP_RETRY_DELAY=2 +FOUNDRYUP_RETRY_MAX_TIME=60 + BINS=(forge cast anvil chisel) HASH_NAMES=() HASH_VALUES=() @@ -111,51 +118,7 @@ main() { # Install by downloading binaries if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-latest} - - # Normalize versions (handle channels, versions without v prefix) - if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then - # Resolve to the latest release (non-prerelease) via the GitHub API - say "fetching latest release from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' - /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } - ') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a latest release for ${FOUNDRYUP_REPO}" - fi - say "resolved release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" - elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then - # Resolve to the latest nightly (prerelease) release via the GitHub API. - # The GitHub API does not guarantee that releases are returned in - # chronological order, so we collect all matching nightlies along with - # their `published_at` timestamps and sort them ourselves. - say "fetching latest nightly releases from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' - /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } - /"published_at"[[:space:]]*:[[:space:]]*"/ { - pub=$0 - gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub) - gsub(/".*/, "", pub) - if (tag ~ /^nightly-/) print pub "\t" tag - tag="" - } - ' | sort -r | awk -F '\t' 'NR==1 { print $2 }') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a nightly release for ${FOUNDRYUP_REPO}" - fi - say "resolved nightly release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then - # Specific nightly tag (e.g. nightly-abc123...) - FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then - # Add v prefix - FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - else - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - fi + resolve_version_and_tag say "installing foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})" @@ -179,7 +142,7 @@ main() { tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp_dir/attestation.txt" ensure download "$ATTESTATION_URL" "$tmp" - + # Read the first line of the attestation file to get the artifact link. # The first line should contain the link to the attestation artifact. attestation_artifact_link="$(head -n1 "$tmp" | tr -d '\r')" @@ -292,7 +255,7 @@ main() { else say 'skipping manpage download: missing "tar"' fi - + if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = true ]; then say "skipped SHA verification for downloaded binaries due to --force flag" else @@ -505,6 +468,18 @@ list() { use() { [ -z "$FOUNDRYUP_VERSION" ] && err "no version provided" + + # If the requested version is a channel (`latest`, `stable`, `nightly`) or a bare semver + # version (e.g. `1.7.0`, `1.6.0-rc1`), resolve it to the immutable tag directory created by + # `--install` (channels hit the GitHub API; semver versions get a `v` prefix). + # Falls back to the literal value for locally-built versions (branches, PRs, commits, custom names). + case "$FOUNDRYUP_VERSION" in + latest|stable|nightly|[0-9]*.[0-9]*.[0-9]*) + resolve_version_and_tag + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + ;; + esac + FOUNDRY_VERSION_DIR="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" if [ -d "$FOUNDRY_VERSION_DIR" ]; then @@ -683,12 +658,70 @@ ensure() { if ! "$@"; then err "command failed: $*"; fi } -# Silently fetches $1 to stdout +# Normalizes `FOUNDRYUP_VERSION` and resolves it to a concrete release tag, +# populating `FOUNDRYUP_TAG`. Handles the `latest`/`stable`/`nightly` channels +# (looked up via the GitHub API). +resolve_version_and_tag() { + FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} + if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then + # Resolve to the latest release (non-prerelease) via the GitHub API. + say "fetching latest release tag from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' + /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } + ') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a latest release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then + # Resolve to the latest nightly (prerelease) release via the GitHub API. + # The GitHub API does not guarantee that releases are returned in + # chronological order, so we collect all matching nightlies along with + # their `published_at` timestamps and sort them ourselves. + say "fetching latest nightly release tags from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' + /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } + /"published_at"[[:space:]]*:[[:space:]]*"/ { + pub=$0 + gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub) + gsub(/".*/, "", pub) + if (tag ~ /^nightly-/) print pub "\t" tag + tag="" + } + ' | sort -r | awk -F '\t' 'NR==1 { print $2 }') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a nightly release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved nightly release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then + # Specific nightly tag (e.g. nightly-abc123...) + FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then + # Add v prefix + FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + else + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + fi +} + +# Silently fetches $1 to stdout. fetch() { if check_cmd curl; then - curl -fsSL "$1" + curl -fsSL \ + --retry "$FOUNDRYUP_MAX_RETRIES" \ + --retry-delay "$FOUNDRYUP_RETRY_DELAY" \ + --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \ + --retry-all-errors \ + "$1" else - wget -qO- "$1" + wget --tries="$FOUNDRYUP_MAX_RETRIES" \ + --waitretry="$FOUNDRYUP_RETRY_DELAY" \ + --retry-on-http-error=403,408,429,500,502,503,504 \ + -qO- "$1" fi } diff --git a/testdata/default/cheats/ExpectRevert.t.sol b/testdata/default/cheats/ExpectRevert.t.sol index 839d97962aa94..ae0c8ed844f5d 100644 --- a/testdata/default/cheats/ExpectRevert.t.sol +++ b/testdata/default/cheats/ExpectRevert.t.sol @@ -305,6 +305,91 @@ contract ExpectRevertWithReverterTest is Test { vm.expectRevert(address(cContract)); aContract.createDContractThroughCContract(); } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // reverts directly, the reverter address argument must be enforced (it used to + // be silently ignored). The matched reverter is the would-be-deployed address. + function testExpectRevertsWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new DContract(); + + expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(abi.encodePacked("Reverted by DContract"), expected); + new DContract(); + } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // synchronously creates another contract that reverts (i.e. innermost frame is + // a CREATE), the matched reverter is the outer would-be-deployed address (the + // contract whose deployment failed). + function testExpectRevertsWithReverterNestedCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new NestedDContractCreator(); + } + + // + // Regression: `expectPartialRevert(bytes4, address)` overload must enforce + // the reverter address argument when matching a top-level CREATE revert. + function testExpectPartialRevertWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + // `Reverted by DContract` triggers Solidity's `Error(string)` selector. + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), expected); + new DContract(); + } + + // + // Regression: `expectRevert(bytes4, address)` (exact 4-byte selector + reverter) + // overload must enforce the reverter address argument for a top-level CREATE. + function testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(DCustomErrorContract.CustomError.selector, expected); + new DCustomErrorContract(); + } + + // + // Regression: `expectRevert(address, uint64)` count-bearing overload must + // exercise the `count > 1` branch in `create_end`. Use CREATE2 with the same + // salt so both deploys would resolve to the same would-be address (each + // constructor reverts so no contract is ever actually placed there). + function testExpectRevertsWithReverterCountTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0x42)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected, 2); + new DContract{salt: salt}(); + new DContract{salt: salt}(); + } + + // + // Regression: CREATE2 deploys must also enforce the reverter address argument. + function testExpectRevertsWithReverterTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0xC0FFEE)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected); + new DContract{salt: salt}(); + } +} + +// Used by `testExpectRevertsWithReverterNestedCreate`: a contract whose constructor +// directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } +} + +// Used by `testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate`: constructor +// reverts with a parameter-less custom error so the full revert data is exactly the +// 4-byte selector. +contract DCustomErrorContract { + error CustomError(); + + constructor() { + revert CustomError(); + } } contract ExpectRevertCount is Test { diff --git a/testdata/default/cheats/GetFoundryVersion.t.sol b/testdata/default/cheats/GetFoundryVersion.t.sol index 6139b8b6b6a5e..f01b7cdd7d213 100644 --- a/testdata/default/cheats/GetFoundryVersion.t.sol +++ b/testdata/default/cheats/GetFoundryVersion.t.sol @@ -84,4 +84,55 @@ contract GetFoundryVersionTest is Test { // Should return true for past versions assertTrue(vm.foundryVersionAtLeast("0.2.0")); } + + /// Returns the `MAJOR.MINOR.PATCH` prefix of `vm.getFoundryVersion()`, + /// stripping any pre-release suffix (`-nightly`, `-dev`, …) and the + /// `+..` build metadata. + function _semverPrefix() internal view returns (string memory) { + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + require(plusSplit.length == 2, "Invalid version format: Missing '+' separator"); + string[] memory dashSplit = vm.split(plusSplit[0], "-"); + return dashSplit[0]; + } + + function testGetFoundryVersionMajorMinorPatchIsParseable() public view { + // The MAJOR.MINOR.PATCH prefix must always be three numeric components, + // regardless of build kind (tagged release / nightly / dev). + string[] memory parts = vm.split(_semverPrefix(), "."); + require(parts.length == 3, "Invalid semver prefix: expected MAJOR.MINOR.PATCH"); + // Each component must parse as a uint (this reverts on garbage). + vm.parseUint(parts[0]); + vm.parseUint(parts[1]); + vm.parseUint(parts[2]); + } + + function testGetFoundryVersionBuildProfile() public view { + // The build profile must be present and non-empty (e.g. "debug", "release", "dist", …). + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + string[] memory metadataComponents = vm.split(plusSplit[1], "."); + require(bytes(metadataComponents[2]).length > 0, "Build profile is empty"); + } + + function testFoundryVersionCmpAndAtLeastAreConsistent() public { + // `foundryVersionAtLeast(v)` must equal `foundryVersionCmp(v) >= 0` for any input. + string[3] memory probes = ["0.0.1", _semverPrefix(), "99.0.0"]; + for (uint256 i = 0; i < probes.length; i++) { + assertEq(vm.foundryVersionAtLeast(probes[i]), vm.foundryVersionCmp(probes[i]) >= 0); + } + } + + function testFoundryVersionCmpRejectsPreRelease() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0-nightly"); + } + + function testFoundryVersionCmpRejectsBuildMetadata() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0+abc1234567.1700000000.release"); + } + + function testFoundryVersionCmpRejectsInvalidVersion() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("not-a-version"); + } } diff --git a/testdata/default/cheats/MockCall.t.sol b/testdata/default/cheats/MockCall.t.sol index e2ac74d6f70fa..d8019ab4f6ee8 100644 --- a/testdata/default/cheats/MockCall.t.sol +++ b/testdata/default/cheats/MockCall.t.sol @@ -158,6 +158,35 @@ contract MockCallTest is Test { assertEq(mock.pay{value: 50}(1), 100); } + function testMockCallWithValueTransfersBalance() public { + Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + assertEq(address(mock).balance, 0); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + } + + function testMockCallWithValueTransfersPrankedSenderBalance() public { + Mock mock = new Mock(); + address sender = address(0xBEEF); + uint256 value = 10; + vm.deal(address(this), 0); + vm.deal(sender, value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + vm.prank(sender); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + assertEq(sender.balance, 0); + } + function testMockCallWithValueCalldataPrecedence() public { Mock mock = new Mock(); @@ -279,17 +308,25 @@ contract MockCallRevertTest is Test { function testMockCallRevertWithValue() public { Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); - vm.mockCallRevert(address(mock), 10, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); + vm.mockCallRevert(address(mock), value, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); assertEq(mock.pay(1), 1); assertEq(mock.pay(2), 2); - try mock.pay{value: 10}(1) { + uint256 initSenderBalance = address(this).balance; + uint256 initTargetBalance = address(mock).balance; + + try mock.pay{value: value}(1) { revert(); } catch (bytes memory err) { require(keccak256(err) == keccak256(ERROR_MESSAGE)); } + + assertEq(address(this).balance, initSenderBalance); + assertEq(address(mock).balance, initTargetBalance); } function testMockCallResetsMockCallRevert() public { diff --git a/testdata/default/cheats/MockCalls.t.sol b/testdata/default/cheats/MockCalls.t.sol index e0f5eef151db6..777543f28e361 100644 --- a/testdata/default/cheats/MockCalls.t.sol +++ b/testdata/default/cheats/MockCalls.t.sol @@ -28,13 +28,17 @@ contract MockCallsTest is Test { mocks[0] = abi.encode(2 ether); mocks[1] = abi.encode(1 ether); mocks[2] = abi.encode(6.423 ether); + vm.deal(address(this), 3 ether); vm.mockCalls(mockErc20, 1 ether, data, mocks); (, bytes memory ret1) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret1, (uint256)), 2 ether); + assertEq(mockErc20.balance, 1 ether); (, bytes memory ret2) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret2, (uint256)), 1 ether); + assertEq(mockErc20.balance, 2 ether); (, bytes memory ret3) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret3, (uint256)), 6.423 ether); + assertEq(mockErc20.balance, 3 ether); } function testMockCalls() public { From 53c39b4bd4832c5539b28900a9c63df4006e7cc7 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 01:06:45 +0700 Subject: [PATCH 09/14] ci: sign release archives, docker images, and publish SBOMs (#519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * anvil: unify Tempo nonce markers across send RPCs (#14536) Co-authored-by: Amp Co-authored-by: steven Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Matthias Seitz * fix(forge): `flaky_gas_report_fallback_with_calldata` deployment cost (#14545) * chore(lint): add missing lints to README (#14551) * chore(bench): update `benchmark.sh` (#14548) Co-authored-by: Matthias Seitz * chore(clippy): fix for_kv_map and useless_borrows_in_formatting (#14554) * chore(clippy): fix for_kv_map and useless_borrows_in_formatting Amp-Thread-ID: https://ampcode.com/threads/T-019df0f9-62e7-74b8-bd5e-da2acce678fb Co-authored-by: Amp * chore(clippy): drop redundant borrows in cheatcodes assert formatters Amp-Thread-ID: https://ampcode.com/threads/T-019df0f9-62e7-74b8-bd5e-da2acce678fb Co-authored-by: Amp --------- Co-authored-by: Amp * fix(ci): use `PATH_USD` fallback fee token in Mail templates (#14546) * chore(deps): bump the actions-weekly group with 3 updates (#14497) * refactor(chisel): migrate to solar (#14532) * feat(lint): add too-many-digits lint (#14549) * feat: feature-gate optimism deps in common-fmt, common, cast (#14539) * feat(forge): support per-test network selection via inline config (#14530) * feat(cli): `--tempo.expires` retry-safe mode (TIP-1009 expiring nonces) (#14521) * fix(forge): `per_test_network_routing` match undeterministic order (#14557) output * chore(ci): run tempo mainnet and testnet checks before devnet (#14556) * Update flake.lock (#14553) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/f374034' (2026-04-25) → 'github:nix-community/fenix/74c1591' (2026-05-02) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/8954b66' (2026-04-21) → 'github:rust-lang/rust-analyzer/64cdaeb' (2026-05-01) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/01fbdee' (2026-04-23) → 'github:NixOS/nixpkgs/c6d6588' (2026-05-01) Co-authored-by: github-actions[bot] * chore(bench): update benchmark results (#14552) * fix(forge): ignore ETH_RPC_URL for test forking (#14555) Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(cast): add Tempo keychain policy commands (#14531) * feat(cast): add tempo keychain policy commands * fix(cast): address keychain policy review * fix(cli): fix jsonwebtoken panic (#14562) `cast` panicked with this message coming from jsonwebtoken: ``` Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'rust_crypto' and 'aws_lc_rs' features is enabled. See the documentation of the CryptoProvider type for more information. ``` This seemingly was introduced with the bump of jsonwebtoken to 10. Now it requires you to pick one backend used by default controlled by the compile time cargo features or call `CryptoProvider::install_default()` at the beginning. I realized that probably it would be better to just select the feature and I picked `aws_lc_rs` as it seems to be increasingly a default and we already are using the C toolchain. Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore(cli): tidy ETH_RPC_URL handling and add forge regression test (#14559) Follow-up to #14555: - Drop the redundant flashbots branch in RpcOpts::dict; self.url(None) already returns FLASHBOTS_URL when --flashbots is set, so the subsequent overwrite was dead code. - Inline the resolve_rpc_url helper back into RpcCommonOpts::url; it was only called from one place and added unneeded surface area. - Restore the doc comment on RpcCommonOpts and document why ETH_RPC_URL is intentionally not a clap env on the shared field (so EvmArgs cannot inherit it). - Add an integration test that runs forge config with ETH_RPC_URL set in the environment and asserts that eth_rpc_url stays None, directly exercising the regression scenario from #14538. Amp-Thread-ID: https://ampcode.com/threads/T-019df243-267f-7779-93e1-5d6686082444 Co-authored-by: zerosnacks Co-authored-by: Amp * feat(cast): open Tempo wallet fund flow for MPP failures (#14505) * feat(cast): open Tempo wallet fund flow for MPP failures * ci(tempo): skip network checks without rpc secrets * Revert "ci(tempo): skip network checks without rpc secrets" This reverts commit f8dd70163f850b854888fd1c962174e1663284f4. * fix(common): address mpp funding review --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * ci: sign release archives, docker images, and publish SBOMs (#14563) - release.yml: emit per-archive sha256 + SPDX SBOM (Syft), cosign keyless sign-blob of the archive, and use actions/attest@v4 for both build provenance and SBOM attestations. Upload all artifacts to the draft release. - docker-publish.yml: enable BuildKit SBOM, capture the build digest, cosign keyless sign each pushed tag, and publish a Sigstore-signed SLSA provenance attestation via actions/attest with push-to-registry. - SECURITY.md: document how external users verify archives and the docker image (gh attestation, cosign, plain sha256, buildx imagetools). - README.md: link to the new verification section. * perf(common): short-circuit `find_by_name_or_identifier` instead of `collect` (#14514) * feat(foundryup): retry GitHub API fetches on transient errors (#14566) GitHub api.github.com occasionally returns transient 403s on certain VMs (per-IP rate limiting / WAF hiccups), causing foundryup to fail to resolve the latest stable / nightly release tag, e.g.: foundryup: fetching latest nightly releases from foundry-rs/foundry... Error: curl: (56) The requested URL returned error: 403 foundryup: failed to fetch releases from GitHub API Add curl/wget retry logic to the `fetch` helper (used exclusively for GitHub API releases endpoints): - curl: --retry 5 --retry-delay 2 --retry-max-time 60, plus --retry-all-errors when supported (curl 7.71+, feature-detected so older curl does not hard-fail). --retry-all-errors is required to retry HTTP 403, which is not in curl's default retryable set. - wget fallback: --tries=5 --waitretry=2 --retry-on-http-error=403,408,429,5xx. `fetch` now buffers to a temp file before emitting to stdout, since curl's --retry-all-errors is unsafe with piped consumers (mid-stream retries can duplicate bytes). Existing callers pipe into awk/grep. Tunable via FOUNDRYUP_MAX_RETRIES (default 5). `download` (binary tarballs, attestations, manpages) is intentionally left unchanged — those rarely fail and changing them affects the attestation existence check semantics. Bumps installer version 1.8.1 -> 1.8.2. Amp-Thread-ID: https://ampcode.com/threads/T-019df2f5-9b97-717a-b959-cf7cbc7ca3bb Co-authored-by: Amp * feat(lint): project-wide passes + pragma-inconsistent (#14543) * feat(lint): project-wide passes + pragma-inconsistent * rm hashset, msg * test(lint): exhaustive pragma-inconsistent coverage + clearer testdata names (#14561) * test(lint): exhaustive coverage for pragma-inconsistent Follow-up to #14543 expanding test coverage for the cross-file `pragma-inconsistent` lint across the syntax variants users encounter in real Solidity projects. Multi-file scenarios (added as `forgetest!` cases in `crates/forge/tests/cli/lint.rs`, since they cannot be expressed in a single `.sol` testdata file): - Negative (must NOT warn): - all files use the same exact pragma (`0.8.20`) - all files use the same caret pragma (`^0.8.20`) - single file in the project - Positive (must warn): - duplicates among a conflict -- two identical files plus one different pragma still emits three warnings - Mixed: - file without an explicit pragma uses the test-utils default (`add_raw_source` is used to bypass the auto-injected pragma) Source bodies are pulled out into module-level `const` raw strings so rustfmt does not collapse the inline `\n`-escaped strings into wide horizontal blobs. Single-file scenarios (added as `.sol` files under `crates/lint/testdata/` in the existing `//~NOTE:` annotation style): - `PragmaInconsistentCaretVsTilde.sol`: `^0.8.20` vs `~0.8.20` - `PragmaInconsistentRangeVsExact.sol`: `>=0.8.0 <0.9.0` vs `0.8.20` -- range satisfies exact but lint is intentionally string-based, matching SLITHER-W1078 - `PragmaInconsistentOrVsExact.sol`: `0.8.20 || 0.8.21` vs `0.8.20` - `PragmaInconsistentThreeDistinct.sol`: `>=0.8.0`, `^0.8.0`, `~0.8.0` -- verifies the `others` list contains every other variant * test(lint): rename pragma-inconsistent testdata to describe the case under test The two testdata files added in #14543 were named `PragmaInconsistent.sol` and `PragmaInconsistent2.sol`, which made them look like duplicates. They actually exercise distinct edge cases of the same string-based detection: - `PragmaInconsistentCaretAboveExact.sol` (was `PragmaInconsistent.sol`): caret range whose lower bound is strictly below the exact version (`^0.8.0` + `0.8.18`). - `PragmaInconsistentCaretMatchesExact.sol` (was `PragmaInconsistent2.sol`): caret range whose lower bound equals the exact version (`^0.8.20` + `0.8.20`) -- the looks-the-same-but-still-distinct case that guards SLITHER-W1078 parity (no semver intersection). Amp-Thread-ID: https://ampcode.com/threads/T-019df243-267f-7779-93e1-5d6686082444 Co-authored-by: Amp --------- Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * refactor(script): reuse shared Tempo CLI opts (#14558) * deps: bump tempo to 6bf9903 (T6 hardfork) + fix alloy-evm 0.34 compat (#14567) * deps: bump tempo to 6bf9903 (T6 hardfork) Bumps tempo crates to 6bf9903d, adding the T6 hardfork variant to TempoHardfork. Without this, cast's tempo_forkSchedule lookup parses the chain's reported active fork ("T6") into TempoHardfork::FromStr, fails because T6 was unknown to the enum, and silently returns is_hardfork_active(T3) = false. That made 'cast keychain auth' fall back to the legacy authorizeKey selector and revert with LegacyAuthorizeKeySelectorChanged on any T6 chain. Also bumps alloy-evm to 0.34 and the optimism git pin to develop (e3b59e7) so alloy-op-evm picks up an EvmFactory impl built against alloy-evm 0.34. Removes the now-unused paradigmxyz/reth-core [patch] entries. No source changes; lockfile churn is transitive only. * fix: adapt AnvilBlockExecutor to alloy-evm 0.34.0 breaking changes - Add Send + 'static bounds to TxResult impl for AnvilTxResult - Change commit_transaction return type from Result to GasOutput - Remove .expect() on commit_transaction call site Amp-Thread-ID: https://ampcode.com/threads/T-019df322-c0f1-73e7-858c-5ca2d242ddb4 * style: rustfmt commit_transaction signature Amp-Thread-ID: https://ampcode.com/threads/T-019df322-c0f1-73e7-858c-5ca2d242ddb4 --------- Co-authored-by: Centaur AI * docs: add forge lint rule docs (#14571) * feat(forge): add fuzz run selection (#14522) * feat(forge): add fuzz run selection * fix(fuzz): make metadata builder const * test(fuzz): cover generated seed replay * fix(forge): persist fuzz worker for run replay * fix(evm): satisfy clippy in fuzz replay * fix(fuzz): reuse fuzz run metadata * forge(lint/docs): validate deployed forge lint docs (#14573) test: validate deployed forge lint docs * feat: gate foundry-primitives behind optimism feature (#14572) * fix(ci): increase permissions for the enhanced attestation writing (#14584) * increase permissions for artifact writing * apply write permissions to release-docker * feat(hardforks, networks): gate optimism behind cargo feature (#14581) * fix(forge): encode Tempo creates as AA calls (#14585) * feat(anvil): gate optimism behind cargo feature (#14577) Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(cast): introduce `vaddr` cmd for TIP-1022 (#14508) * feat(cast): introduce `vaddr` cmd for tip-1022 * fix: doc * chore: touch-ups * add tests * chore: move tests to tempo ci * feat: add vaddr watch test * feat: count 0 hadling, add `no_register` flag * fix: remove sweep subcommand * fix: make clippy happy * feat(bench): nightly regression tracking workflow (#14586) * fix(cli): fix release version strings for immutable tags, bump to 1.7.1 (#14496) * Fix release version metadata for immutable tags Amp-Thread-ID: https://ampcode.com/threads/T-019dd617-b29f-7409-8523-9858a1504f17 Co-authored-by: Amp * Derive nightly release suffix from commit SHA Amp-Thread-ID: https://ampcode.com/threads/T-019dd617-b29f-7409-8523-9858a1504f17 Co-authored-by: Amp * Apply suggestion from @zerosnacks * Apply suggestion from @zerosnacks * Apply suggestion from @zerosnacks * bump to v1.7.1 * avoid appending whole sha hash, not necessary, handle version cmp correctly. after v1.7.1 release we need to bump to v1.7.2 for nightlies following it to compare correctly * Make foundryVersionCmp tolerate new version format and add tests - Strip both pre-release ('-nightly', '-dev') and build metadata ('+..') from SEMVER_VERSION before comparison so the cheatcode keeps working for tagged releases (which have no '-' separator). - Extract strip_semver_metadata helper and add Rust unit tests covering all SEMVER_VERSION shapes, version_cmp ordering, and parse_version rejection of pre-release/build/garbage input. - Extend the Solidity test suite for vm.getFoundryVersion()/foundryVersionCmp/foundryVersionAtLeast: validate MAJOR.MINOR.PATCH parseability, build profile value, cmp/atLeast invariant, and error paths for invalid user-supplied versions. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * fix(test): drop view from solidity tests using assert helpers and fix fmt - assertTrue/assertEq aren't view, so testGetFoundryVersionBuildProfile and testFoundryVersionCmpAndAtLeastAreConsistent can't be view either. - Collapse the buildType assertion onto one line to satisfy forge fmt. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * test(version): assert build profile is non-empty instead of debug|release The dist profile (used for distributed release binaries) is also valid; just require non-empty so any future profile works. Amp-Thread-ID: https://ampcode.com/threads/T-019dd971-fcb7-7149-9680-f0134130844c Co-authored-by: Amp * Normalize nightly- to nightly in release_version Ensures tarball and Docker nightly artifacts produce the same version string. The commit identifier is already included in the SemVer build metadata (after `+`), so collapsing `nightly-` to `nightly` avoids duplicating the SHA in the pre-release tag. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019df79e-d4c9-707c-85eb-2efbf59160b3 --------- Co-authored-by: Centaur AI Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks * fix(evm): query `state_snapshot.storage` in `ForkDbStateSnapshot::storage_ref` (#14007) * fix(evm): query `state_snapshot.storage` in `ForkDbStateSnapshot::storage_ref` * test(evm): cover `ForkDbStateSnapshot::storage_ref` snapshot lookup * fix(cast): consistent `--json` output for `keychain` subcommands (#14590) - `keychain rl`: wrap remaining limit in `{"remaining":"..."}` object instead of emitting a bare JSON string - `keychain policy add-call`: emit `{"status":"already_present","target":"..."}` when the rule already exists, instead of plain text - `send_keychain_tx`: wrap sponsor hash in `{"sponsor_hash":"0x..."}` object when --tempo.print-sponsor-hash is used with --json Add CLI tests covering the rl and sponsor-hash JSON output shapes. * feat(tempo): add sponsored transaction plumbing (#14560) * feat(tempo): add sponsored transaction plumbing * addressing mablr comments * fix tempo sponsor signer future layout * preserve json output for tempo sponsor preview * fix(cast): `--json` output support for `vaddr` (#14591) * feat(tempo): add named nonce lanes (#14527) * fix(cheatcodes): transfer value for payable mock calls (#14547) * test: updated tests * fix: execute value transfer * test: improve * imp: review item * test: vm.prank test * imp: moved mocked-call handling after prank application --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(lint): add inline-assembly lint (#14575) * feat(lint): add inline-assembly lint * lint(inline-assembly): also recognize `/// @solidity memory-safe-assembly` NatSpec Amp-Thread-ID: https://ampcode.com/threads/T-019df4b6-1b76-734c-9a9b-29db9fb7d461 Co-authored-by: Amp --------- Co-authored-by: Amp * refactor(script): remove `ScriptConfig::{fee_token,expires_at}` in favour of `TempoOpts` (#14594) * feat(evm-core): gate optimism behind cargo feature (#14593) * fix(cli): resolve Tempo expires once (#14595) fix(cli): resolve tempo expires once * feat(cli): gate optimism behind cargo feature (#14596) * fix(anvil): classify EVM halts as transaction rejections (#14592) Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat: drop optimism deps under no-default-features (#14600) * fix(forge): `--fuzz-seed` parameter is not effective in `forge coverage` (#14610) fix --fuzz-seed not effective in forge coverage * fix(foundryup): mirror tag resolution for install & use (#14611) * fix(foundryup): mirror tag resolution for install & use * fix(foundryup): mirror semver version normalization in `use` `install` auto-prepends `v` to bare semver versions (e.g. `1.7.0` -> `v1.7.0`) so the on-disk directory is always `v`-prefixed. `use` was doing a literal lookup, so `foundryup -u 1.7.0` failed even though `foundryup -i 1.7.0` had succeeded. Broaden the channel `case` in `use()` to also match bare semver inputs (`MAJOR.MINOR.PATCH[-prerelease]`) so they go through the same `resolve_version_and_tag` normalizer. The pattern is intentionally tighter than `install`'s `[[:digit:]]*` so locally-built versions whose names happen to start with a digit are still looked up literally. Amp-Thread-ID: https://ampcode.com/threads/T-019dfc78-8557-712b-9944-bbff9a4a3b76 Co-authored-by: Amp * chore(foundryup): clarify tag-resolution log and error messages Distinguish the GitHub API tag-resolution phase from the actual binary download by consistently referring to "release tag(s)" in the `resolve_version_and_tag` helper's `say` and `err` messages. Amp-Thread-ID: https://ampcode.com/threads/T-019dfc78-8557-712b-9944-bbff9a4a3b76 Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * fix(ci): keep no-default builds free of op deps (#14612) * feat: cast unauthorized flow → wallet.tempo access-key authorization (#14517) * feat: cast unauthorized flow → wallet.tempo access-key authorization Amp-Thread-ID: https://ampcode.com/threads/T-019df174-9538-713b-b8c9-5001b1ad4719 Co-authored-by: Amp * fmt * feat(cast): replace TEMPO_NO_BROWSER env with flag * revert token addresses --------- Co-authored-by: Amp Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * docs(expect-emit): clarify next-call semantics and warn about caught-revert leak (#14620) docs(cheatcodes): clarify expectEmit next-call semantics and caught-revert leak expectEmit is a 'next call' assertion. If the call immediately after expectEmit reverts and the revert is swallowed by the caller (low-level call or try/catch), the unmatched expectation can leak forward and be satisfied by a later unrelated emission, silently turning a broken test green. Document the constraint on the natspec for both no-arg and topic-checking overloads, and regenerate cheatcodes.json. Refs: https://github.com/foundry-rs/foundry/issues/14618 Amp-Thread-ID: https://ampcode.com/threads/T-019dfd96-7a03-7249-8c10-af20ee2729f5 Co-authored-by: Amp * fix(cheatcodes): enforce `expectRevert` reverter address for CREATE frames (#14615) * fix(cheatcodes): enforce `expectRevert` reverter address for CREATE frames The reverter address argument to `vm.expectRevert` was silently ignored when the innermost reverting frame was a CREATE (top-level or nested), because create_end never populated `expected_revert.reverted_by`. Mirror call_end's logic in create_end: when the outcome reverts and a reverter address is expected, record outcome.address (revm guarantees this is Some(would-be address) whenever the constructor executed). Adds positive regression tests for top-level and nested-CREATE reverts, and a negative regression test asserting wrong-reverter now fails. Co-authored-by: Amp * improve coverage * add Derek's suggested test cases * fix: forge fmt for ExpectRevert.t.sol Amp-Thread-ID: https://ampcode.com/threads/T-019dfdc5-5414-70b6-9f49-cb5797a37a29 Co-authored-by: Amp --------- Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(script): keep plain Tempo broadcasts non-AA (#14616) * fix(script): don't force Tempo AA fee_token from --network tempo alone Plain --network tempo (or any selection that just sets the network to Tempo) does not by itself imply a Tempo AA / type 0x76 transaction. Defaulting tempo.common.fee_token to PATH_USD_ADDRESS solely from evm_opts.networks.is_tempo() caused every unsigned broadcast tx to flow through TempoOpts::apply, which set fee_token on the request and promoted it to the Tempo AA tx envelope. Signers that only know how to sign ordinary Ethereum transactions (e.g. the Ledger Ethereum app) then rejected the transaction with 'received an unexpected empty response'. Gate the default on an actual Tempo AA opt-in: - --batch (Tempo batch txs are themselves AA and need a fee token), or - any explicit --tempo.* flag (sponsor, expiring nonce, nonce key/lane, ...) which already forces an AA tx and benefits from a default fee token. Explicit --tempo.fee-token continues to win over the default in all cases, and non-Tempo networks never default the fee token. Add unit tests for each scenario. Amp-Thread-ID: https://ampcode.com/threads/T-019dfd37-2354-712f-95b1-2584fd47ad5e Co-authored-by: Amp * fix(script): don't force eth_estimateGas on plain Tempo broadcasts Plain --network tempo produces an ordinary EIP-1559/legacy transaction (see tempo-alloy::TempoTransactionRequest::output_tx_type), so the local simulation gas estimate is sufficient. Forcing RPC re-estimation in this case can surface node-side errors such as 'gas required exceeds allowance (0)' (Geth-style balance/gasPrice cap from eth_estimateGas) on flows that previously worked, including Ledger-signed broadcasts that just got unblocked from the type 0x76 regression. Match tempo-foundry's behaviour: only force eth_estimateGas on Tempo when the user has actually opted into Tempo AA semantics (--batch or any explicit --tempo.* flag). Extract the gating into needs_tempo_aa_rpc_estimate(...) and add focused unit tests mirroring the fee-token gating tests. Amp-Thread-ID: https://ampcode.com/threads/T-019dfd37-2354-712f-95b1-2584fd47ad5e Co-authored-by: Amp * fix(script): don't re-estimate plain Tempo chain broadcasts --------- Co-authored-by: Amp * fix(cheatcodes): preserve reverts with `expectEmit` (#14619) * test: added regression test * fix: re-order revert handling * refactor: simplify * lint: fmt * polish: tighten comment, extend test with revert reason and custom error Amp-Thread-ID: https://ampcode.com/threads/T-019dfd96-7a03-7249-8c10-af20ee2729f5 Co-authored-by: Amp --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * feat(lint): add tx-origin detector (#14589) * feat(lint): add tx-origin detector * test(lint): address tx-origin review feedback * fix: ui bless * fix(lint): cover tx-origin index and ternary predicates * test(lint): bless tx-origin snapshot --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * refactor(tempo): prepare batch access key txs w/ helper (#14597) fix(tempo): prepare batch access key txs before estimation * fix(anvil): respect non-zero genesis block in Otterscan APIs (#14490) fix(anvil): respect non-zero genesis block in Otterscan APIs The three Otterscan address-history endpoints (`ots_searchTransactionsBefore`/`After`, `ots_getTransactionBySenderAndNonce`) hardcoded `unwrap_or(1)` / `unwrap_or_default()` as the lower bound of their block scan, which breaks when `genesis_block_number` is non-zero (e.g. `genesis.json` `number: 73`). Expose `Backend::genesis_number()` and fall back to `genesis_number() + 1` in non-fork mode, mirroring the existing post-fork `f.block_number() + 1` convention. --------- Co-authored-by: Isagi Yates Co-authored-by: Amp Co-authored-by: steven Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: figtracer Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Sergei Shulepov Co-authored-by: zerosnacks Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: cui Co-authored-by: Centaur AI Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Nikki Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: Mikhail Mikheev <16622558+mmv08@users.noreply.github.com> Co-authored-by: lazymio Co-authored-by: Emma Jamieson-Hoare Co-authored-by: VIkions <99107287+vikions@users.noreply.github.com> Co-authored-by: Aïssata --- .github/scripts/commit-and-read-benchmarks.sh | 114 -- .github/scripts/commit-benchmark-results.sh | 75 + .github/scripts/compare-nightly.sh | 56 + .github/scripts/read-benchmark-results.sh | 37 + .github/scripts/tempo-check.sh | 86 +- .github/workflows/benchmarks-nightly.yml | 217 +++ .github/workflows/benchmarks.yml | 73 +- .github/workflows/ci-tempo.yml | 44 +- .github/workflows/ci.yml | 17 + .github/workflows/crate-checks.yml | 2 +- .github/workflows/docker-publish.yml | 30 + .github/workflows/nix.yml | 4 +- .github/workflows/npm.yml | 4 +- .github/workflows/release.yml | 78 +- .github/workflows/test-flaky.yml | 2 +- .github/workflows/test-isolate.yml | 2 +- .github/workflows/test.yml | 4 +- Cargo.lock | 1011 +++++------ Cargo.toml | 98 +- README.md | 2 + SECURITY.md | 109 ++ benches/LATEST.md | 134 +- benches/src/main.rs | 40 +- benches/src/results.rs | 19 + benchmark.sh | 60 +- crates/anvil/Cargo.toml | 34 +- crates/anvil/core/Cargo.toml | 10 +- crates/anvil/src/cmd.rs | 27 +- crates/anvil/src/config.rs | 6 +- crates/anvil/src/eth/api.rs | 44 +- crates/anvil/src/eth/backend/executor.rs | 23 +- crates/anvil/src/eth/backend/mem/mod.rs | 267 ++- crates/anvil/src/eth/backend/mem/optimism.rs | 61 + .../anvil/src/eth/{error.rs => error/mod.rs} | 69 +- crates/anvil/src/eth/error/optimism.rs | 62 + crates/anvil/src/eth/otterscan/api.rs | 19 +- crates/anvil/src/eth/pool/transactions.rs | 4 +- crates/anvil/src/eth/sign.rs | 8 +- crates/anvil/src/{evm.rs => evm/mod.rs} | 84 +- crates/anvil/src/evm/optimism.rs | 87 + crates/anvil/src/lib.rs | 3 + crates/anvil/tests/it/main.rs | 1 + crates/anvil/tests/it/revert.rs | 50 + crates/cast/Cargo.toml | 17 +- crates/cast/src/args.rs | 7 + crates/cast/src/cmd/batch_mktx.rs | 27 +- crates/cast/src/cmd/batch_send.rs | 32 +- crates/cast/src/cmd/call.rs | 25 +- crates/cast/src/cmd/keychain.rs | 1022 ++++++++++- crates/cast/src/cmd/mktx.rs | 44 +- crates/cast/src/cmd/mod.rs | 3 + crates/cast/src/cmd/run.rs | 17 +- crates/cast/src/cmd/send.rs | 72 +- crates/cast/src/cmd/tempo.rs | 45 + crates/cast/src/cmd/tip20/mine.rs | 23 +- crates/cast/src/cmd/tip20/mod.rs | 2 +- crates/cast/src/cmd/vaddr/create.rs | 181 ++ crates/cast/src/cmd/vaddr/mod.rs | 131 ++ crates/cast/src/cmd/vaddr/resolve.rs | 52 + crates/cast/src/cmd/vaddr/watch.rs | 108 ++ crates/cast/src/cmd/wallet/mod.rs | 13 +- crates/cast/src/lib.rs | 4 +- crates/cast/src/opts.rs | 25 +- crates/cast/src/tempo.rs | 3 + crates/cast/src/tx.rs | 30 +- crates/cast/tests/cli/keychain.rs | 76 + crates/cast/tests/cli/main.rs | 119 ++ crates/cheatcodes/Cargo.toml | 10 + crates/cheatcodes/assets/cheatcodes.json | 4 +- crates/cheatcodes/spec/src/vm.rs | 6 + crates/cheatcodes/src/inspector.rs | 145 +- crates/cheatcodes/src/test/assert.rs | 4 +- crates/cheatcodes/src/version.rs | 67 +- crates/chisel/Cargo.toml | 8 +- crates/chisel/src/executor.rs | 1617 ++++++++--------- crates/chisel/src/source.rs | 561 ++---- crates/chisel/tests/it/repl/mod.rs | 20 + crates/cli/Cargo.toml | 7 + crates/cli/src/opts/evm.rs | 11 + crates/cli/src/opts/rpc.rs | 56 +- crates/cli/src/opts/rpc_common.rs | 7 +- crates/cli/src/opts/tempo.rs | 320 +++- crates/cli/src/utils/tempo.rs | 193 +- crates/common/Cargo.toml | 20 +- crates/common/build.rs | 20 +- crates/common/fmt/Cargo.toml | 8 +- crates/common/fmt/src/ui.rs | 8 + crates/common/src/contracts.rs | 14 +- crates/common/src/provider/mpp/keys.rs | 73 +- crates/common/src/provider/mpp/session.rs | 10 + crates/common/src/provider/mpp/transport.rs | 922 +++++++++- crates/common/src/provider/mpp/ws.rs | 4 + .../common/src/provider/runtime_transport.rs | 6 +- crates/common/src/tempo/auth.rs | 494 +++++ crates/common/src/tempo/keystore.rs | 147 +- crates/common/src/tempo/mod.rs | 186 ++ crates/common/src/transactions/builder.rs | 57 +- crates/common/src/transactions/receipt.rs | 2 + crates/config/src/fuzz.rs | 6 + crates/config/src/inline/mod.rs | 39 + crates/debugger/Cargo.toml | 8 + crates/doc/Cargo.toml | 4 + crates/doc/src/writer/as_doc.rs | 4 +- crates/evm/core/Cargo.toml | 24 +- crates/evm/core/src/decode.rs | 4 +- crates/evm/core/src/env.rs | 605 +++--- crates/evm/core/src/evm/mod.rs | 21 +- crates/evm/core/src/evm/op.rs | 22 +- crates/evm/core/src/fork/database.rs | 53 +- crates/evm/core/src/lib.rs | 3 + crates/evm/core/src/opts.rs | 7 +- crates/evm/coverage/Cargo.toml | 4 + crates/evm/evm/Cargo.toml | 13 + crates/evm/evm/src/executors/fuzz/mod.rs | 125 +- crates/evm/evm/src/executors/invariant/mod.rs | 2 +- crates/evm/fuzz/Cargo.toml | 9 + crates/evm/fuzz/src/lib.rs | 35 +- crates/evm/hardforks/Cargo.toml | 8 +- crates/evm/hardforks/src/lib.rs | 74 +- crates/evm/networks/Cargo.toml | 8 +- crates/evm/networks/src/lib.rs | 238 ++- crates/evm/networks/src/optimism.rs | 25 + crates/evm/traces/Cargo.toml | 4 + crates/fmt/Cargo.toml | 4 + crates/fmt/src/state/mod.rs | 2 +- crates/forge/Cargo.toml | 14 +- crates/forge/assets/tempo/MailTemplate.s.sol | 2 +- crates/forge/assets/tempo/MailTemplate.t.sol | 2 +- crates/forge/src/cmd/coverage.rs | 7 +- crates/forge/src/cmd/create.rs | 43 +- crates/forge/src/cmd/snapshot.rs | 7 +- crates/forge/src/cmd/test/mod.rs | 226 ++- crates/forge/src/cmd/test/summary.rs | 4 +- crates/forge/src/gas_report.rs | 2 +- crates/forge/src/multi_runner.rs | 32 + crates/forge/src/runner.rs | 24 +- crates/forge/tests/cli/cmd.rs | 4 +- crates/forge/tests/cli/config.rs | 28 + crates/forge/tests/cli/failure_assertions.rs | 7 +- crates/forge/tests/cli/inline_config.rs | 104 ++ crates/forge/tests/cli/lint.rs | 289 ++- crates/forge/tests/cli/lint/geiger.rs | 10 +- crates/forge/tests/cli/script.rs | 2 +- crates/forge/tests/cli/test_cmd/fuzz.rs | 145 ++ crates/forge/tests/cli/test_cmd/repros.rs | 60 + .../tests/fixtures/ExpectRevertFailures.t.sol | 57 + crates/lint/Cargo.toml | 4 + crates/lint/README.md | 8 + crates/lint/docs/README.md | 52 + crates/lint/docs/_template.md | 28 + crates/lint/docs/asm-keccak256.md | 42 + crates/lint/docs/block-timestamp.md | 44 + crates/lint/docs/boolean-cst.md | 37 + crates/lint/docs/boolean-equal.md | 34 + crates/lint/docs/could-be-immutable.md | 42 + crates/lint/docs/custom-errors.md | 45 + crates/lint/docs/divide-before-multiply.md | 32 + crates/lint/docs/erc20-unchecked-transfer.md | 43 + crates/lint/docs/incorrect-erc20-interface.md | 42 + .../lint/docs/incorrect-erc721-interface.md | 48 + crates/lint/docs/incorrect-shift.md | 37 + crates/lint/docs/inline-assembly.md | 69 + crates/lint/docs/interface-file-naming.md | 31 + crates/lint/docs/interface-naming.md | 31 + crates/lint/docs/missing-zero-check.md | 39 + crates/lint/docs/mixed-case-function.md | 32 + crates/lint/docs/mixed-case-variable.md | 36 + crates/lint/docs/multi-contract-file.md | 37 + crates/lint/docs/named-struct-fields.md | 31 + crates/lint/docs/pascal-case-struct.md | 31 + crates/lint/docs/pragma-inconsistent.md | 41 + crates/lint/docs/rtlo.md | 32 + .../lint/docs/screaming-snake-case-const.md | 30 + .../docs/screaming-snake-case-immutable.md | 31 + crates/lint/docs/too-many-digits.md | 32 + crates/lint/docs/tx-origin.md | 34 + crates/lint/docs/unaliased-plain-import.md | 34 + crates/lint/docs/unchecked-call.md | 34 + crates/lint/docs/unsafe-cheatcode.md | 35 + crates/lint/docs/unsafe-typecast.md | 40 + crates/lint/docs/unused-import.md | 40 + crates/lint/docs/unused-state-variables.md | 39 + crates/lint/docs/unwrapped-modifier-logic.md | 51 + crates/lint/src/linter/mod.rs | 2 + crates/lint/src/linter/project.rs | 92 + crates/lint/src/sol/info/inline_assembly.rs | 71 + crates/lint/src/sol/info/mod.rs | 12 + crates/lint/src/sol/info/pragma_directive.rs | 71 + crates/lint/src/sol/info/too_many_digits.rs | 50 + crates/lint/src/sol/macros.rs | 42 +- crates/lint/src/sol/med/mod.rs | 4 + crates/lint/src/sol/med/tx_origin.rs | 101 + crates/lint/src/sol/mod.rs | 133 +- crates/lint/testdata/BlockTimestamp.stderr | 24 +- crates/lint/testdata/BooleanCst.stderr | 10 +- crates/lint/testdata/BooleanEqual.stderr | 14 +- crates/lint/testdata/CouldBeImmutable.stderr | 14 +- crates/lint/testdata/CustomErrors.stderr | 10 +- .../lint/testdata/DivideBeforeMultiply.stderr | 12 +- crates/lint/testdata/Imports.stderr | 26 +- .../testdata/IncorrectERC20Interface.stderr | 30 +- .../testdata/IncorrectERC721Interface.stderr | 38 +- crates/lint/testdata/IncorrectShift.stderr | 10 +- crates/lint/testdata/InlineAssembly.sol | 110 ++ crates/lint/testdata/InlineAssembly.stderr | 96 + crates/lint/testdata/Keccak256.sol | 1 + crates/lint/testdata/Keccak256.stderr | 36 +- crates/lint/testdata/MissingZeroCheck.stderr | 46 +- crates/lint/testdata/MixedCase.stderr | 38 +- crates/lint/testdata/MultiContractFile.stderr | 10 +- .../MultiContractFile_InterfaceLibrary.stderr | 6 +- crates/lint/testdata/NamedStructFields.stderr | 2 +- .../PragmaInconsistentCaretAboveExact.sol | 7 + .../PragmaInconsistentCaretAboveExact.stderr | 16 + .../PragmaInconsistentCaretMatchesExact.sol | 7 + ...PragmaInconsistentCaretMatchesExact.stderr | 16 + .../PragmaInconsistentCaretVsTilde.sol | 7 + .../PragmaInconsistentCaretVsTilde.stderr | 16 + .../testdata/PragmaInconsistentOrVsExact.sol | 7 + .../PragmaInconsistentOrVsExact.stderr | 16 + .../PragmaInconsistentRangeVsExact.sol | 7 + .../PragmaInconsistentRangeVsExact.stderr | 16 + .../PragmaInconsistentThreeDistinct.sol | 8 + .../PragmaInconsistentThreeDistinct.stderr | 24 + crates/lint/testdata/Rtlo.stderr | 48 +- crates/lint/testdata/RtloCommentsOnly.stderr | 8 +- .../lint/testdata/ScreamingSnakeCase.stderr | 16 +- crates/lint/testdata/StructPascalCase.stderr | 12 +- crates/lint/testdata/TooManyDigits.sol | 73 + crates/lint/testdata/TooManyDigits.stderr | 72 + crates/lint/testdata/TxOrigin.sol | 65 + crates/lint/testdata/TxOrigin.stderr | 72 + crates/lint/testdata/UncheckedCall.stderr | 16 +- .../testdata/UncheckedTransferERC20.stderr | 22 +- crates/lint/testdata/UnsafeCheatcodes.stderr | 26 +- crates/lint/testdata/UnsafeTypecast.stderr | 330 ++-- .../lint/testdata/UnusedStateVariables.stderr | 10 +- .../testdata/UnwrappedModifierLogic.stderr | 22 +- crates/primitives/Cargo.toml | 17 +- crates/primitives/src/network/mod.rs | 10 +- crates/primitives/src/network/optimism.rs | 47 + crates/primitives/src/network/receipt.rs | 40 +- crates/primitives/src/transaction/envelope.rs | 281 +-- crates/primitives/src/transaction/mod.rs | 6 +- crates/primitives/src/transaction/optimism.rs | 300 +++ crates/primitives/src/transaction/receipt.rs | 114 +- crates/primitives/src/transaction/request.rs | 110 +- crates/script-sequence/Cargo.toml | 4 + crates/script/Cargo.toml | 12 + crates/script/src/broadcast.rs | 143 +- crates/script/src/lib.rs | 131 +- crates/script/src/runner.rs | 10 +- crates/script/src/verify.rs | 2 +- crates/sol-macro-gen/Cargo.toml | 4 + crates/test-utils/Cargo.toml | 4 + crates/verify/Cargo.toml | 9 + docs/dev/lintrules.md | 2 + flake.lock | 18 +- foundryup/README.md | 4 +- foundryup/foundryup | 135 +- testdata/default/cheats/ExpectRevert.t.sol | 85 + .../default/cheats/GetFoundryVersion.t.sol | 51 + testdata/default/cheats/MockCall.t.sol | 41 +- testdata/default/cheats/MockCalls.t.sol | 4 + 264 files changed, 13431 insertions(+), 4233 deletions(-) delete mode 100755 .github/scripts/commit-and-read-benchmarks.sh create mode 100755 .github/scripts/commit-benchmark-results.sh create mode 100755 .github/scripts/compare-nightly.sh create mode 100755 .github/scripts/read-benchmark-results.sh create mode 100644 .github/workflows/benchmarks-nightly.yml create mode 100644 crates/anvil/src/eth/backend/mem/optimism.rs rename crates/anvil/src/eth/{error.rs => error/mod.rs} (91%) create mode 100644 crates/anvil/src/eth/error/optimism.rs rename crates/anvil/src/{evm.rs => evm/mod.rs} (64%) create mode 100644 crates/anvil/src/evm/optimism.rs create mode 100644 crates/cast/src/cmd/tempo.rs create mode 100644 crates/cast/src/cmd/vaddr/create.rs create mode 100644 crates/cast/src/cmd/vaddr/mod.rs create mode 100644 crates/cast/src/cmd/vaddr/resolve.rs create mode 100644 crates/cast/src/cmd/vaddr/watch.rs create mode 100644 crates/cast/src/tempo.rs create mode 100644 crates/cast/tests/cli/keychain.rs create mode 100644 crates/common/src/tempo/auth.rs create mode 100644 crates/evm/networks/src/optimism.rs create mode 100644 crates/lint/docs/README.md create mode 100644 crates/lint/docs/_template.md create mode 100644 crates/lint/docs/asm-keccak256.md create mode 100644 crates/lint/docs/block-timestamp.md create mode 100644 crates/lint/docs/boolean-cst.md create mode 100644 crates/lint/docs/boolean-equal.md create mode 100644 crates/lint/docs/could-be-immutable.md create mode 100644 crates/lint/docs/custom-errors.md create mode 100644 crates/lint/docs/divide-before-multiply.md create mode 100644 crates/lint/docs/erc20-unchecked-transfer.md create mode 100644 crates/lint/docs/incorrect-erc20-interface.md create mode 100644 crates/lint/docs/incorrect-erc721-interface.md create mode 100644 crates/lint/docs/incorrect-shift.md create mode 100644 crates/lint/docs/inline-assembly.md create mode 100644 crates/lint/docs/interface-file-naming.md create mode 100644 crates/lint/docs/interface-naming.md create mode 100644 crates/lint/docs/missing-zero-check.md create mode 100644 crates/lint/docs/mixed-case-function.md create mode 100644 crates/lint/docs/mixed-case-variable.md create mode 100644 crates/lint/docs/multi-contract-file.md create mode 100644 crates/lint/docs/named-struct-fields.md create mode 100644 crates/lint/docs/pascal-case-struct.md create mode 100644 crates/lint/docs/pragma-inconsistent.md create mode 100644 crates/lint/docs/rtlo.md create mode 100644 crates/lint/docs/screaming-snake-case-const.md create mode 100644 crates/lint/docs/screaming-snake-case-immutable.md create mode 100644 crates/lint/docs/too-many-digits.md create mode 100644 crates/lint/docs/tx-origin.md create mode 100644 crates/lint/docs/unaliased-plain-import.md create mode 100644 crates/lint/docs/unchecked-call.md create mode 100644 crates/lint/docs/unsafe-cheatcode.md create mode 100644 crates/lint/docs/unsafe-typecast.md create mode 100644 crates/lint/docs/unused-import.md create mode 100644 crates/lint/docs/unused-state-variables.md create mode 100644 crates/lint/docs/unwrapped-modifier-logic.md create mode 100644 crates/lint/src/linter/project.rs create mode 100644 crates/lint/src/sol/info/inline_assembly.rs create mode 100644 crates/lint/src/sol/info/pragma_directive.rs create mode 100644 crates/lint/src/sol/info/too_many_digits.rs create mode 100644 crates/lint/src/sol/med/tx_origin.rs create mode 100644 crates/lint/testdata/InlineAssembly.sol create mode 100644 crates/lint/testdata/InlineAssembly.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.sol create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr create mode 100644 crates/lint/testdata/TooManyDigits.sol create mode 100644 crates/lint/testdata/TooManyDigits.stderr create mode 100644 crates/lint/testdata/TxOrigin.sol create mode 100644 crates/lint/testdata/TxOrigin.stderr create mode 100644 crates/primitives/src/network/optimism.rs create mode 100644 crates/primitives/src/transaction/optimism.rs diff --git a/.github/scripts/commit-and-read-benchmarks.sh b/.github/scripts/commit-and-read-benchmarks.sh deleted file mode 100755 index 358b53a73155a..0000000000000 --- a/.github/scripts/commit-and-read-benchmarks.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Script to commit benchmark results and read them for GitHub Actions output -# Usage: ./commit-and-read-benchmarks.sh - -OUTPUT_DIR="${1:-benches}" -GITHUB_EVENT_NAME="${2:-pull_request}" -GITHUB_REPOSITORY="${3:-}" - -# Global variable for branch name -BRANCH_NAME="" - -# Function to commit benchmark results -commit_results() { - echo "Configuring git..." - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # For PR runs, fetch and checkout the PR branch to ensure we're up to date - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" - git fetch origin "$GITHUB_HEAD_REF" - git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" - fi - - echo "Adding benchmark file..." - git add "$OUTPUT_DIR/LATEST.md" - - if git diff --staged --quiet; then - echo "No changes to commit" - else - echo "Committing benchmark results..." - git commit -m "chore(\`benches\`): update benchmark results - -🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) - -Co-Authored-By: github-actions " - - echo "Pushing to repository..." - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - # For manual runs, we're on a new branch - git push origin "$BRANCH_NAME" - elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - # For PR runs, push to the PR branch - if [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Pushing to PR branch: $GITHUB_HEAD_REF" - git push origin "$GITHUB_HEAD_REF" - else - echo "Error: GITHUB_HEAD_REF not set for pull_request event" - exit 1 - fi - else - # This workflow should only run on workflow_dispatch or pull_request - echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" - echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" - exit 1 - fi - echo "Successfully pushed benchmark results" - fi -} - -# Function to read benchmark results and output for GitHub Actions -read_results() { - if [ -f "$OUTPUT_DIR/LATEST.md" ]; then - echo "Reading benchmark results..." - - # Output full results - { - echo 'results<> "$GITHUB_OUTPUT" - - # Format results for PR comment - echo "Formatting results for PR comment..." - FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") - - { - echo 'pr_comment<> "$GITHUB_OUTPUT" - - echo "Successfully read and formatted benchmark results" - else - echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" - fi -} - -# Main execution -echo "Starting benchmark results processing..." - -# Create new branch for manual runs -if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - echo "Manual workflow run detected, creating new branch..." - BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$BRANCH_NAME" - echo "Created branch: $BRANCH_NAME" - - # Output branch name for later use - echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" -fi - -# Always commit benchmark results -echo "Committing benchmark results..." -commit_results - -# Always read results for output -read_results - -echo "Benchmark results processing complete" \ No newline at end of file diff --git a/.github/scripts/commit-benchmark-results.sh b/.github/scripts/commit-benchmark-results.sh new file mode 100755 index 0000000000000..f7dba8980fd64 --- /dev/null +++ b/.github/scripts/commit-benchmark-results.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -euo pipefail + +# Script to commit and push benchmark results. +# +# This script is intended to run from the lightweight `publish-results` job, +# which checks out the repo with credentials and only operates on the +# trusted artifact produced by the benchmark job. Keeping the write-scoped +# token away from the bench job (which runs untrusted third-party builds) +# limits the blast radius of a compromised dependency. +# +# Usage: ./commit-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" +GITHUB_EVENT_NAME="${2:-workflow_dispatch}" +GITHUB_REPOSITORY="${3:-}" + +if [ ! -f "$OUTPUT_DIR/LATEST.md" ]; then + echo "Error: $OUTPUT_DIR/LATEST.md not found, nothing to commit" + exit 1 +fi + +echo "Configuring git..." +git config --local user.email "action@github.com" +git config --local user.name "GitHub Action" + +# Decide which branch to commit to based on the event. +BRANCH_NAME="" +case "$GITHUB_EVENT_NAME" in + workflow_dispatch) + echo "Manual workflow run detected, creating new branch..." + BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH_NAME" + echo "Created branch: $BRANCH_NAME" + ;; + pull_request) + if [ -z "${GITHUB_HEAD_REF:-}" ]; then + echo "Error: GITHUB_HEAD_REF not set for pull_request event" + exit 1 + fi + echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" + git fetch origin "$GITHUB_HEAD_REF" + git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" + BRANCH_NAME="$GITHUB_HEAD_REF" + ;; + *) + echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" + echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" + exit 1 + ;; +esac + +# Always emit the branch name so downstream steps (e.g. PR creation) can use it. +echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + +echo "Adding benchmark file..." +git add "$OUTPUT_DIR/LATEST.md" + +if git diff --staged --quiet; then + echo "No changes to commit" + echo "committed=false" >> "$GITHUB_OUTPUT" + exit 0 +fi + +echo "Committing benchmark results..." +git commit -m "chore(\`benches\`): update benchmark results + +🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) + +Co-Authored-By: github-actions " + +echo "Pushing to repository..." +git push origin "$BRANCH_NAME" +echo "Successfully pushed benchmark results to $BRANCH_NAME" +echo "committed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/compare-nightly.sh b/.github/scripts/compare-nightly.sh new file mode 100755 index 0000000000000..674cc0fe01754 --- /dev/null +++ b/.github/scripts/compare-nightly.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Compare two nightly benchmark JSON summaries and report regressions. +# +# Usage: compare-nightly.sh [warn_pct] [fail_pct] +# Exits 0 if no regressions, 1 if any metric exceeds fail_pct. +# Exits 0 gracefully when prev.json is missing (first run / gap > 7 days). +set -euo pipefail + +PREV_JSON="${1:-}" +TODAY_JSON="${2:-}" +WARN="${3:-1}" +FAIL="${4:-3}" + +PREV_JSON="$PREV_JSON" TODAY_JSON="$TODAY_JSON" WARN="$WARN" FAIL="$FAIL" \ +python3 - <<'EOF' +import json, os, sys + +warn = float(os.environ["WARN"]) +fail = float(os.environ["FAIL"]) + +prev_path = os.environ.get("PREV_JSON", "") +prev = json.load(open(prev_path)) if prev_path and os.path.isfile(prev_path) else {} +with open(os.environ["TODAY_JSON"]) as f: + today = json.load(f) + +print("## Nightly Benchmark Regression Report\n") +print("| Benchmark | Previous | Today | Δ | Status |") +print("|-----------|----------|-------|---|--------|") + +has_regression = False +all_keys = sorted(prev.keys() | today.keys()) +for key in all_keys: + t = today.get(key) + p = prev.get(key) + if t is None: + print(f"| `{key}` | {p:.5f}s | N/A | — | ⚠️ Missing |") + has_regression = True + continue + if p is None: + print(f"| `{key}` | N/A | {t:.5f}s | — | 🆕 New |") + continue + delta = (t - p) / p * 100 + if delta >= fail: + status = "🔴 Regression" + has_regression = True + elif delta >= warn: + status = "🟡 Warning" + elif delta <= -warn: + status = "🟢 Improvement" + else: + status = "➡️ OK" + sign = "+" if delta > 0 else "" + print(f"| `{key}` | {p}s | {t}s | {sign}{delta:.1f}% | {status} |") + +sys.exit(1 if has_regression else 0) +EOF diff --git a/.github/scripts/read-benchmark-results.sh b/.github/scripts/read-benchmark-results.sh new file mode 100755 index 0000000000000..548611a7d204a --- /dev/null +++ b/.github/scripts/read-benchmark-results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# Script to read benchmark results and emit them as GitHub Actions outputs. +# This script performs no git operations — it only reads the combined +# benchmark file and writes outputs for the workflow to consume. +# +# Usage: ./read-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" + +echo "Reading benchmark results from $OUTPUT_DIR..." + +if [ -f "$OUTPUT_DIR/LATEST.md" ]; then + # Output full results + { + echo 'results<> "$GITHUB_OUTPUT" + + # Format results for PR comment + echo "Formatting results for PR comment..." + FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") + + { + echo 'pr_comment<> "$GITHUB_OUTPUT" + + echo "Successfully read and formatted benchmark results" +else + echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" +fi diff --git a/.github/scripts/tempo-check.sh b/.github/scripts/tempo-check.sh index b730c466bde55..3caea992cfe7e 100755 --- a/.github/scripts/tempo-check.sh +++ b/.github/scripts/tempo-check.sh @@ -445,7 +445,7 @@ echo -e "\n=== CAST SEND WITH SPONSOR (--tempo.sponsor-signature) ===" # Test sponsored transactions using pre-signed signature. # Step 1: Get the fee_payer_signature_hash using --tempo.print-sponsor-hash # Step 2: Sign it with the sponsor's private key -# Step 3: Send with --tempo.sponsor-signature +# Step 3: Send with --tempo.sponsor and --tempo.sponsor-signature # Step 1: Get the hash that the sponsor needs to sign FEE_PAYER_HASH=$(cast mktx ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ @@ -460,7 +460,7 @@ printf "Sponsor signature: %s\n" "$SPONSOR_SIG" # Step 3: Send the sponsored transaction with the signature RECEIPT=$(cast send ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ 0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D 'increment()' --private-key "$PK" \ - --tempo.sponsor-signature "$SPONSOR_SIG" --json) + --tempo.sponsor "$SPONSOR_ADDR" --tempo.sponsor-signature "$SPONSOR_SIG" --json) # Verify the fee_payer in the receipt matches the sponsor address RECEIPT_FEE_PAYER=$(echo "$RECEIPT" | jq -r '.feePayer // .fee_payer // empty') @@ -897,3 +897,85 @@ check_has_code "Nonce" "0x4e4F4E4345000000000000000000000000000000" check_has_code "AccountKeychain" "0xaAAAaaAA00000000000000000000000000000000" echo -e "\n=== CHISEL FORK TESTS COMPLETE ===" + +# --- cast virtual-address (TIP-1022) tests --- + +echo -e "\n=== CAST VIRTUAL-ADDRESS: SETUP MASTER WALLET ===" +vaddr_master_json="$(cast wallet new --json)" +VADDR_MASTER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_master_json")" +VADDR_MASTER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_master_json")" +printf "Master address: %s\n" "$VADDR_MASTER_ADDR" +fund_and_wait "$VADDR_MASTER_ADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: CREATE (mine + register) ===" +# Use the `vaddr` alias to also exercise it. +VADDR_CREATE_OUT=$(cast vaddr create \ +--owner "$VADDR_MASTER_ADDR" \ +--private-key "$VADDR_MASTER_PK" \ +--rpc-url "$ETH_RPC_URL") +echo "$VADDR_CREATE_OUT" +VADDR=$(echo "$VADDR_CREATE_OUT" | sed -n 's/^ tag=0x000000000000 \(0x[a-fA-F0-9]\{40\}\).*/\1/p' | head -1) +if [[ -z "$VADDR" ]]; then +echo "ERROR: failed to parse virtual address from create output" +exit 1 +fi +echo "Virtual address: $VADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: RESOLVE ===" +VADDR_RESOLVE_OUT=$(cast virtual-address resolve "$VADDR" --rpc-url "$ETH_RPC_URL") +echo "$VADDR_RESOLVE_OUT" +RESOLVED_MASTER=$(echo "$VADDR_RESOLVE_OUT" | sed -n 's/^Master address: \(0x[a-fA-F0-9]\{40\}\).*/\1/p') +RESOLVED_LOWER=$(echo "$RESOLVED_MASTER" | tr '[:upper:]' '[:lower:]') +EXPECTED_LOWER=$(echo "$VADDR_MASTER_ADDR" | tr '[:upper:]' '[:lower:]') +if [[ "$RESOLVED_LOWER" != "$EXPECTED_LOWER" ]]; then +echo "ERROR: resolve returned master $RESOLVED_MASTER, expected $VADDR_MASTER_ADDR" +exit 1 +fi +echo "OK: resolve returned the registered master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: AUTO-FORWARD TO MASTER ===" +# Create a separate sender, fund it, and transfer the fee token to the +# virtual address. The protocol must auto-forward to the master wallet. +vaddr_sender_json="$(cast wallet new --json)" +VADDR_SENDER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_sender_json")" +VADDR_SENDER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_sender_json")" +fund_and_wait "$VADDR_SENDER_ADDR" + +BAL_BEFORE=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance before: $BAL_BEFORE" + +# Capture the current block before the transfer so `cast vaddr watch` can +# replay the Transfer log via --from-block. +BLOCK_BEFORE_TRANSFER=$(cast block-number --rpc-url "$ETH_RPC_URL") + +AMOUNT=1000000 +cast send "$FEE_TOKEN" 'transfer(address,uint256)' "$VADDR" "$AMOUNT" \ +--rpc-url "$ETH_RPC_URL" --private-key "$VADDR_SENDER_PK" + +BAL_AFTER=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance after: $BAL_AFTER" + +EXPECTED=$((BAL_BEFORE + AMOUNT)) +if [[ "$BAL_AFTER" != "$EXPECTED" ]]; then +echo "ERROR: master balance grew by $((BAL_AFTER - BAL_BEFORE)), expected $AMOUNT" +exit 1 +fi +echo "OK: transfer to virtual address auto-forwarded to master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: WATCH ===" +# Tail incoming TIP-20 transfers to the virtual address. `cast vaddr watch` +# polls indefinitely, so we cap it with `timeout`; the historical fetch via +# --from-block emits the prior Transfer log immediately at startup. +WATCH_OUT=$(timeout 5 cast vaddr watch "$VADDR" \ + --token "$FEE_TOKEN" \ + --from-block "$BLOCK_BEFORE_TRANSFER" \ + --rpc-url "$ETH_RPC_URL" 2>&1 || true) +echo "$WATCH_OUT" + +EXPECTED_PATTERN="token=$FEE_TOKEN from=$VADDR_SENDER_ADDR amount=$AMOUNT" +echo "Expected pattern: $EXPECTED_PATTERN" +if ! echo "$WATCH_OUT" | grep -iqF "$EXPECTED_PATTERN"; then + echo "ERROR: cast vaddr watch output did not contain expected '$EXPECTED_PATTERN'" + exit 1 +fi +echo "OK: cast vaddr watch reported correct token/from/amount" diff --git a/.github/workflows/benchmarks-nightly.yml b/.github/workflows/benchmarks-nightly.yml new file mode 100644 index 0000000000000..8569f52ce3b93 --- /dev/null +++ b/.github/workflows/benchmarks-nightly.yml @@ -0,0 +1,217 @@ +name: Nightly Benchmarks (AAVE v4) + +permissions: {} + +on: + schedule: + - cron: "0 2 * * *" # 2am UTC nightly + workflow_dispatch: # allow manual triggering for testing + +env: + AAVE_V4_REPO: "aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" + RUSTC_WRAPPER: "sccache" + +jobs: + run-benchmarks: + name: Run Nightly Benchmarks + runs-on: depot-ubuntu-24.04-32 + permissions: + contents: read + actions: read # needed to download artifacts from previous runs + outputs: + has_regression: ${{ steps.compare.outputs.has_regression }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + + - name: Setup Foundry + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + ./.github/scripts/setup-foundryup.sh + printf '%s\n' "$GITHUB_WORKSPACE/.foundry/bin" >> "$GITHUB_PATH" + + - name: Build benchmark binary + run: cargo build --locked --release --bin foundry-bench + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24" + + - name: Install hyperfine + run: | + curl -L https://github.com/sharkdp/hyperfine/releases/download/v1.19.0/hyperfine-v1.19.0-x86_64-unknown-linux-gnu.tar.gz | tar xz + sudo mv hyperfine-v1.19.0-x86_64-unknown-linux-gnu/hyperfine /usr/local/bin/ + rm -rf hyperfine-v1.19.0-x86_64-unknown-linux-gnu + + - name: Download previous benchmark results + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p prev-results + PREV_RUN_ID=$(gh run list \ + --workflow=benchmarks-nightly.yml \ + --status=success \ + --limit=1 \ + --json databaseId \ + -q '.[0].databaseId // empty' 2>/dev/null || true) + if [[ -n "$PREV_RUN_ID" ]]; then + echo "Downloading results from previous run $PREV_RUN_ID" + gh run download "$PREV_RUN_ID" \ + --name nightly-bench-results \ + --dir prev-results/ || true + else + echo "No previous successful run found, skipping download." + fi + + - name: Run forge test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches --force-install \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_test \ + --json-output "nightly-${DATE}-forge_test.json" \ + --verbose + + - name: Run forge fuzz test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_fuzz_test \ + --json-output "nightly-${DATE}-forge_fuzz_test.json" \ + --verbose + + - name: Run forge isolate test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_isolate_test \ + --json-output "nightly-${DATE}-forge_isolate_test.json" \ + --verbose + + - name: Run forge build benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --json-output "nightly-${DATE}-forge_build.json" \ + --verbose + + - name: Run forge coverage benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_coverage \ + --json-output "nightly-${DATE}-forge_coverage.json" \ + --verbose + + - name: Merge benchmark JSON results + run: | + DATE=$(date -u +%Y-%m-%d) + shopt -s nullglob + parts=( benches/nightly-${DATE}-*.json ) + if [[ ${#parts[@]} -eq 0 ]]; then + echo "No benchmark results produced — all steps failed." + exit 1 + fi + jq -s 'add' "${parts[@]}" > "benches/nightly-${DATE}.json" + echo "Merged ${#parts[@]} result file(s) into nightly-${DATE}.json" + + - name: Compare with previous results + id: compare + run: | + DATE=$(date -u +%Y-%m-%d) + PREV_JSON=$(ls prev-results/nightly-*.json 2>/dev/null | head -1 || true) + TODAY_JSON="benches/nightly-${DATE}.json" + if ./.github/scripts/compare-nightly.sh "$PREV_JSON" "$TODAY_JSON" > regression.md 2>&1; then + echo "has_regression=false" >> "$GITHUB_OUTPUT" + else + echo "has_regression=true" >> "$GITHUB_OUTPUT" + fi + cat regression.md + + - name: Upload benchmark results + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nightly-bench-results + retention-days: 7 + path: | + benches/nightly-*.json + regression.md + + report-regression: + name: Report Regression + needs: run-benchmarks + if: needs.run-benchmarks.outputs.has_regression == 'true' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Download benchmark results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nightly-bench-results + path: results/ + + - name: Open regression issue + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + DATE=$(date -u +%Y-%m-%d) + BODY="$(cat results/regression.md) + + --- + + **Run**: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + **Date**: ${DATE} + **Repo benchmarked**: \`aave/aave-v4\` (pinned commit) + **Threshold**: 🔴 >=3% regression, 🟡 >=1% warning" + + gh issue create \ + --title "[Nightly Regression] ${DATE}" \ + --body "$BODY" \ + --label "regression" \ + --repo "$GH_REPO" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5d4767ad3b554..a136703abc294 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -10,17 +10,17 @@ on: required: false type: string versions: - description: "Comma-separated list of Foundry versions to benchmark (e.g., stable,nightly,v1.0.0)" + description: "Comma-separated list of Foundry versions to benchmark (optional, defaults to 'v1.5.1,v1.7.0')" required: false type: string - default: "stable,nightly" repos: - description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev][ ] (e.g. vectorized/solady:v0.1.26 --nmc BrokenTest). Leave empty to use the per-benchmark default repo lists." + description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev] (e.g. aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35). Leave empty to use the per-benchmark default repo lists." required: false type: string - default: "" env: + DEFAULT_VERSIONS: "v1.5.1,v1.7.0" + # Repos to benchmark per step. Each comma-separated entry has the form # org/repo[:rev][ ] # where anything after the first whitespace is appended to every benchmark @@ -29,27 +29,23 @@ env: TEST_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting ISOLATE_TEST_REPOS: >- ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest, - vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, + vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest', uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting BUILD_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 COVERAGE_REPOS: >- ithacaxyz/account:v0.5.7, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 @@ -60,7 +56,7 @@ jobs: name: Run All Benchmarks runs-on: depot-ubuntu-24.04-32 permissions: - contents: write + contents: read steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -93,7 +89,7 @@ jobs: run: cargo build --locked --release --bin foundry-bench - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" @@ -106,59 +102,61 @@ jobs: - name: Run forge test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_test,forge_fuzz_test \ - --output-file forge_test_bench.md + --output-file forge_test_bench.md \ + --verbose - name: Run forge isolate test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.ISOLATE_TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_isolate_test \ - --output-file forge_isolate_test_bench.md + --output-file forge_isolate_test_bench.md \ + --verbose - name: Run forge build benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.BUILD_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_build_no_cache,forge_build_with_cache \ - --output-file forge_build_bench.md + --output-file forge_build_bench.md \ + --verbose - name: Run forge coverage benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.COVERAGE_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_coverage \ - --output-file forge_coverage_bench.md + --output-file forge_coverage_bench.md \ + --verbose - name: Combine benchmark results run: ./.github/scripts/combine-benchmarks.sh benches - - name: Commit and read benchmark results + - name: Read benchmark results id: benchmark_results - env: - GITHUB_HEAD_REF: ${{ github.head_ref }} - run: ./.github/scripts/commit-and-read-benchmarks.sh benches "${{ github.event_name }}" "${{ github.repository }}" + run: ./.github/scripts/read-benchmark-results.sh benches - name: Upload benchmark results as artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -172,21 +170,21 @@ jobs: benches/LATEST.md outputs: - branch_name: ${{ steps.benchmark_results.outputs.branch_name }} pr_comment: ${{ steps.benchmark_results.outputs.pr_comment }} publish-results: name: Publish Results needs: run-benchmarks runs-on: ubuntu-latest + # All git writes happen here, on a clean ubuntu-latest runner that has + # never executed third-party benchmark code. permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + # persist-credentials defaults to true so we can push. - name: Download benchmark results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -194,19 +192,22 @@ jobs: name: benchmark-results path: benches/ - - name: Push branch for manual runs - if: github.event_name == 'workflow_dispatch' - run: | - git push origin "${{ needs.run-benchmarks.outputs.branch_name }}" - echo "Pushed branch: ${{ needs.run-benchmarks.outputs.branch_name }}" + - name: Commit benchmark results + id: commit_results + env: + GITHUB_HEAD_REF: ${{ github.head_ref }} + run: ./.github/scripts/commit-benchmark-results.sh benches "${{ github.event_name }}" "${{ github.repository }}" - name: Create PR for manual runs - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.commit_results.outputs.committed == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + BRANCH_NAME: ${{ steps.commit_results.outputs.branch_name }} + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | - const branchName = '${{ needs.run-benchmarks.outputs.branch_name }}'; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const branchName = process.env.BRANCH_NAME; + const prComment = process.env.PR_COMMENT; // Create the pull request const { data: pr } = await github.rest.pulls.create({ @@ -231,10 +232,12 @@ jobs: - name: Comment on PR if: github.event.inputs.pr_number != '' || github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | const prNumber = ${{ github.event.inputs.pr_number || github.event.pull_request.number }}; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const prComment = process.env.PR_COMMENT; const comment = `${prComment} diff --git a/.github/workflows/ci-tempo.yml b/.github/workflows/ci-tempo.yml index ad4c424b7e8b2..1ef4c760f324e 100644 --- a/.github/workflows/ci-tempo.yml +++ b/.github/workflows/ci-tempo.yml @@ -69,14 +69,16 @@ jobs: run: | cargo test --locked -p foundry-common --lib tempo::tests::test_fork_schedule_parses_configured_rpcs -- --exact --nocapture - - name: Run Tempo check on devnet + - name: Run Tempo check on mainnet if: | - github.event_name == 'push' || - github.event_name == 'pull_request' || - github.event.inputs.network == 'devnet' || + github.event_name == 'schedule' || + github.event.inputs.network == 'mainnet' || github.event.inputs.network == 'all' env: - ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} + ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} + TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" + VERIFIER_URL: ${{ secrets.VERIFIER_URL }} + PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} run: | if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then @@ -86,16 +88,6 @@ jobs: ./.github/scripts/tempo-deploy.sh fi - - name: Run Tempo wallet tests on devnet - if: | - github.event_name == 'push' || - github.event_name == 'pull_request' || - github.event.inputs.network == 'devnet' || - github.event.inputs.network == 'all' - env: - ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} - run: ./.github/scripts/tempo-wallet.sh - - name: Run Tempo check on testnet if: | github.event_name == 'schedule' || @@ -113,16 +105,14 @@ jobs: ./.github/scripts/tempo-deploy.sh fi - - name: Run Tempo check on mainnet + - name: Run Tempo check on devnet if: | - github.event_name == 'schedule' || - github.event.inputs.network == 'mainnet' || + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || github.event.inputs.network == 'all' env: - ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} - TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" - VERIFIER_URL: ${{ secrets.VERIFIER_URL }} - PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} run: | if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then @@ -132,6 +122,16 @@ jobs: ./.github/scripts/tempo-deploy.sh fi + - name: Run Tempo wallet tests on devnet + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || + github.event.inputs.network == 'all' + env: + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} + run: ./.github/scripts/tempo-wallet.sh + # If the nightly run fails, this will create an issue to signal so. issue: name: Open an issue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eb90a76cdbfd..a434028fdd18c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,23 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test --workspace --doc --locked + no-default-features: + runs-on: depot-ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - run: cargo build --workspace --no-default-features --locked + typos: runs-on: depot-ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/crate-checks.yml b/.github/workflows/crate-checks.yml index eb865bddc10e3..f0d460da6fbb1 100644 --- a/.github/workflows/crate-checks.yml +++ b/.github/workflows/crate-checks.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: cargo-hack - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b97a99d5310a4..6120735657ee6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -32,6 +32,8 @@ jobs: name: build and push runs-on: depot-ubuntu-latest permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -92,6 +94,7 @@ jobs: uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1 - name: Build and push Foundry image + id: build uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0 with: build-args: | @@ -106,3 +109,30 @@ jobs: platforms: linux/amd64,linux/arm64 push: true no-cache: true + sbom: true + provenance: mode=max + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign image with cosign (keyless) + env: + DOCKER_TAGS: ${{ steps.docker_tagging.outputs.docker_tags }} + DIGEST: ${{ steps.build.outputs.digest }} + shell: bash + run: | + set -euo pipefail + IFS=',' read -r -a tags <<< "$DOCKER_TAGS" + for tag in "${tags[@]}"; do + # Strip any tag suffix and pin to immutable digest, then sign. + ref="${tag%%:*}@${DIGEST}" + printf 'Signing %s\n' "$ref" + cosign sign --yes "$ref" + done + + - name: Image build provenance attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 0a18c99a41f82..8528b71f299a9 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -19,7 +19,7 @@ jobs: contents: write pull-requests: write steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -38,7 +38,7 @@ jobs: permissions: contents: read steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index fdc5e5716c577..323059e99e6b6 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -143,7 +143,7 @@ jobs: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -259,7 +259,7 @@ jobs: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 682c9214284f6..38fa791fb655f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Build changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2 + uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2.1 with: configuration: "./.github/changelog.json" fromTag: ${{ steps.release_info.outputs.from_tag || '' }} @@ -117,6 +117,8 @@ jobs: needs: prepare uses: ./.github/workflows/docker-publish.yml permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -129,9 +131,10 @@ jobs: # way, GitHub's immutable-releases setting seals the release at publish. release: permissions: - id-token: write - contents: write attestations: write + artifact-metadata: write + contents: write + id-token: write name: release ${{ matrix.target }} (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} timeout-minutes: 240 @@ -264,6 +267,38 @@ jobs: printf "file_name=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.zip" >> "$GITHUB_OUTPUT" fi printf "foundry_attestation=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.attestation.txt" >> "$GITHUB_OUTPUT" + printf "foundry_sbom=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.spdx.json" >> "$GITHUB_OUTPUT" + printf "foundry_checksum=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sha256" >> "$GITHUB_OUTPUT" + printf "foundry_signature=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sigstore.json" >> "$GITHUB_OUTPUT" + + - name: Generate archive checksum + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + shell: bash + run: | + set -euo pipefail + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + else + shasum -a 256 "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + fi + cat "$FOUNDRY_CHECKSUM" + + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + + - name: Generate SBOM (SPDX) + env: + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + VERSION_NAME: ${{ (env.IS_NIGHTLY == 'true' && 'nightly') || needs.prepare.outputs.tag_name }} + shell: bash + run: | + set -euo pipefail + syft scan dir:. \ + --source-name foundry \ + --source-version "$VERSION_NAME" \ + -o spdx-json="$FOUNDRY_SBOM" - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -292,15 +327,37 @@ jobs: tar -czvf "foundry_man_${VERSION_NAME}.tar.gz" forge.1.gz cast.1.gz anvil.1.gz chisel.1.gz printf 'foundry_man=%s\n' "foundry_man_${VERSION_NAME}.tar.gz" >> "$GITHUB_OUTPUT" - - name: Binaries attestation + - name: Binaries and archive provenance attestation id: attestation - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: | ${{ env.anvil_bin_path }} ${{ env.cast_bin_path }} ${{ env.chisel_bin_path }} ${{ env.forge_bin_path }} + ${{ steps.artifacts.outputs.file_name }} + + - name: Archive SBOM attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: ${{ steps.artifacts.outputs.file_name }} + sbom-path: ${{ steps.artifacts.outputs.foundry_sbom }} + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign archive with cosign (keyless) + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} + shell: bash + run: | + set -euo pipefail + cosign sign-blob \ + --yes \ + --bundle "$FOUNDRY_SIGNATURE" \ + "$FILE_NAME" - name: Record attestation URL env: @@ -321,11 +378,20 @@ jobs: TAG_NAME: ${{ needs.prepare.outputs.tag_name }} FILE_NAME: ${{ steps.artifacts.outputs.file_name }} FOUNDRY_ATTESTATION: ${{ steps.artifacts.outputs.foundry_attestation }} + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} FOUNDRY_MAN: ${{ steps.man.outputs.foundry_man }} shell: bash run: | set -euo pipefail - files=("$FILE_NAME" "$FOUNDRY_ATTESTATION") + files=( + "$FILE_NAME" + "$FOUNDRY_ATTESTATION" + "$FOUNDRY_SBOM" + "$FOUNDRY_CHECKSUM" + "$FOUNDRY_SIGNATURE" + ) if [[ -n "${FOUNDRY_MAN:-}" ]]; then files+=("$FOUNDRY_MAN") fi diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index d6244f826887e..9caa254f05c10 100644 --- a/.github/workflows/test-flaky.yml +++ b/.github/workflows/test-flaky.yml @@ -33,7 +33,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index 141e3a049a73b..6763f1f80bde3 100644 --- a/.github/workflows/test-isolate.yml +++ b/.github/workflows/test-isolate.yml @@ -37,7 +37,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaa46c8e79f7c..daa4822b6e395 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,14 +73,14 @@ jobs: toolchain: stable target: ${{ matrix.target }} - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest # External tests dependencies - name: Setup Node.js if: contains(matrix.name, 'external') - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 - name: Install Bun diff --git a/Cargo.lock b/Cargo.lock index 781b2c7f02b50..9db38f1967f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,21 +77,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a547705d5c1b42575a0542bae2ba45bc62a6154be86611afaef1c0ab5c38598e" +checksum = "d8010fc7e9e8643ef4e758cdccf3eef26734594aedf88a9d5ed35e51837d42ef" dependencies = [ "alloy-consensus", "alloy-contract", "alloy-core", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-network", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -116,14 +116,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8c24c95e90c1608c2d91cff1b451d796474168d3310ccc8b7cd12502ca8169" +checksum = "e3d64da86c616b5092ea64eea648f311bbd58630a0b384c42d699175d6f9122b" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "alloy-tx-macros", "auto_impl", @@ -143,23 +143,23 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d211ad0ef468a70a7a829e49683ff59ad25f02b4ab3764344c4c2663329a52c" +checksum = "8fd98696ca3617d3a9ba1a6f2011880cbfd5618228dab6400c9f8bca457859a8" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-contract" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59d55233ac14aa7fa6bcdcad45ba305e90c556065e0947cd9f243c4469e7c2d" +checksum = "de3df0aadc569a8b277808a7d0ad0e421180654ea36a3c59e9ed2bb968c9a1cd" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -238,12 +238,12 @@ dependencies = [ [[package]] name = "alloy-eip5792" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250ba1168b8a049185a68c4dfa7f2a6a4046bd26fcc8c68632caeb216a5e12dc" +checksum = "1ceb16e7fe5a95825305f218ccd356665f848831f94ce2bbf55339bf5d21e88a" dependencies = [ "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] @@ -265,13 +265,14 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +checksum = "ec6ae911a2fc304a7cb80a79fb7bed6d1474aed4e7c203df1f8ff538f64fc78d" dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "once_cell", "serde", ] @@ -300,9 +301,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae69eaa5096b47ffe97e6a5d6bde7e7fa2dec106af22a9315621d11039c3de3c" +checksum = "64c0456f5f7a4497e9342d20f528e30f5288ddfa0d6a012bd5044afee46cd8a0" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -310,7 +311,7 @@ dependencies = [ "alloy-eip7928", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "auto_impl", "borsh", "c-kzg", @@ -325,9 +326,9 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a8c1330ad33c95b5958573bca9a1ad0b419a51d76bb4c521556fbba8539b8d" +checksum = "d5638cbbffb318d440fdb009de019090d8d117dae40de9d10cdb29891ea59eb9" dependencies = [ "alloy-contract", "alloy-primitives", @@ -339,12 +340,12 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.33.2" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc4b83cb672156663e6094d098beb509965b7fe684bb3d6e44bb9ca2e9ae714" +checksum = "c1ceeea6dcbbcd4e546b27700763a6f6c3b3fee30054209884f521078b6fda4f" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-hardforks", "alloy-primitives", "alloy-rpc-types-engine", @@ -359,13 +360,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39789db0b3f3bbef0e6549c87bc6842b73886ebabee1405b6941685b1cc34083" +checksum = "a71ff8b55d2b8aa05259f474cae7dea0e4991724dc18936b81cb23ec492a0c2a" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "borsh", "serde", @@ -400,9 +401,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662b525af73e86b2167dae923261c8edf440ba7e1426b30a8b993177bc214c02" +checksum = "19e352478b756bad5d7203148e4b461861282ea2ded3da406ba24868b52cd098" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -415,19 +416,19 @@ dependencies = [ [[package]] name = "alloy-network" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c657c2d9751d3c7d94990554b231e5372c3c2e4bad842806280b6151a0d6a05d" +checksum = "ed08ae169869e08370ed121612e0d3dadac33d1a256e9f2465926b23f0bd7d95" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-any", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-sol-types", "async-trait", @@ -441,24 +442,24 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e7c4bb0ebbd6d7406d2808968f43c0d5186c69c5e58cedcbee7380f4cd1fcf" +checksum = "02e6c7ad28afe348a9a9c5624b67ee5b3607b8de98d5816b3056ecdfa6fa2697" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-op-evm" version = "0.31.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -472,7 +473,7 @@ dependencies = [ [[package]] name = "alloy-op-hardforks" version = "0.4.7" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -513,13 +514,13 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fea0fc2628cdbc851aaa333124f9d8ab9f567ab8d4c20202819db13aa1a534" +checksum = "93a7c17472b55482d4734154c2f5ed13f72e03f6752cebb927f6a2d8b52e646c" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network", "alloy-network-primitives", @@ -547,7 +548,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "thiserror 2.0.18", @@ -559,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc7b42e514613c717887dc77bb58d35e845557ebd63a18c3f92a77094e4891f" +checksum = "a8d86958b02bca85103d64fa60d7b364a8b017c6e40f2b02c3f50ca22964a738" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -603,9 +604,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ee7b51752c68fb95f21705e402700750e692b1d21ccc294ac48fadc8655d53" +checksum = "5beb5c2fe6b960c8e8b038e69fd502a90a2e930afa4770efb748b163b0767729" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -616,7 +617,7 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "tokio", @@ -629,9 +630,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa76988f54105ad4398828e8aaf1a39b3f07f91fb79091529056689514ee8c2" +checksum = "4ee1257a278f6d293e05c5162c5940a1561b1aa85ded0028b464c81de37ebfa5" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -640,44 +641,44 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-rpc-types-txpool", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-anvil" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d276bea4e92e4991269d31b9abd3e722eed2565b82036478a4416adb8dd4992" +checksum = "df32156f085e74eac942b6103744be49b817c302341aaa8cb0c1c88dc29228d9" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1a9a3bda9be7f6515316eb792710532411878bbfc88934973f4b371376b00d" +checksum = "6a234bfbdf7a76c3d13808f729af5321852de3dedcaa6fc6d5f54787aaf54c6a" dependencies = [ "alloy-consensus-any", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] [[package]] name = "alloy-rpc-types-beacon" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5d68ddca890854fb78291cbde06115473ded00b2337d0f815e92c0c1f8003" +checksum = "296450f5e76bece0116c939b9437b0421a5da9c5d40031bf4cf9b38d3d94e475" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "derive_more", @@ -689,9 +690,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea21739e232c221779741eba7e7b9bc19ad8ff777b72736647ae519f5c9f6f33" +checksum = "0ab075ac1c25bcf697f133b7cd92e2fb26afe213e872ef79fdf77f0d7bcb3793" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -702,15 +703,15 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f05338cfb4ee5508ff76f01c88142cab8a4579db74b7d9432936c26e4f11374" +checksum = "73b12366c96f4013e1aeebc96c6b56e5f33f07853c42ea2f485045c0c157a4a1" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "ethereum_ssz", "ethereum_ssz_derive", @@ -722,17 +723,17 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda4ece0050154ab278241aeffade58916b04f38254832e8cb6e4671c6e72ed2" +checksum = "56a282daf869eeb7383d3d5c2deb35b0b3fb45ecb329513af4090fc61245ee18" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "itertools 0.14.0", "serde", @@ -743,13 +744,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5905ac3663b0859d67b82d912acce20887d20682a0cadde79c8a763b133a515" +checksum = "6184b5d14152b68b0bb8beb621339d94f0b761a37958bb365fbf7c00922125c2" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", "thiserror 2.0.18", @@ -757,13 +758,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fbf71892d4df9cae8d35dc96f15d522384bb93806205465e2c8c012b7f0a34" +checksum = "f00b631c361e7c7baaf4f1f5a9877730f3507fed2acb9d4b34841b8184b2ec28" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] @@ -780,9 +781,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beaa5c581a67e2743d95b4849eb9cfeb90866429cdaa6d8f6b75eb988b2d0cd9" +checksum = "a0eada2558e921b39dfcead33c487364df9b31374f5733c1c9d2c891c4529933" dependencies = [ "alloy-primitives", "serde", @@ -791,9 +792,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5da9ae50f9b48d7b4e2e5cde87175257be7e5e56909a7794720597c1d9806f6" +checksum = "41eb29f7a8adcd8941fbb8e134022a133e6f8dfd345f2e3b7109599f8a7dca08" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -808,9 +809,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a57d1e72b1f9b11e5e71ebdab0569cb02277a462bbea6793fcaebfcd794ae9" +checksum = "1258987fbc82716b5153ec7bb95a8a295e7640871b8f03d8ec7c4000dc80c215" dependencies = [ "alloy-consensus", "alloy-network", @@ -827,9 +828,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b27f20b5298b76a5a3b7cdbe6bdb184ab1ebd6e120e00dad748867673f5c90" +checksum = "7ffc2a49bca5b73c6964711b57452f6c36a6bcb7f845ab7e9ad05b5a828d0161" dependencies = [ "alloy-consensus", "alloy-network", @@ -845,9 +846,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c7acc40ffbfd37d4113eb619863099f3235d78d044006a1eecb94d8b0b2f1a" +checksum = "94e11ddaddfb98c1ddce737dc440225565b0ae0987ac9ad5e59a85db5904878c" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -865,9 +866,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b794002d57fd2f71b4c87298a41ca24dfc0f2cf6630d95106a477e451747ba" +checksum = "bef839e7ce9b59aa60fa9a175e97986c6145c888d643b0f1fb0a3e7b8e56a2e2" dependencies = [ "alloy-consensus", "alloy-network", @@ -885,9 +886,9 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a09a865ae9e1f05478429ef0d935b16467f35c6e0b02cb10f23f66a3b33fc3" +checksum = "44eb341d0013784da6a39e5bbdc11b95d6744993b12a1c3fd55df795a850dd42" dependencies = [ "alloy-consensus", "alloy-network", @@ -902,9 +903,9 @@ dependencies = [ [[package]] name = "alloy-signer-turnkey" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bb8218544ab635281f1be180a1cfd9b5d549db686faa7e85b3b2c10969819e" +checksum = "82ff16b4166fb90bbe79bd1e49244824fb3cadc6b8cd11e9c8a002c1f8c07492" dependencies = [ "alloy-consensus", "alloy-network", @@ -991,9 +992,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19dec9bfb59647254afdecbb5ddcddd7ba02edcd48ffa40510bddfbed0be1634" +checksum = "3ac7a80c0bac3e44559d53d002e34c461dc2f23262b42cafec019bc70551abbe" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -1014,14 +1015,14 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2035f3c4d6bee20624da2dcf765d469b292398e48d766ffade61b0fcf8b4d45d" +checksum = "eed3ed3300a998f88639ed619fdbbd88bd82865e00c6a8ecb796c99eb12358f6" dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "tower", "tracing", @@ -1030,9 +1031,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfad7aa9206fcb831ae401b6a1c893a402b8eed74f9c8ffbb7a7323afb0d9a4c" +checksum = "1075d9d30fd4d71e50000fd4afb19ed2664ceab20c2a29f3889a6e988329e02d" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -1050,9 +1051,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aa8ff49386df3e008b73c7fb0a5479410e8493fdb86a8b916877a16e8aead9" +checksum = "0e3bff84b2b2a46eb34cc522dc3f889a2867c70be90a377421429b662b3ec4ce" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1085,9 +1086,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520337f3d3d063a7fe20f47aaa62d695e3dc0372b34f601560dee24e76988b9" +checksum = "99fce0350197dcd4ba4e9a7dd43915d908c0eb0e7352755791709a705e1c76b6" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1223,13 +1224,13 @@ dependencies = [ [[package]] name = "anvil" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-network", @@ -1241,7 +1242,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-beacon", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -1276,7 +1277,7 @@ dependencies = [ "parking_lot", "rand 0.8.6", "rand 0.9.4", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -1297,17 +1298,17 @@ dependencies = [ [[package]] name = "anvil-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-eip5792", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "foundry-common", "foundry-evm", @@ -1321,7 +1322,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "serde", "serde_json", @@ -1329,7 +1330,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "1.6.0" +version = "1.7.1" dependencies = [ "anvil-rpc", "async-trait", @@ -1669,20 +1670,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ascii-canvas" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" -dependencies = [ - "term", -] - [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -1955,9 +1947,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.101.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -2172,9 +2164,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.14" +version = "1.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -2361,9 +2353,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -2579,7 +2571,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -2745,13 +2737,13 @@ dependencies = [ [[package]] name = "cast" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-evm", "alloy-hardforks", @@ -2763,7 +2755,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-beacon", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -2822,9 +2814,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -2832,12 +2824,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -2876,7 +2862,7 @@ dependencies = [ [[package]] name = "chisel" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -2890,10 +2876,9 @@ dependencies = [ "foundry-compilers", "foundry-config", "foundry-evm", - "foundry-solang-parser", "foundry-test-utils", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "rexpect", "rustyline", "semver 1.0.28", @@ -2997,9 +2982,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" dependencies = [ "clap", ] @@ -3042,7 +3027,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3195,7 +3180,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3364,9 +3349,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -3375,9 +3360,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -3425,9 +3410,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -3538,9 +3523,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" @@ -3821,9 +3806,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -3973,9 +3958,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", @@ -3999,7 +3984,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4187,15 +4172,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -4304,7 +4280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4502,15 +4478,15 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment2" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4380ce44915a6227efbb61e3885bc1c8e99fb9820f5db612abfac2c5cfc46871" +checksum = "87d63dee16df12076c7770919713c0b92f4e1c85eac828dc2ade0b6c998f016b" dependencies = [ "atomic", "parking_lot", "serde", "tempfile", - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.11+spec-1.1.0", "uncased", "version_check", ] @@ -4607,7 +4583,7 @@ checksum = "932dcfbd51320af5f27f1ba02d2e567dec332cac7d2c221ba45d8e767264c4dc" [[package]] name = "forge" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4661,7 +4637,7 @@ dependencies = [ "rand 0.9.4", "rayon", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4689,7 +4665,7 @@ dependencies = [ [[package]] name = "forge-doc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "derive_more", @@ -4711,7 +4687,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "foundry-common", "foundry-config", @@ -4725,7 +4701,7 @@ dependencies = [ [[package]] name = "forge-lint" -version = "1.6.0" +version = "1.7.1" dependencies = [ "eyre", "foundry-common", @@ -4739,12 +4715,12 @@ dependencies = [ [[package]] name = "forge-script" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-json-abi", "alloy-network", @@ -4787,7 +4763,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-network", "alloy-primitives", @@ -4803,7 +4779,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -4818,7 +4794,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4841,7 +4817,7 @@ dependencies = [ "futures", "itertools 0.14.0", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4864,7 +4840,7 @@ dependencies = [ [[package]] name = "foundry-bench" -version = "1.6.0" +version = "1.7.1" dependencies = [ "chrono", "clap", @@ -4889,7 +4865,7 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "foundry-compilers", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -4899,7 +4875,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4952,7 +4928,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4963,11 +4939,11 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-json-abi", "alloy-network", @@ -5007,6 +4983,7 @@ dependencies = [ "tempo-primitives", "tikv-jemallocator", "tokio", + "toml", "tracing", "tracing-subscriber 0.3.23", "tracing-tracy", @@ -5015,7 +4992,7 @@ dependencies = [ [[package]] name = "foundry-cli-markdown" -version = "1.6.0" +version = "1.7.1" dependencies = [ "clap", "pretty_assertions", @@ -5023,12 +5000,12 @@ dependencies = [ [[package]] name = "foundry-common" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-abi", "alloy-json-rpc", "alloy-network", @@ -5040,6 +5017,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-engine", "alloy-signer", + "alloy-signer-local", "alloy-sol-types", "alloy-transport", "alloy-transport-ipc", @@ -5047,6 +5025,7 @@ dependencies = [ "anstream 0.6.21", "anstyle", "axum", + "base64 0.22.1", "chrono", "ciborium", "clap", @@ -5064,18 +5043,20 @@ dependencies = [ "futures", "itertools 0.14.0", "jiff", + "k256", "mpp", "num-format", "op-alloy-network", "op-alloy-rpc-types", "path-slash", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "rustls", "semver 1.0.28", "serde", "serde_json", + "sha2 0.10.9", "solar-compiler", "tempfile", "tempo-alloy", @@ -5094,14 +5075,14 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", "alloy-primitives", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "chrono", "comfy-table", "eyre", @@ -5217,7 +5198,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5257,7 +5238,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "crossterm", @@ -5275,7 +5256,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5312,7 +5293,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -5324,7 +5305,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -5339,7 +5320,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "anvil", "auto_impl", @@ -5376,7 +5357,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "eyre", @@ -5392,7 +5373,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5417,7 +5398,7 @@ dependencies = [ [[package]] name = "foundry-evm-hardforks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -5432,10 +5413,10 @@ dependencies = [ [[package]] name = "foundry-evm-networks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -5448,11 +5429,11 @@ dependencies = [ [[package]] name = "foundry-evm-sancov" -version = "1.6.0" +version = "1.7.1" [[package]] name = "foundry-evm-traces" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5470,7 +5451,7 @@ dependencies = [ "itertools 0.14.0", "memchr", "rayon", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -5509,7 +5490,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -5520,7 +5501,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "1.6.0" +version = "1.7.1" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -5530,7 +5511,7 @@ dependencies = [ [[package]] name = "foundry-primitives" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-evm", @@ -5541,7 +5522,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "derive_more", "op-alloy-consensus", @@ -5555,23 +5536,9 @@ dependencies = [ "tempo-revm", ] -[[package]] -name = "foundry-solang-parser" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9645e75b89f977423690f3b4bfd8d84825e5fdabd7803cbce6d4a2c4d54972b4" -dependencies = [ - "itertools 0.14.0", - "lalrpop", - "lalrpop-util", - "phf 0.11.3", - "thiserror 2.0.18", - "unicode-xid", -] - [[package]] name = "foundry-test-utils" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5586,7 +5553,7 @@ dependencies = [ "parking_lot", "rand 0.9.4", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "snapbox", "svm-rs", @@ -5814,7 +5781,7 @@ dependencies = [ "once_cell", "prost 0.14.3", "prost-types 0.14.3", - "reqwest 0.13.2", + "reqwest 0.13.3", "secret-vault-value", "serde", "serde_json", @@ -6188,9 +6155,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -6582,9 +6549,9 @@ dependencies = [ [[package]] name = "interprocess" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be5e5c847dbdb44564bd85294740d031f4f8aeb3464e5375ef7141f7538db69" +checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b" dependencies = [ "doctest-file", "futures-core", @@ -6592,7 +6559,7 @@ dependencies = [ "recvmsg", "tokio", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6641,7 +6608,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6694,9 +6661,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -6707,31 +6674,15 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -6741,7 +6692,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -6762,15 +6713,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -6802,9 +6744,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -6841,6 +6783,7 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", "getrandom 0.2.17", "js-sys", @@ -6923,45 +6866,14 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] -[[package]] -name = "lalrpop" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.14.0", - "lalrpop-util", - "petgraph", - "regex", - "regex-syntax", - "sha3", - "string_cache 0.8.9", - "term", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" -dependencies = [ - "regex-automata", - "rustversion", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -6982,9 +6894,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -6994,12 +6906,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" dependencies = [ "cc", - "libc", ] [[package]] @@ -7299,12 +7210,12 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] @@ -7331,9 +7242,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" dependencies = [ "libmimalloc-sys", ] @@ -7588,7 +7499,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7809,7 +7720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b685c8311c9171d1bd2895222965d25616b2de2cb5819dd3504ed9250df9fecd" dependencies = [ "ahash", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "parking_lot", "stable_deref_trait", ] @@ -7817,7 +7728,7 @@ dependencies = [ [[package]] name = "op-alloy" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "op-alloy-consensus", "op-alloy-network", @@ -7829,15 +7740,15 @@ dependencies = [ [[package]] name = "op-alloy-consensus" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "derive_more", "reth-codecs", @@ -7856,7 +7767,7 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", "alloy-network", @@ -7869,7 +7780,7 @@ dependencies = [ [[package]] name = "op-alloy-provider" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-network", "alloy-primitives", @@ -7883,15 +7794,15 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "op-alloy-consensus", "reth-rpc-traits", @@ -7903,15 +7814,14 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-engine", - "alloy-serde 2.0.1", - "derive_more", + "alloy-serde 2.0.4", "ethereum_ssz", "ethereum_ssz_derive", "op-alloy-consensus", @@ -7924,7 +7834,7 @@ dependencies = [ [[package]] name = "op-revm" version = "19.0.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "auto_impl", "revm", @@ -8142,16 +8052,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.14.0", -] - [[package]] name = "pharos" version = "0.5.3" @@ -8168,7 +8068,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -8178,7 +8077,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", + "phf_macros", "phf_shared 0.13.1", "serde", ] @@ -8223,19 +8122,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "phf_macros" version = "0.13.1" @@ -8571,7 +8457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8740,7 +8626,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -9091,9 +8977,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -9135,12 +9021,12 @@ dependencies = [ [[package]] name = "reth-chainspec" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-primitives", @@ -9155,11 +9041,12 @@ dependencies = [ [[package]] name = "reth-codecs" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce542a96bf888f31854803e80b3340bc233927743aa580838014e8a88fe0d66" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-trie", @@ -9173,8 +9060,9 @@ dependencies = [ [[package]] name = "reth-codecs-derive" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634c90f1cc0f9887680ca785b0b21aa961070b9465917bf65afaec56a6d005bb" dependencies = [ "proc-macro2", "quote", @@ -9183,8 +9071,8 @@ dependencies = [ [[package]] name = "reth-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9196,11 +9084,11 @@ dependencies = [ [[package]] name = "reth-consensus-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9209,8 +9097,8 @@ dependencies = [ [[package]] name = "reth-db-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9233,10 +9121,10 @@ dependencies = [ [[package]] name = "reth-db-models" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "bytes", "modular-bitfield", @@ -9247,11 +9135,11 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9263,8 +9151,8 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -9276,11 +9164,11 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-eth", "reth-codecs", @@ -9290,11 +9178,11 @@ dependencies = [ [[package]] name = "reth-evm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "auto_impl", @@ -9312,11 +9200,11 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rpc-types-engine", @@ -9332,8 +9220,8 @@ dependencies = [ [[package]] name = "reth-execution-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-evm", "alloy-primitives", @@ -9345,11 +9233,11 @@ dependencies = [ [[package]] name = "reth-execution-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rlp", @@ -9364,8 +9252,8 @@ dependencies = [ [[package]] name = "reth-network-peers" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9377,11 +9265,12 @@ dependencies = [ [[package]] name = "reth-primitives-traits" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee12e304adbacbb32248c9806ebafbe1e2811fbfefe53c5e5b710a8438b7ec0" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-rlp", @@ -9405,8 +9294,8 @@ dependencies = [ [[package]] name = "reth-prune-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9420,8 +9309,8 @@ dependencies = [ [[package]] name = "reth-revm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9433,8 +9322,8 @@ dependencies = [ [[package]] name = "reth-rpc-convert" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9453,9 +9342,9 @@ dependencies = [ [[package]] name = "reth-rpc-traits" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b766da61ec7c46596386b4bc88d9b57d1939d3da2bc9e927567a8a23650e5ce9" +checksum = "860fe223501a76ff14aa3bf164f739f31008c2a2905ac85708bfd88f042e6151" dependencies = [ "alloy-consensus", "alloy-network", @@ -9468,8 +9357,8 @@ dependencies = [ [[package]] name = "reth-stages-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "bytes", @@ -9481,8 +9370,8 @@ dependencies = [ [[package]] name = "reth-static-file-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9495,11 +9384,11 @@ dependencies = [ [[package]] name = "reth-storage-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "auto_impl", @@ -9518,10 +9407,10 @@ dependencies = [ [[package]] name = "reth-storage-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "derive_more", @@ -9536,14 +9425,14 @@ dependencies = [ [[package]] name = "reth-trie-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "arrayvec", "bytes", @@ -9559,8 +9448,9 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12fafa33d2f420a9d39249a3e0357b1928d09429f30758b85280409092873b2" dependencies = [ "zstd", ] @@ -9843,9 +9733,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -9853,20 +9743,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.4.0" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rtoolbox" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327b72899159dfae8060c51a1f6aebe955245bcd9cc4997eed0f623caea022e4" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", "windows-sys 0.59.0", @@ -9874,9 +9764,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" dependencies = [ "alloy-rlp", "arbitrary", @@ -10001,14 +9891,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -10034,9 +9924,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -10044,13 +9934,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -10060,7 +9950,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10478,9 +10368,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", @@ -10497,9 +10387,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -10560,14 +10450,14 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -10701,9 +10591,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -10842,7 +10732,7 @@ dependencies = [ "derive_more", "dunce", "inturn", - "itertools 0.12.1", + "itertools 0.14.0", "itoa", "normalize-path", "once_map", @@ -10877,7 +10767,7 @@ dependencies = [ "alloy-primitives", "bitflags 2.11.1", "bumpalo", - "itertools 0.12.1", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", @@ -11011,18 +10901,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -11181,7 +11059,7 @@ checksum = "4572dd9845e37ca0293acb5fe591a7f61b51f1b7b62d3dc6fb8e99e2664f3755" dependencies = [ "const-hex", "dirs", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -11301,22 +11179,22 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tempo-alloy" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-contract", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer-local", "alloy-sol-types", "alloy-transport", @@ -11334,9 +11212,9 @@ dependencies = [ [[package]] name = "tempo-chainspec" version = "1.5.3" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-hardforks", @@ -11353,7 +11231,7 @@ dependencies = [ [[package]] name = "tempo-consensus" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11371,7 +11249,7 @@ dependencies = [ [[package]] name = "tempo-contracts" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-contract", "alloy-primitives", @@ -11382,7 +11260,7 @@ dependencies = [ [[package]] name = "tempo-evm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11409,7 +11287,7 @@ dependencies = [ [[package]] name = "tempo-precompiles" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "alloy-evm", @@ -11429,7 +11307,7 @@ dependencies = [ [[package]] name = "tempo-precompiles-macros" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "proc-macro2", @@ -11440,15 +11318,15 @@ dependencies = [ [[package]] name = "tempo-primitives" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "aws-lc-rs", "base64 0.22.1", "derive_more", @@ -11471,7 +11349,7 @@ dependencies = [ [[package]] name = "tempo-revm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11502,15 +11380,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "term" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "terminal_size" version = "0.4.4" @@ -11518,7 +11387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11552,9 +11421,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" [[package]] name = "thiserror" @@ -11695,9 +11564,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -11864,9 +11733,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap 2.14.0", + "serde_core", + "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -11875,7 +11746,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -12220,9 +12091,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -12506,9 +12377,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174a690eb3293a5666442b0738d080df9ea6b9e03782bbe78875c89ff914a77c" +checksum = "2d7cb4a83971db3f6ae36f0aa41eaf5985d2e2b469581fa755c132f9c2a1ec89" dependencies = [ "anyhow", "bon", @@ -12519,9 +12390,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f628f4acc90a5c1a8136495eaf5f9ef94e03c174d6fb2e6de691bc58fc721ee" +checksum = "bba14c9676943b2899cea2ed7ea194b89b3d13564a3c93a61882a978b123a41c" dependencies = [ "anyhow", "bon", @@ -12533,9 +12404,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390d0442b660baedd7a6f60d2af01bd8967e0d7fe69cd15e13c82c811d82b709" +checksum = "fb684e6d170ef15a9b3c20561779a50ba8c806f8acdaff47c0a2c5c4c6cadd43" dependencies = [ "anyhow", "bon", @@ -12600,11 +12471,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -12613,14 +12484,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -12631,9 +12502,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -12641,9 +12512,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12651,9 +12522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -12664,9 +12535,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -12764,7 +12635,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -12804,9 +12675,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -12824,13 +12695,13 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", - "string_cache 0.9.0", + "string_cache", "string_cache_codegen", ] @@ -12841,7 +12712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni 0.22.4", + "jni", "log", "ndk-context", "objc2", @@ -12914,7 +12785,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13035,15 +12906,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -13080,21 +12942,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -13137,12 +12984,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -13155,12 +12996,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -13173,12 +13008,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -13203,12 +13032,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -13221,12 +13044,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -13239,12 +13056,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -13257,12 +13068,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -13286,9 +13091,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -13302,6 +13107,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index c412dad16366c..4db027dd400cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.6.0" +version = "1.7.1" edition = "2024" rust-version = "1.89" authors = ["Foundry Contributors"] @@ -174,9 +174,6 @@ foundry-compilers.opt-level = 3 serde_json.opt-level = 3 serde.opt-level = 3 -foundry-solang-parser.opt-level = 3 -lalrpop-util.opt-level = 3 - solar-compiler.opt-level = 3 solar-ast.opt-level = 3 solar-data-structures.opt-level = 3 @@ -307,40 +304,40 @@ crossterm.opt-level = "s" alloy-json-abi.opt-level = "s" [workspace.dependencies] -anvil = { path = "crates/anvil" } -cast = { path = "crates/cast" } -chisel = { path = "crates/chisel" } -forge = { path = "crates/forge" } - -forge-doc = { path = "crates/doc" } -forge-fmt = { path = "crates/fmt" } -forge-lint = { path = "crates/lint" } -forge-verify = { path = "crates/verify" } -forge-script = { path = "crates/script" } -forge-sol-macro-gen = { path = "crates/sol-macro-gen" } -forge-script-sequence = { path = "crates/script-sequence" } -foundry-cheatcodes = { path = "crates/cheatcodes" } +anvil = { path = "crates/anvil", default-features = false } +cast = { path = "crates/cast", default-features = false } +chisel = { path = "crates/chisel", default-features = false } +forge = { path = "crates/forge", default-features = false } + +forge-doc = { path = "crates/doc", default-features = false } +forge-fmt = { path = "crates/fmt", default-features = false } +forge-lint = { path = "crates/lint", default-features = false } +forge-verify = { path = "crates/verify", default-features = false } +forge-script = { path = "crates/script", default-features = false } +forge-sol-macro-gen = { path = "crates/sol-macro-gen", default-features = false } +forge-script-sequence = { path = "crates/script-sequence", default-features = false } +foundry-cheatcodes = { path = "crates/cheatcodes", default-features = false } foundry-cheatcodes-spec = { path = "crates/cheatcodes/spec" } -foundry-cli = { path = "crates/cli" } +foundry-cli = { path = "crates/cli", default-features = false } foundry-cli-markdown = { path = "crates/cli-markdown" } -foundry-common = { path = "crates/common" } -foundry-common-fmt = { path = "crates/common/fmt" } +foundry-common = { path = "crates/common", default-features = false } +foundry-common-fmt = { path = "crates/common/fmt", default-features = false } foundry-config = { path = "crates/config" } -foundry-debugger = { path = "crates/debugger" } -foundry-evm = { path = "crates/evm/evm" } +foundry-debugger = { path = "crates/debugger", default-features = false } +foundry-evm = { path = "crates/evm/evm", default-features = false } foundry-evm-abi = { path = "crates/evm/abi" } -foundry-evm-core = { path = "crates/evm/core" } -foundry-evm-coverage = { path = "crates/evm/coverage" } -foundry-evm-hardforks = { path = "crates/evm/hardforks" } -foundry-evm-networks = { path = "crates/evm/networks" } -foundry-evm-fuzz = { path = "crates/evm/fuzz" } +foundry-evm-core = { path = "crates/evm/core", default-features = false } +foundry-evm-coverage = { path = "crates/evm/coverage", default-features = false } +foundry-evm-hardforks = { path = "crates/evm/hardforks", default-features = false } +foundry-evm-networks = { path = "crates/evm/networks", default-features = false } +foundry-evm-fuzz = { path = "crates/evm/fuzz", default-features = false } foundry-evm-sancov = { path = "crates/evm/sancov" } -foundry-evm-traces = { path = "crates/evm/traces" } +foundry-evm-traces = { path = "crates/evm/traces", default-features = false } foundry-macros = { path = "crates/macros" } -foundry-test-utils = { path = "crates/test-utils" } +foundry-test-utils = { path = "crates/test-utils", default-features = false } foundry-wallets = { version = "0.1.0", default-features = false } foundry-linking = { path = "crates/linking" } -foundry-primitives = { path = "crates/primitives" } +foundry-primitives = { path = "crates/primitives", default-features = false } # solc & compilation utilities foundry-block-explorers = { version = "0.23.0", default-features = false } @@ -349,7 +346,6 @@ foundry-compilers = { version = "0.20.0", default-features = false, features = [ "svm-solc", ] } foundry-fork-db = { version = "0.26.0", features = ["zstd"] } -solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar = { package = "solar-compiler", version = "=0.1.8", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ "rustls", @@ -410,7 +406,7 @@ op-alloy-rpc-types = "0.24.0" op-alloy-flz = "0.13.1" ## alloy-evm -alloy-evm = "0.33.2" +alloy-evm = "0.34.0" alloy-op-evm = "0.31.0" # revm @@ -515,17 +511,17 @@ mpp = { git = "https://github.com/tempoxyz/mpp-rs", rev = "554d20112eb014bd223d5 "reqwest-rustls-tls", "ws", ] } -tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false, features = [ +tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false, features = [ "serde", ] } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false } -tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a", default-features = false, features = [ +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false } +tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa", default-features = false, features = [ "serde", ] } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } ## Pinned dependencies. Enabled for the workspace in crates/test-utils. @@ -589,27 +585,21 @@ rexpect = { git = "https://github.com/rust-cli/rexpect", rev = "2ed0b1898d7edaf6 ## alloy-evm # alloy-evm = { git = "https://github.com/paradigmxyz/evm.git", rev = "04d8e4a" } -## reth-core -reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } -reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } - ## op-revm / op-alloy / alloy-op-evm -op-revm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } -alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +op-revm = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } +alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "e3b59e76588f99db17205f5601e45a5b00f0bfbb" } ## foundry-fork-db # foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "2f90eb86d4549fa15a8cc2d99bfc1039bc083977" } ## tempo — unify crates.io versions (pulled by mpp) with git rev -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "8bd4d01d37e3cc324030baacbce2da0862d7735a" } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "6bf9903d6a75cc264029dcf54183adea38d3cfaa" } # solar solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } diff --git a/README.md b/README.md index c9f0a45c57b0a..90cd1d8a7865e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ foundryup See the [installation guide](https://getfoundry.sh/getting-started/installation) for more details. +To verify a downloaded release archive or container image, see [Verifying Releases](./SECURITY.md#verifying-releases). + ## Getting Started Initialize a new project, build and test: diff --git a/SECURITY.md b/SECURITY.md index d84327cc18e91..6296066db5e73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,3 +3,112 @@ ## Reporting a Vulnerability Contact [security@tempo.xyz](mailto:security@tempo.xyz). + +## Verifying Releases + +Every official Foundry release ships with multiple, independent integrity +artifacts. All signing is keyless via [Sigstore](https://www.sigstore.dev/) — +no Foundry-managed key material is involved, and every signature is recorded +in the public [Rekor](https://docs.sigstore.dev/logging/overview/) transparency +log. The signing identity is the GitHub Actions OIDC token of this repository's +`release.yml` / `docker-publish.yml` workflows. + +### Per-release artifacts + +For each `foundry___.{tar.gz,zip}` archive on the +[releases page](https://github.com/foundry-rs/foundry/releases), the same +release also publishes: + +| Suffix | Purpose | +| --- | --- | +| `.sha256` | SHA-256 checksum of the archive (`sha256sum` format) | +| `.sigstore.json` | Cosign keyless signature bundle (cert + signature + Rekor proof) over the archive | +| `.spdx.json` | SPDX 2.3 SBOM of the source workspace used for the build | +| `.attestation.txt` | URL of the GitHub artifact-attestation summary | + +In addition, GitHub stores SLSA build-provenance and SBOM attestations against +the archive's digest; these are queryable via `gh attestation` without +downloading anything else. + +### Verifying an archive + +Pick whichever toolchain you have available — they verify the same signatures. + +#### Option 1: GitHub CLI (simplest) + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry +``` + +This computes the file's digest, fetches the matching attestation from GitHub, +and verifies the Sigstore signature plus the SLSA provenance predicate. Add +`--signer-workflow foundry-rs/foundry/.github/workflows/release.yml` to also +require the workflow identity. + +To verify the SBOM attestation specifically: + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry \ + --predicate-type 'https://spdx.dev/Document/v2.3' +``` + +#### Option 2: Cosign (offline-friendly) + +Download the archive and its `.sigstore.json` bundle from the release page, +then: + +```bash +cosign verify-blob \ + --bundle foundry_v1.4.0_linux_amd64.sigstore.json \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/release\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + foundry_v1.4.0_linux_amd64.tar.gz +``` + +For nightly builds the certificate identity points at `refs/heads/master` +instead of a tag; the regex above matches both. + +#### Option 3: Plain checksum (integrity only) + +```bash +sha256sum -c foundry_v1.4.0_linux_amd64.sha256 # GNU coreutils +shasum -a 256 -c foundry_v1.4.0_linux_amd64.sha256 # macOS +``` + +This proves the bytes match what was uploaded, but says nothing about who +uploaded them. Combine with one of the verifications above for end-to-end +trust. + +### Verifying the Docker image + +Container signatures and attestations are pushed as OCI referrers to GHCR, so +no separate files need to be downloaded. + +```bash +# Cosign keyless signature on the image +cosign verify ghcr.io/foundry-rs/foundry:v1.4.0 \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' + +# SLSA build-provenance attestation +gh attestation verify oci://ghcr.io/foundry-rs/foundry:v1.4.0 \ + --repo foundry-rs/foundry + +# Inspect the buildx-attached SBOM and provenance +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .SBOM }}' +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .Provenance }}' +``` + +To pin to an immutable digest (recommended for reproducible deployments): + +```bash +docker pull ghcr.io/foundry-rs/foundry:v1.4.0 +DIGEST=$(docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 --format '{{ .Manifest.Digest }}') +cosign verify "ghcr.io/foundry-rs/foundry@${DIGEST}" \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' +``` diff --git a/benches/LATEST.md b/benches/LATEST.md index 238a691229389..cb75f8d68780b 100644 --- a/benches/LATEST.md +++ b/benches/LATEST.md @@ -1,74 +1,108 @@ -# Foundry Benchmark Results +# 📊 Foundry Benchmark Results -**Date**: 2026-04-24 23:10:24 +**Generated at**: 2026-05-02 21:53:46 UTC -## Repositories Tested +## Forge Test + +### Repositories Tested 1. [ithacaxyz/account](https://github.com/ithacaxyz/account) -2. [Vectorized/solady](https://github.com/Vectorized/solady) -3. [Uniswap/v4-core](https://github.com/Uniswap/v4-core) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) 4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) -5. [aave/aave-v4](https://github.com/aave/aave-v4) - -## Foundry Versions +### Foundry Versions - **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) -- **nightly**: forge Version: 1.6.0-nightly (a249f5c 2026-04-24) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) -## Forge Test - -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| vectorized-solady | 1.46 s | 1.38 s | -| aave-aave-v4 | 4m 14.2s | 3m 29.1s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.78 s | 0.965 s | +| vectorized-solady | 0.995 s | 0.645 s | +| uniswap-v4-core | 5.97 s | 1.51 s | +| sparkdotfi-spark-psm | 19.98 s | 10.20 s | ## Forge Fuzz Test -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | -------- | -| ithacaxyz-account | 2.81 s | 1.59 s | -| vectorized-solady | 1.40 s | 1.34 s | -| Uniswap-v4-core | 3.01 s | 2.87 s | -| sparkdotfi-spark-psm | 2.04 s | 1.87 s | -| aave-aave-v4 | 3m 46.0s | 3m 17.3s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.54 s | 0.923 s | +| vectorized-solady | 0.929 s | 0.617 s | +| uniswap-v4-core | 6.44 s | 1.40 s | +| sparkdotfi-spark-psm | 2.25 s | 2.03 s | ## Forge Test (Isolated) -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| Uniswap-v4-core | 3.50 s | 3.48 s. | -| aave-aave-v4 | 4m 14.0s | 3m 53.4s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 3.05 s | 1.02 s | +| vectorized-solady | 0.871 s | 0.741 s | +| uniswap-v4-core | 6.81 s | 1.68 s | +| sparkdotfi-spark-psm | 21.96 s | 11.26 s | + +## Forge Build -## Forge Build (No Cache) +### Repositories Tested -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| ithacaxyz-account | 26.06 s | 26.61 s | -| vectorized-solady | 14.20 s | 14.26 s | -| Uniswap-v4-core | 2m 1.3s | 2m 5.0s | -| sparkdotfi-spark-psm | 15.16 s | 15.30 s | -| aave-aave-v4 | 3m 37.0s | 3m 35.1s | +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +### No Cache + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 34.58 s | 33.29 s | +| vectorized-solady | 14.40 s | 14.41 s | +| uniswap-v4-core | 2m 17.6s | 2m 17.7s | +| sparkdotfi-spark-psm | 12.62 s | 12.61 s | -## Forge Build (With Cache) +### With Cache -| Repository | v1.5.1 | nightly | -| -------------------- | ------- | ------- | -| ithacaxyz-account | 0.167 s | 0.201 s | -| vectorized-solady | 0.099 s | 0.098 s | -| Uniswap-v4-core | 0.139 s | 0.140 s | -| sparkdotfi-spark-psm | 0.168 s | 0.173 s | -| aave-aave-v4 | 0.370 s | 0.357 s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 0.083 s | 0.089 s | +| vectorized-solady | 0.062 s | 0.064 s | +| uniswap-v4-core | 0.071 s | 0.074 s | +| sparkdotfi-spark-psm | 0.066 s | 0.068 s | ## Forge Coverage -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | ---------- | -| Uniswap-v4-core | 1m 13.9s | 1m 10.3s | -| sparkdotfi-spark-psm | 2m 54.7s | 2m 50.0s | -| aave-aave-v4 | 11m 20.8s | 10m 58.7s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [uniswap/v4-core](https://github.com/uniswap/v4-core) +3. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 29.35 s | 18.69 s | +| uniswap-v4-core | 1m 26.8s | 1m 4.1s | +| sparkdotfi-spark-psm | 2m 1.6s | 1m 28.4s | ## System Information -- **OS**: macos -- **CPU**: 12 -- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) \ No newline at end of file + +- **OS**: linux +- **CPU**: 32 +- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) diff --git a/benches/src/main.rs b/benches/src/main.rs index 60e815cecb0ec..8d7134b1c25bc 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -39,9 +39,15 @@ struct Cli { #[clap(long, default_value = ".")] output_dir: PathBuf, - /// Name of the output file (default: LATEST.md) - #[clap(long, default_value = "LATEST.md")] - output_file: String, + /// Name of the output file. Defaults to LATEST.md unless --json-output is set + /// without this flag, in which case no Markdown is written. + #[clap(long)] + output_file: Option, + + /// Filename for a flat JSON summary (benchmark/repo -> mean_seconds). + /// Resolved relative to --output-dir. Used by the nightly regression comparison script. + #[clap(long)] + json_output: Option, /// Run only specific benchmarks (comma-separated: /// forge_test,forge_build_no_cache,forge_build_with_cache,forge_fuzz_test,forge_coverage) @@ -216,12 +222,28 @@ fn main() -> Result<()> { } } - // Generate markdown report - sh_println!("📝 Generating report..."); - let markdown = results.generate_markdown(&versions, &repos); - let output_path = cli.output_dir.join(cli.output_file); - fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; - sh_println!("✅ Report written to: {}", output_path.display()); + // Write Markdown report unless --json-output is set without an explicit --output-file. + let md_filename = match cli.output_file { + Some(f) => Some(f), + None if cli.json_output.is_none() => Some("LATEST.md".to_string()), + None => None, + }; + if let Some(filename) = md_filename { + sh_println!("📝 Generating report..."); + let markdown = results.generate_markdown(&versions, &repos); + let output_path = cli.output_dir.join(filename); + fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; + sh_println!("✅ Report written to: {}", output_path.display()); + } + + if let Some(json_filename) = cli.json_output { + let summary = results.generate_json_summary(&versions); + let json = + serde_json::to_string_pretty(&summary).wrap_err("Failed to serialize JSON summary")?; + let json_path = cli.output_dir.join(json_filename); + fs::write(&json_path, json).wrap_err("Failed to write JSON summary")?; + sh_println!("✅ JSON summary written to: {}", json_path.display()); + } Ok(()) } diff --git a/benches/src/results.rs b/benches/src/results.rs index 447e8ed2766b4..e7d57250fc9a1 100644 --- a/benches/src/results.rs +++ b/benches/src/results.rs @@ -66,6 +66,25 @@ impl BenchmarkResults { self.version_details.insert(version.to_string(), details); } + /// Generate a flat JSON summary mapping `"benchmark/repo" -> mean_seconds`. + /// + /// Used by the nightly regression comparison script. + pub fn generate_json_summary(&self, versions: &[String]) -> HashMap { + let mut summary = HashMap::new(); + for (benchmark_name, version_data) in &self.data { + for version in versions { + if let Some(repo_data) = version_data.get(version) { + for (repo_name, result) in repo_data { + let key = format!("{benchmark_name}/{repo_name}"); + let rounded = (result.mean * 10_000.0).round() / 10_000.0; + summary.insert(key, rounded); + } + } + } + } + summary + } + pub fn generate_markdown(&self, versions: &[String], repos: &[RepoConfig]) -> String { let mut output = String::new(); diff --git a/benchmark.sh b/benchmark.sh index 2faffa93dfa1d..ac6159099069b 100755 --- a/benchmark.sh +++ b/benchmark.sh @@ -1,36 +1,52 @@ #!/bin/bash -versions="v1.3.6,v1.4.0-rc1" +versions="v1.5.1,v1.7.0" # Repositories -export ITHACA_ACCOUNT="ithacaxyz/account:v0.3.2" -export SOLADY_REPO="Vectorized/solady:v0.1.22" -export UNISWAP_V4_CORE="Uniswap/v4-core:59d3ecf" -export SPARK_PSM="sparkdotfi/spark-psm:v1.0.0" +ITHACA_ACCOUNT="ithacaxyz/account:v0.5.7" +SOLADY_REPO="vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test'" +AAVE_V4="aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" +UNISWAP_V4_CORE="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc TickMathTestTest" +SPARK_PSM="sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting" -# Benches -echo "===========FORGE TEST AND BUILD BENCHMARKS===========" +SOLADY_ISOLATE="vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest'" +ITHACA_ISOLATE="ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_test,forge_fuzz_test,forge_build_no_cache,forge_build_with_cache \ - --output-dir ./benches/results \ - --output-file TEST_BUILD.md +SOLADY_BUILD="vectorized/solady:v0.1.26" +UNISWAP_BUILD="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75" +SPARK_PSM_BUILD="sparkdotfi/spark-psm:v1.0.0" -echo "===========FORGE COVERAGE BENCHMARKS===========" +# Benches +echo "===========FORGE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_coverage \ - --output-dir ./benches/results \ - --output-file COVERAGE.md +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_REPO,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ + --benchmarks forge_test,forge_fuzz_test \ + --output-dir ./benches \ + --output-file forge_test_bench.md echo "===========FORGE ISOLATE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ISOLATE,$SOLADY_ISOLATE,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ --benchmarks forge_isolate_test \ - --output-dir ./benches/results \ - --output-file ISOLATE_TEST.md + --output-dir ./benches \ + --output-file forge_isolate_test_bench.md + +echo "===========FORGE BUILD BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_BUILD,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --output-dir ./benches \ + --output-file forge_build_bench.md + +echo "===========FORGE COVERAGE BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_coverage \ + --output-dir ./benches \ + --output-file forge_coverage_bench.md echo "===========BENCHMARKS COMPLETED===========" diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index b664266450d07..d6404b21e2e8e 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -20,15 +20,15 @@ required-features = ["cli"] [dependencies] # foundry internal -anvil-core = { path = "core" } +anvil-core = { path = "core", default-features = false } anvil-rpc = { path = "rpc" } anvil-server = { path = "server" } -foundry-cli.workspace = true +foundry-cli = { workspace = true, optional = true } foundry-common.workspace = true foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-networks.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } tempo-chainspec.workspace = true tempo-primitives.workspace = true tempo-precompiles.workspace = true @@ -37,7 +37,7 @@ tempo-revm.workspace = true # alloy alloy-evm = { workspace = true, features = ["call-util"] } -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = ["serde"] } alloy-consensus = { workspace = true, features = ["k256", "kzg"] } alloy-contract = { workspace = true, features = ["pubsub"] } @@ -63,7 +63,8 @@ alloy-transport.workspace = true alloy-chains.workspace = true alloy-genesis.workspace = true alloy-trie.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde"] } +op-alloy-consensus = { workspace = true, features = ["serde"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } # revm revm = { workspace = true, features = [ @@ -73,7 +74,7 @@ revm = { workspace = true, features = [ "c-kzg", ] } revm-inspectors.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } # axum related axum.workspace = true @@ -120,17 +121,28 @@ reqwest.workspace = true foundry-test-utils.workspace = true tokio = { workspace = true, features = ["full"] } -op-alloy-rpc-types.workspace = true tempo-alloy.workspace = true [features] -default = ["cli", "jemalloc", "asm-keccak"] +default = ["cli", "jemalloc", "asm-keccak", "optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "anvil-core/optimism", + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli?/optimism", +] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] -jemalloc = ["foundry-cli/jemalloc"] -mimalloc = ["foundry-cli/mimalloc"] -tracy-allocator = ["foundry-cli/tracy-allocator"] +jemalloc = ["foundry-cli?/jemalloc"] +mimalloc = ["foundry-cli?/mimalloc"] +tracy-allocator = ["foundry-cli?/tracy-allocator"] cli = ["tokio/full", "cmd"] cmd = [ + "dep:foundry-cli", "clap", "clap_complete", "dep:fdlimit", diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index cf4b952ecfaa3..8456413a78b1f 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [dependencies] foundry-common.workspace = true foundry-evm.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } revm = { workspace = true, default-features = false, features = [ "std", "serde", @@ -39,3 +39,11 @@ bytes.workspace = true # misc rand.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index fb75529b82908..4fd2aabb82a8b 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -12,7 +12,9 @@ use clap::Parser; use core::fmt; use foundry_common::shell; use foundry_config::{Chain, Config, FigmentProviders}; -use foundry_evm::hardfork::{EthereumHardfork, OpHardfork}; +#[cfg(feature = "optimism")] +use foundry_evm::hardfork::OpHardfork; +use foundry_evm::hardfork::{EthereumHardfork, FoundryHardfork}; use foundry_evm_networks::NetworkConfigs; use foundry_primitives::FoundryReceiptEnvelope; use futures::FutureExt; @@ -240,15 +242,7 @@ impl NodeArgs { } let hardfork = match &self.hardfork { - Some(hf) => { - if self.evm.networks.is_optimism() { - Some(OpHardfork::from_str(hf)?.into()) - } else if self.evm.networks.is_tempo() { - Some(TempoHardfork::from_str(hf)?.into()) - } else { - Some(EthereumHardfork::from_str(hf)?.into()) - } - } + Some(hf) => Some(parse_hardfork(hf, &self.evm.networks)?), None => None, }; @@ -849,6 +843,19 @@ impl FromStr for ForkUrl { } } +/// Parses a hardfork string against the active network configuration. +fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result { + #[cfg(feature = "optimism")] + if networks.is_optimism() { + return Ok(OpHardfork::from_str(hf)?.into()); + } + if networks.is_tempo() { + Ok(TempoHardfork::from_str(hf)?.into()) + } else { + Ok(EthereumHardfork::from_str(hf)?.into()) + } +} + /// Clap's value parser for genesis. Loads a genesis.json file. fn read_genesis_file(path: &str) -> Result { foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string()) diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 23cd6e61bc076..7c91590c071fa 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -37,7 +37,7 @@ use foundry_config::Config; use foundry_evm::{ backend::{BlockchainDb, BlockchainDbMeta, SharedBackend}, constants::DEFAULT_CREATE2_DEPLOYER, - hardfork::{FoundryHardfork, OpHardfork}, + hardfork::FoundryHardfork, utils::{ apply_chain_and_block_specific_env_changes, block_env_from_header, get_blob_base_fee_update_fraction, @@ -577,8 +577,9 @@ impl NodeConfig { if let Some(hardfork) = self.hardfork { return hardfork; } + #[cfg(feature = "optimism")] if self.networks.is_optimism() { - return OpHardfork::default().into(); + return foundry_evm::hardforks::OpHardfork::default().into(); } if self.networks.is_tempo() { return TempoHardfork::default().into(); @@ -1079,6 +1080,7 @@ impl NodeConfig { } /// Enable Optimism network features. + #[cfg(feature = "optimism")] #[must_use] pub fn with_optimism(mut self) -> Self { self.networks = NetworkConfigs::with_optimism(); diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 78cce938318c1..4900c18eebd82 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -1882,6 +1882,7 @@ impl EthApi { fn sign_request(&self, from: &Address, typed_tx: FoundryTypedTx) -> Result { match typed_tx { + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => return Ok(build_impersonated(typed_tx)), _ => { for signer in self.signers.iter() { @@ -2210,9 +2211,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; - debug_assert!(requires != provides); + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -2288,11 +2293,10 @@ impl EthApi { let priority = self.transaction_priority(&pending_transaction.transaction); // Tempo txs use a 2D nonce system — no sequential ordering by account nonce. - let (requires, provides) = if let FoundryTxEnvelope::Tempo(aa_tx) = - pending_transaction.transaction.as_ref() - && !aa_tx.tx().nonce_key.is_zero() + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) { - (vec![], vec![pending_transaction.hash().to_vec()]) + (requires, provides) } else { let on_chain_nonce = self.backend.current_nonce(from).await?; let nonce = pending_transaction.transaction.nonce(); @@ -3192,8 +3196,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -3549,6 +3558,7 @@ impl EthApi { requires: Vec, provides: Vec, ) -> Result { + debug_assert!(requires != provides); let from = *pending_transaction.sender(); let priority = self.transaction_priority(&pending_transaction.transaction); let pool_transaction = @@ -3565,7 +3575,9 @@ impl EthApi { FoundryTxEnvelope::Eip1559(_) => self.backend.ensure_eip1559_active(), FoundryTxEnvelope::Eip4844(_) => self.backend.ensure_eip4844_active(), FoundryTxEnvelope::Eip7702(_) => self.backend.ensure_eip7702_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => Err(BlockchainError::InvalidTransactionRequest( "not implemented for post-exec tx".to_string(), )), @@ -3634,6 +3646,20 @@ fn required_marker(provided_nonce: u64, on_chain_nonce: u64, from: Address) -> V if on_chain_nonce <= prev_nonce { vec![to_marker(prev_nonce, from)] } else { Vec::new() } } +fn tempo_parallel_nonce_markers( + pending_transaction: &PendingTransaction, +) -> Option<(Vec, Vec)> { + // Tempo txs with non-zero nonce_key use a 2D nonce system and should not + // be sequenced by account nonce markers. + if let FoundryTxEnvelope::Tempo(aa_tx) = pending_transaction.transaction.as_ref() + && !aa_tx.tx().nonce_key.is_zero() + { + Some((vec![], vec![pending_transaction.hash().to_vec()])) + } else { + None + } +} + fn convert_transact_out(out: &Option) -> Bytes { match out { None => Default::default(), diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 614f409c3eb65..c41937055fdd4 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -67,9 +67,11 @@ impl ReceiptBuilder for FoundryReceiptBuilder { FoundryTxType::Eip1559 => FoundryReceiptEnvelope::Eip1559(receipt), FoundryTxType::Eip4844 => FoundryReceiptEnvelope::Eip4844(receipt), FoundryTxType::Eip7702 => FoundryReceiptEnvelope::Eip7702(receipt), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { unreachable!("deposit receipts are built in commit_transaction") } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt), FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt), } @@ -85,7 +87,7 @@ pub struct AnvilTxResult { pub sender: Address, } -impl TxResult for AnvilTxResult { +impl TxResult for AnvilTxResult { type HaltReason = H; fn result(&self) -> &ResultAndState { @@ -217,12 +219,10 @@ where }) } - fn commit_transaction( - &mut self, - output: Self::Result, - ) -> Result { + fn commit_transaction(&mut self, output: Self::Result) -> GasOutput { let AnvilTxResult { inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type }, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] sender, } = output; @@ -237,6 +237,7 @@ where self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used); } + #[cfg(feature = "optimism")] let receipt = if tx_type == FoundryTxType::Deposit { let deposit_nonce = state.get(&sender).map(|acc| acc.info.nonce); let receipt = alloy_consensus::Receipt { @@ -262,11 +263,19 @@ where cumulative_gas_used: self.gas_used, }) }; + #[cfg(not(feature = "optimism"))] + let receipt = self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx_type, + evm: &self.evm, + result, + state: &state, + cumulative_gas_used: self.gas_used, + }); self.receipts.push(receipt); self.evm.db_mut().commit(state); - Ok(GasOutput::new(gas_used)) + GasOutput::new(gas_used) } fn finish( @@ -429,7 +438,7 @@ where let exec_result = result.result().result.clone(); let gas_used = result.result().result.tx_gas_used(); - executor.commit_transaction(result).expect("commit failed"); + executor.commit_transaction(result); let traces = executor.evm_mut().inspector_mut().finish_transaction(inspector_config); diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index f404031445611..1c8dfffab9d95 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -57,6 +57,7 @@ use alloy_network::{ AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, Network, NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, UnknownTypedTransaction, }; +#[cfg(feature = "optimism")] use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; use alloy_primitives::{ Address, B256, Bloom, Bytes, TxHash, TxKind, U64, U256, hex, keccak256, logs_bloom, @@ -108,20 +109,60 @@ use foundry_evm::{ }, }; use foundry_evm_networks::NetworkConfigs; +#[cfg(feature = "optimism")] +use foundry_primitives::get_deposit_tx_parts; use foundry_primitives::{ FoundryNetwork, FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope, - FoundryTxReceipt, get_deposit_tx_parts, + FoundryTxReceipt, }; use futures::channel::mpsc::{UnboundedSender, unbounded}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait}; -use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +#[cfg(feature = "optimism")] +use op_revm::{OpSpecId, OpTransaction, transaction::deposit::DepositTransactionParts}; + +/// Side-channel container for OP-specific deposit info produced by +/// [`Backend::build_call_env`] and consumed by the OP transact path. +/// +/// When the `optimism` feature is enabled, this is an alias for +/// `op_revm::DepositTransactionParts`. When disabled, it is a zero-sized +/// stand-in so the eth/tempo dispatch chain still type-checks. +#[cfg(feature = "optimism")] +type OpCallDepositInfo = DepositTransactionParts; +#[cfg(not(feature = "optimism"))] +#[derive(Default, Clone, Debug)] +struct OpCallDepositInfo; + +/// Marker trait that abstracts over the per-network inspector trait bounds +/// required by the in-memory backend. The OP bound is only included when the +/// `optimism` feature is enabled. +#[cfg(feature = "optimism")] +pub trait BackendInspector: + Inspector> + Inspector> + Inspector> +{ +} +#[cfg(feature = "optimism")] +impl BackendInspector for T where + T: Inspector> + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +pub trait BackendInspector: + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +impl BackendInspector for T where + T: Inspector> + Inspector> +{ +} use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ DatabaseCommit, Inspector, context::{Block as RevmBlock, BlockEnv, Cfg, TxEnv}, context_interface::{ block::BlobExcessGasAndPrice, - result::{EVMError, ExecutionResult, HaltReason, Output, ResultAndState}, + result::{ExecutionResult, HaltReason, Output, ResultAndState}, }, database::{CacheDB, DbAccount, WrapDatabaseRef}, interpreter::InstructionResult, @@ -157,6 +198,8 @@ pub mod cache; pub mod fork_db; pub mod in_memory_db; pub mod inspector; +#[cfg(feature = "optimism")] +pub mod optimism; pub mod state; pub mod storage; @@ -419,6 +462,11 @@ impl Backend { self.genesis.timestamp } + /// Returns the configured genesis block number. + pub const fn genesis_number(&self) -> u64 { + self.genesis.number + } + /// Returns balance of the given account. pub async fn current_balance(&self, address: Address) -> DatabaseResult { Ok(self.get_account(address).await?.balance) @@ -490,12 +538,21 @@ impl Backend { } /// Returns true if op-stack deposits are active - pub fn is_optimism(&self) -> bool { + #[cfg(feature = "optimism")] + pub const fn is_optimism(&self) -> bool { self.networks.is_optimism() } + /// Returns true if op-stack deposits are active. + /// + /// Always `false` when built without the `optimism` feature. + #[cfg(not(feature = "optimism"))] + pub const fn is_optimism(&self) -> bool { + false + } + /// Returns true if Tempo network mode is active - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { self.networks.is_tempo() } @@ -589,7 +646,8 @@ impl Backend { } /// Returns an error if op-stack deposits are not active - pub fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { + #[cfg(feature = "optimism")] + pub const fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { if self.is_optimism() { return Ok(()); } @@ -597,7 +655,7 @@ impl Backend { } /// Returns an error if Tempo transactions are not active - pub fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { + pub const fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { if self.is_tempo() { return Ok(()); } @@ -1139,58 +1197,53 @@ impl Backend { db: &'db DB, evm_env: &EvmEnv, inspector: &mut I, - tx_env: OpTransaction, + tx_env: TxEnv, + op_deposit: OpCallDepositInfo, ) -> Result, BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_env = EvmEnv::new( - evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), - evm_env.block_env.clone(), - ); - let mut evm = OpEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - op_env, - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { - EVMError::Database(db) => EVMError::Database(db), - EVMError::Header(h) => EVMError::Header(h), - EVMError::Custom(s) => EVMError::Custom(s), - EVMError::CustomAny(err) => EVMError::CustomAny(err), - EVMError::Transaction(t) => EVMError::Transaction(t), - })?; - Ok(ResultAndState { - result: result.result.map_haltreason(|h| match h { - OpHaltReason::Base(eth) => eth, - _ => HaltReason::PrecompileError, - }), - state: result.state, - }) - } else if self.is_tempo() { - self.transact_tempo_with_inspector_ref( - db, - evm_env, - inspector, - TempoTxEnv::from(tx_env.base), - ) + let op_tx = OpTransaction { base: tx_env, deposit: op_deposit, ..Default::default() }; + return self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx); + } + // `op_deposit` only matters on the OP path; eth/tempo ignore it. + let _ = op_deposit; + if self.is_tempo() { + self.transact_tempo_with_inspector_ref(db, evm_env, inspector, TempoTxEnv::from(tx_env)) } else { - let mut evm = EthEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - evm_env.clone(), - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - Ok(evm.transact(tx_env.base)?) + self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env) } } + /// Eth path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an Ethereum EVM, injects precompiles, and transacts with a + /// plain [`TxEnv`]. + fn transact_eth_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: TxEnv, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let mut evm = EthEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + evm_env.clone(), + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + Ok(evm.transact(tx_env)?) + } + /// Builds the appropriate tx env from a [`FoundryTxEnvelope`], executes via the correct /// EVM backend (Op/Tempo/Eth), and returns both the result and the base [`TxEnv`]. fn transact_envelope_with_inspector_ref<'db, I, DB>( @@ -1203,9 +1256,7 @@ impl Backend { ) -> Result<(ResultAndState, TxEnv), BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { if tx.is_tempo() { @@ -1213,14 +1264,21 @@ impl Backend { FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); let base = tx_env.inner.clone(); let result = self.transact_tempo_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) - } else { - let tx_env: OpTransaction = + return Ok((result, base)); + } + #[cfg(feature = "optimism")] + if self.is_optimism() { + let op_tx: OpTransaction = FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); - let base = tx_env.base.clone(); - let result = self.transact_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) + let base = op_tx.base.clone(); + let result = self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx)?; + return Ok((result, base)); } + let tx_env: TxEnv = + FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); + let base = tx_env.clone(); + let result = self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env)?; + Ok((result, base)) } /// Creates a Tempo EVM, injects precompiles, and transacts with a native [`TempoTxEnv`]. @@ -1298,6 +1356,7 @@ impl Backend { }}; } + #[cfg(feature = "optimism")] if self.is_optimism() { let op_env = EvmEnv::new( evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), @@ -1305,8 +1364,10 @@ impl Backend { ); let mut evm = OpEvmFactory::::default().create_evm_with_inspector(db, op_env, inspector); - run!(evm) - } else if self.is_tempo() { + return run!(evm); + } + + if self.is_tempo() { let hardfork = TempoHardfork::from(self.hardfork); let tempo_env = EvmEnv::new( evm_env @@ -1338,7 +1399,7 @@ impl Backend { request: WithOtherFields, fee_details: FeeDetails, block_env: BlockEnv, - ) -> (EvmEnv, OpTransaction) { + ) -> (EvmEnv, TxEnv, OpCallDepositInfo) { let tx_type = request.minimal_tx_type() as u8; let WithOtherFields:: { @@ -1391,7 +1452,7 @@ impl Backend { let caller = from.unwrap_or_default(); let to = to.as_ref().and_then(TxKind::to); let blob_hashes = blob_versioned_hashes.unwrap_or_default(); - let mut base = TxEnv { + let mut tx_env = TxEnv { caller, gas_limit, gas_price, @@ -1413,11 +1474,10 @@ impl Backend { blob_hashes, ..Default::default() }; - base.set_signed_authorization(authorization_list.unwrap_or_default()); - let mut tx_env = OpTransaction { base, ..Default::default() }; + tx_env.set_signed_authorization(authorization_list.unwrap_or_default()); if let Some(nonce) = nonce { - tx_env.base.nonce = nonce; + tx_env.nonce = nonce; } if evm_env.block_env.basefee == 0 { @@ -1427,13 +1487,22 @@ impl Backend { } // Deposit transaction? (only valid when op-stack deposits are active) - if self.ensure_op_deposits_active().is_ok() + #[cfg(feature = "optimism")] + let op_deposit = if self.ensure_op_deposits_active().is_ok() && let Ok(deposit) = get_deposit_tx_parts(&other) { - tx_env.deposit = deposit; - } + deposit + } else { + OpCallDepositInfo::default() + }; + #[cfg(not(feature = "optimism"))] + let op_deposit = { + // `other` carries OP-only deposit fields; consumed only when feature is enabled. + let _ = &other; + OpCallDepositInfo::default() + }; - (evm_env, tx_env) + (evm_env, tx_env, op_deposit) } pub fn call_with_state( @@ -1467,13 +1536,13 @@ impl Backend { (fee_token, nonce_key, valid_before, valid_after) }); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state } = if let Some((fee_token, nonce_key, valid_before, valid_after)) = tempo_overrides { use tempo_primitives::transaction::Call; - let base = tx_env.base; + let base = tx_env; let mut tempo_tx = TempoTxEnv::from(base.clone()); tempo_tx.fee_token = fee_token; @@ -1495,7 +1564,13 @@ impl Backend { } self.transact_tempo_with_inspector_ref(state, &evm_env, &mut inspector, tempo_tx)? } else { - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)? + self.transact_with_inspector_ref( + state, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )? }; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -1518,9 +1593,9 @@ impl Backend { let mut inspector = AccessListInspector::new(request.access_list.clone().unwrap_or_default()); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)?; + self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env, op_deposit)?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); let access_list = inspector.access_list(); Ok((exit_reason, out, gas_used, access_list)) @@ -2912,7 +2987,7 @@ where TracingInspectorConfig::from_geth_call_config(&call_config), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let ResultAndState { result, state: _ } = self .transact_with_inspector_ref( @@ -2920,6 +2995,7 @@ where &evm_env, &mut inspector, tx_env, + op_deposit, )?; inspector.print_logs(); @@ -2945,13 +3021,14 @@ where ), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env, + op_deposit, )?; Ok(inspector @@ -2973,22 +3050,22 @@ where } #[cfg(feature = "js-tracer")] GethDebugTracerType::JsTracer(code) => { - use alloy_evm::IntoTxEnv; let config = tracer_config.into_json(); let mut inspector = revm_inspectors::tracing::js::JsInspector::new(code, config) .map_err(|err| BlockchainError::Message(err.to_string()))?; - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block.clone()); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env.clone(), + op_deposit, )?; let res = inspector - .json_result(result, &OpTx(tx_env).into_tx_env(), &block, &cache_db) + .json_result(result, &tx_env, &block, &cache_db) .map_err(|err| BlockchainError::Message(err.to_string()))?; Ok(GethTrace::JS(res)) @@ -3001,9 +3078,14 @@ where .build_inspector() .with_tracing_config(TracingInspectorConfig::from_geth_config(&config)); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block); - let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); + let ResultAndState { result, state: _ } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -3187,10 +3269,7 @@ where f: F, ) -> Result where - for<'a> I: Inspector>>>> - + Inspector>>>> - + Inspector>>>> - + 'a, + for<'a> I: BackendInspector>>> + 'a, for<'a> F: FnOnce(ResultAndState, CacheDB>, I, TxEnv, EvmEnv) -> T, { @@ -3965,7 +4044,7 @@ impl Backend { )? .or_zero_fees(); - let (mut evm_env, tx_env) = self.build_call_env( + let (mut evm_env, tx_env, op_deposit) = self.build_call_env( WithOtherFields::new(request.clone()), fee_details, block_env.clone(), @@ -3986,8 +4065,13 @@ impl Backend { inspector = inspector.with_transfers(); } trace!(target: "backend", env=?evm_env, spec=?evm_env.spec_id(),"simulate evm env"); - let ResultAndState { result, state } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let ResultAndState { result, state } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; trace!(target: "backend", ?result, ?request, "simulate call"); inspector.print_logs(); @@ -4359,7 +4443,10 @@ where } // Nonce validation — skip for deposits (L1→L2) and Tempo txs (2D nonce system) + #[cfg(feature = "optimism")] let is_deposit_tx = pending.transaction.as_ref().is_deposit(); + #[cfg(not(feature = "optimism"))] + let is_deposit_tx = false; let is_tempo_tx = pending.transaction.as_ref().is_tempo(); let nonce = tx.nonce(); if nonce < account.nonce && !is_deposit_tx && !is_tempo_tx { @@ -4475,6 +4562,7 @@ where ); let value = tx.value(); match tx.as_ref() { + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(deposit_tx) => { // Deposit transactions // https://specs.optimism.io/protocol/deposits.html#execution @@ -4538,6 +4626,7 @@ pub fn transaction_build( info: Option, base_fee: Option, ) -> AnyRpcTransaction { + #[cfg(feature = "optimism")] if let FoundryTxEnvelope::Deposit(deposit_tx) = eth_transaction.as_ref() { let dep_tx = deposit_tx; diff --git a/crates/anvil/src/eth/backend/mem/optimism.rs b/crates/anvil/src/eth/backend/mem/optimism.rs new file mode 100644 index 0000000000000..e9a94cc254fb7 --- /dev/null +++ b/crates/anvil/src/eth/backend/mem/optimism.rs @@ -0,0 +1,61 @@ +//! Optimism-specific transact helpers for the in-memory backend. + +use super::Backend; +use crate::eth::error::BlockchainError; +use alloy_evm::{Database, Evm, EvmEnv, EvmFactory}; +use alloy_network::Network; +use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; +use foundry_evm::backend::DatabaseError; +use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +use revm::{ + DatabaseRef, Inspector, + context::{ + TxEnv, + result::{EVMError, HaltReason, ResultAndState}, + }, + database_interface::WrapDatabaseRef, +}; + +impl Backend { + /// Optimism path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an OP EVM, injects precompiles, transacts, and maps the + /// OP-specific halt reason back to the shared [`HaltReason`]. + pub(super) fn transact_op_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: OpTransaction, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let op_env = EvmEnv::new( + evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), + evm_env.block_env.clone(), + ); + let mut evm = OpEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + op_env, + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { + EVMError::Database(db) => EVMError::Database(db), + EVMError::Header(h) => EVMError::Header(h), + EVMError::Custom(s) => EVMError::Custom(s), + EVMError::CustomAny(err) => EVMError::CustomAny(err), + EVMError::Transaction(t) => EVMError::Transaction(t), + })?; + Ok(ResultAndState { + result: result.result.map_haltreason(|h| match h { + OpHaltReason::Base(eth) => eth, + _ => HaltReason::PrecompileError, + }), + state: result.state, + }) + } +} diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error/mod.rs similarity index 91% rename from crates/anvil/src/eth/error.rs rename to crates/anvil/src/eth/error/mod.rs index 3b2ada43d7731..482df681b184a 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error/mod.rs @@ -12,7 +12,6 @@ use anvil_rpc::{ response::ResponseResult, }; use foundry_evm::{backend::DatabaseError, decode::RevertDecoder}; -use op_revm::OpTransactionError; use revm::{ context_interface::result::{EVMError, InvalidHeader, InvalidTransaction}, interpreter::InstructionResult, @@ -21,6 +20,9 @@ use serde::Serialize; use tempo_revm::TempoInvalidTransaction; use tokio::time::Duration; +#[cfg(feature = "optimism")] +mod optimism; + pub(crate) type Result = std::result::Result; #[derive(Debug, thiserror::Error)] @@ -163,51 +165,6 @@ where } } -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => match err { - OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), - OpTransactionError::DepositSystemTxPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::HaltedDepositPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), - }, - EVMError::Header(err) => match err { - InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, - InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, - }, - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => { - let op_err: OpTransactionError = err.0; - EVMError::::Transaction(op_err).into() - } - EVMError::Header(err) => EVMError::::Header(err).into(), - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - impl From> for BlockchainError where T: Into, @@ -451,16 +408,6 @@ impl From for InvalidTransactionError { } } -impl From for InvalidTransactionError { - fn from(value: OpTransactionError) -> Self { - match value { - OpTransactionError::Base(err) => err.into(), - OpTransactionError::DepositSystemTxPostRegolith - | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, - OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, - } - } -} /// Helper trait to easily convert results to rpc results pub(crate) trait ToRpcResponseResult { fn to_rpc_result(self) -> ResponseResult; @@ -577,9 +524,13 @@ impl ToRpcResponseResult for Result { err => RpcError::internal_error_with(format!("Fork Error: {err:?}")), } } - err @ BlockchainError::EvmError(_) => { - RpcError::internal_error_with(err.to_string()) - } + err @ BlockchainError::EvmError(_) => RpcError { + // VM halts are execution failures, not JSON-RPC server faults. REVERT has a + // dedicated code/data path above; other halts, such as invalid opcode, do not. + code: ErrorCode::TransactionRejected, + message: err.to_string().into(), + data: None, + }, err @ BlockchainError::EvmOverrideError(_) => { RpcError::invalid_params(err.to_string()) } diff --git a/crates/anvil/src/eth/error/optimism.rs b/crates/anvil/src/eth/error/optimism.rs new file mode 100644 index 0000000000000..1207fde30a72a --- /dev/null +++ b/crates/anvil/src/eth/error/optimism.rs @@ -0,0 +1,62 @@ +//! Optimism-specific error conversions for [`BlockchainError`] and +//! [`InvalidTransactionError`]. + +use super::{BlockchainError, InvalidTransactionError}; +use op_revm::OpTransactionError; +use revm::context_interface::result::{EVMError, InvalidHeader}; + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => match err { + OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), + OpTransactionError::DepositSystemTxPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::HaltedDepositPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), + }, + EVMError::Header(err) => match err { + InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, + InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, + }, + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => { + let op_err: OpTransactionError = err.0; + EVMError::::Transaction(op_err).into() + } + EVMError::Header(err) => EVMError::::Header(err).into(), + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From for InvalidTransactionError { + fn from(value: OpTransactionError) -> Self { + match value { + OpTransactionError::Base(err) => err.into(), + OpTransactionError::DepositSystemTxPostRegolith + | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, + OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, + } + } +} diff --git a/crates/anvil/src/eth/otterscan/api.rs b/crates/anvil/src/eth/otterscan/api.rs index 4da86933020ae..2ccf65ba721f5 100644 --- a/crates/anvil/src/eth/otterscan/api.rs +++ b/crates/anvil/src/eth/otterscan/api.rs @@ -155,9 +155,12 @@ impl EthApi { let best = self.backend.best_number(); // we go from given block (defaulting to best) down to first block - // considering only post-fork + // considering only post-fork (or post-genesis in non-fork mode) let from = if block_number == 0 { best } else { block_number - 1 }; - let to = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + let to = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let first_page = from >= best; let mut last_page = false; @@ -198,8 +201,11 @@ impl EthApi { node_info!("ots_searchTransactionsAfter"); let best = self.backend.best_number(); - // we go from the first post-fork block, up to the tip - let first_block = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + // we go from the first post-fork (or post-genesis) block, up to the tip + let first_block = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let from = if block_number == 0 { first_block } else { block_number + 1 }; let to = best; @@ -248,7 +254,10 @@ impl EthApi { ) -> Result> { node_info!("ots_getTransactionBySenderAndNonce"); - let from = self.get_fork().map(|f| f.block_number() + 1).unwrap_or_default(); + let from = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let to = self.backend.best_number(); for n in (from..=to).rev() { diff --git a/crates/anvil/src/eth/pool/transactions.rs b/crates/anvil/src/eth/pool/transactions.rs index df65822e1eab3..5280987483dd7 100644 --- a/crates/anvil/src/eth/pool/transactions.rs +++ b/crates/anvil/src/eth/pool/transactions.rs @@ -123,10 +123,10 @@ impl PoolTransaction { impl fmt::Debug for PoolTransaction { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "Transaction {{ ")?; - write!(fmt, "hash: {:?}, ", &self.pending_transaction.hash())?; + write!(fmt, "hash: {:?}, ", self.pending_transaction.hash())?; write!(fmt, "requires: [{}], ", hex_fmt_many(self.requires.iter()))?; write!(fmt, "provides: [{}], ", hex_fmt_many(self.provides.iter()))?; - write!(fmt, "raw tx: {:?}", &self.pending_transaction)?; + write!(fmt, "raw tx: {:?}", self.pending_transaction)?; write!(fmt, "}}")?; Ok(()) } diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index 3fdf6192c4537..d1736c3093056 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -1,5 +1,7 @@ use crate::eth::error::BlockchainError; -use alloy_consensus::{Sealed, SignableTransaction}; +#[cfg(feature = "optimism")] +use alloy_consensus::Sealed; +use alloy_consensus::SignableTransaction; use alloy_dyn_abi::TypedData; use alloy_network::{Network, TxSignerSync}; use alloy_primitives::{Address, B256, Signature, map::AddressHashMap}; @@ -130,9 +132,11 @@ impl Signer for DevSigner { let sig = signer.sign_transaction_sync(&mut t)?; FoundryTxEnvelope::Eip4844(t.into_signed(sig)) } + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => { unreachable!("op deposit txs should not be signed") } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be signed") } @@ -156,7 +160,9 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope { FoundryTypedTx::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)), FoundryTypedTx::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)), FoundryTypedTx::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be impersonated") } diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm/mod.rs similarity index 64% rename from crates/anvil/src/evm.rs rename to crates/anvil/src/evm/mod.rs index d1b40ba56ebbd..85e43d371b097 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm/mod.rs @@ -2,6 +2,9 @@ use alloy_evm::precompiles::DynPrecompile; use alloy_primitives::Address; use std::fmt::Debug; +#[cfg(feature = "optimism")] +mod optimism; + /// Object-safe trait that enables injecting extra precompiles when using /// `anvil` as a library. pub trait PrecompileFactory: Send + Sync + Unpin + Debug { @@ -15,14 +18,12 @@ mod tests { use crate::PrecompileFactory; use alloy_evm::{ - EthEvm, Evm, EvmEnv, EvmFactory, + EthEvm, Evm, eth::EthEvmContext, precompiles::{DynPrecompile, PrecompilesMap}, }; - use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; - use alloy_primitives::{Address, Bytes, TxKind, U256, address}; + use alloy_primitives::{Address, Bytes, TxKind, address}; use itertools::Itertools; - use op_revm::{OpSpecId, OpTransaction}; use revm::{ Journal, context::{BlockEnv, CfgEnv, Evm as RevmEvm, JournalTr, LocalContext, TxEnv}, @@ -35,20 +36,19 @@ mod tests { }; // A precompile activated in the `Prague` spec (BLS12-381 G2 map). - const ETH_PRAGUE_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000011"); + pub(super) const ETH_PRAGUE_PRECOMPILE: Address = + address!("0x0000000000000000000000000000000000000011"); // A precompile activated in the `Osaka` spec (EIP-7951). const ETH_OSAKA_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A precompile activated in the `Isthmus` spec. - const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A custom precompile address and payload for testing. - const PRECOMPILE_ADDR: Address = address!("0x0000000000000000000000000000000000000071"); - const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; + pub(super) const PRECOMPILE_ADDR: Address = + address!("0x0000000000000000000000000000000000000071"); + pub(super) const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; #[derive(Debug)] - struct CustomPrecompileFactory; + pub(super) struct CustomPrecompileFactory; impl PrecompileFactory for CustomPrecompileFactory { fn precompiles(&self) -> Vec<(Address, DynPrecompile)> { @@ -109,34 +109,6 @@ mod tests { (tx_env, eth_evm) } - /// Creates a new OP EVM instance. - fn create_op_evm( - _spec: SpecId, - op_spec: OpSpecId, - ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { - let tx = OpTx(OpTransaction:: { - base: TxEnv { - kind: TxKind::Call(PRECOMPILE_ADDR), - data: PAYLOAD.into(), - ..Default::default() - }, - ..Default::default() - }); - - let mut evm = OpEvmFactory::::default().create_evm_with_inspector( - EmptyDB::default(), - EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), - NoOpInspector, - ); - - if op_spec == OpSpecId::ISTHMUS { - evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); - evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); - } - - (tx, evm) - } - #[test] fn build_eth_evm_with_extra_precompiles_osaka_spec() { let (tx_env, mut evm) = create_eth_evm(SpecId::OSAKA); @@ -187,38 +159,4 @@ mod tests { assert!(result.result.is_success()); assert_eq!(result.result.output(), Some(&PAYLOAD.into())); } - - #[test] - fn build_op_evm_with_extra_precompiles_isthmus_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); - - assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } - - #[test] - fn build_op_evm_with_extra_precompiles_bedrock_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); - - assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } } diff --git a/crates/anvil/src/evm/optimism.rs b/crates/anvil/src/evm/optimism.rs new file mode 100644 index 0000000000000..526375fec31ea --- /dev/null +++ b/crates/anvil/src/evm/optimism.rs @@ -0,0 +1,87 @@ +//! Optimism-specific EVM helpers. + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use super::super::tests::{ + CustomPrecompileFactory, ETH_PRAGUE_PRECOMPILE, PAYLOAD, PRECOMPILE_ADDR, + }; + use crate::PrecompileFactory; + use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; + use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; + use alloy_primitives::{Address, TxKind, U256, address}; + use itertools::Itertools; + use op_revm::{OpSpecId, OpTransaction}; + use revm::{ + context::{BlockEnv, CfgEnv, TxEnv}, + database::{EmptyDB, EmptyDBTyped}, + inspector::NoOpInspector, + primitives::hardfork::SpecId, + }; + + // A precompile activated in the `Isthmus` spec. + const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); + + /// Creates a new OP EVM instance. + fn create_op_evm( + _spec: SpecId, + op_spec: OpSpecId, + ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { + let tx = OpTx(OpTransaction:: { + base: TxEnv { + kind: TxKind::Call(PRECOMPILE_ADDR), + data: PAYLOAD.into(), + ..Default::default() + }, + ..Default::default() + }); + + let mut evm = OpEvmFactory::::default().create_evm_with_inspector( + EmptyDB::default(), + EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), + NoOpInspector, + ); + + if op_spec == OpSpecId::ISTHMUS { + evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); + evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); + } + + (tx, evm) + } + + #[test] + fn build_op_evm_with_extra_precompiles_isthmus_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); + + assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } + + #[test] + fn build_op_evm_with_extra_precompiles_bedrock_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); + + assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } +} diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index 26e587e8b5123..a661e9c765b26 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::{ error::{NodeError, NodeResult}, eth::{ diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index c4879e36d5240..216476c69eeb8 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -11,6 +11,7 @@ mod gas; mod genesis; mod ipc; mod logs; +#[cfg(feature = "optimism")] mod optimism; mod otterscan; mod proof; diff --git a/crates/anvil/tests/it/revert.rs b/crates/anvil/tests/it/revert.rs index ab85fc89abf80..a15454fa5593e 100644 --- a/crates/anvil/tests/it/revert.rs +++ b/crates/anvil/tests/it/revert.rs @@ -28,6 +28,38 @@ async fn test_deploy_reverting() { assert!(!receipt.inner.inner.status()); } +#[tokio::test(flavor = "multi_thread")] +async fn test_invalid_opcode_rpc_error_code() { + let (_api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + let sender = handle.dev_accounts().next().unwrap(); + + // Deploy a contract whose runtime bytecode is the invalid opcode 0xfe. + let code = bytes!("60fe60005360016000f3"); + let tx = TransactionRequest::default().from(sender).with_deploy_code(code); + let receipt = provider + .send_transaction(WithOtherFields::new(tx)) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + let contract = receipt.contract_address.unwrap(); + + for (method, params) in [ + ("eth_call", serde_json::json!([{ "from": sender, "to": contract }, "latest"])), + ("eth_estimateGas", serde_json::json!([{ "from": sender, "to": contract }])), + ] { + let error = rpc_error(&handle.http_endpoint(), method, params).await; + assert_eq!(error["code"], serde_json::json!(-32003), "{error}"); + assert!(error.get("data").is_none(), "{error}"); + + let message = error["message"].as_str().unwrap(); + assert!(message.contains("EVM error InvalidFEOpcode"), "{error}"); + assert!(!message.contains("execution reverted"), "{error}"); + } +} + #[tokio::test(flavor = "multi_thread")] async fn test_revert_messages() { sol!( @@ -124,3 +156,21 @@ async fn test_solc_revert_custom_errors() { let s = err.to_string(); assert!(s.contains("execution reverted"), "{s:?}"); } + +async fn rpc_error(endpoint: &str, method: &str, params: serde_json::Value) -> serde_json::Value { + let response = reqwest::Client::new() + .post(endpoint) + .json(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + })) + .send() + .await + .unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + + let body = response.json::().await.unwrap(); + body.get("error").cloned().unwrap_or_else(|| panic!("expected JSON-RPC error, got {body}")) +} diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index dc5be77a244da..03be26a631a11 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -61,9 +61,9 @@ tempo-contracts.workspace = true tempo-primitives.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-flz.workspace = true -op-alloy-network.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-flz = { workspace = true, optional = true } +op-alloy-network = { workspace = true, optional = true } chrono.workspace = true eyre.workspace = true @@ -100,7 +100,7 @@ anvil.workspace = true foundry-test-utils.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -109,3 +109,12 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "dep:op-alloy-flz", + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "foundry-common/optimism", + "foundry-evm-networks/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 6dc2ed6acf430..23fda25c40faa 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -29,6 +29,7 @@ use foundry_common::{ shell, stdin, }; use foundry_evm_networks::NetworkVariant; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; use std::time::Instant; use tempo_alloy::TempoNetwork; @@ -351,6 +352,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let output = if raw || fields.contains(&"raw".into()) { match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -569,6 +571,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let is_raw = raw || field.as_ref().is_some_and(|f| f == "raw"); let output = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -791,6 +794,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::DecodeTransaction { tx, network } => { let tx = stdin::unwrap_line(tx)?; let decoded_tx = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { SimpleCast::decode_raw_transaction::(&tx)? } @@ -809,6 +813,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::Erc20Token { command } => command.run().await?, CastSubcommand::Tip20Token { command } => command.run().await?, CastSubcommand::Keychain { command } => command.run().await?, + CastSubcommand::Tempo { command } => command.run().await?, + CastSubcommand::VirtualAddress { command } => command.run().await?, + #[cfg(feature = "optimism")] CastSubcommand::DAEstimate(cmd) => { cmd.run().await?; } diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index ae5c9668f522f..e25798086d512 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy_consensus::SignableTransaction; use alloy_eips::eip2718::Encodable2718; -use alloy_network::{EthereumWallet, NetworkTransactionBuilder}; +use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder}; use alloy_primitives::Address; use alloy_provider::Provider; use alloy_signer::Signer; @@ -17,7 +17,7 @@ use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use tempo_alloy::TempoNetwork; @@ -53,7 +53,7 @@ pub struct BatchMakeTxArgs { impl BatchMakeTxArgs { pub async fn run(self) -> Result<()> { - let Self { calls, tx, eth, raw_unsigned, ethsign } = self; + let Self { calls, mut tx, eth, raw_unsigned, ethsign } = self; let has_nonce = tx.nonce.is_some(); if calls.is_empty() { @@ -63,6 +63,10 @@ impl BatchMakeTxArgs { let config = eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + // Resolve signer to detect keychain mode let (signer, tempo_access_key) = eth.wallet.maybe_signer().await?; @@ -92,14 +96,14 @@ impl BatchMakeTxArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as raw unsigned. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Set calls on the transaction builder.tx.calls = tempo_calls; @@ -117,6 +121,7 @@ impl BatchMakeTxArgs { let from = eth.wallet.from.unwrap_or(Address::ZERO); let (tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -125,6 +130,7 @@ impl BatchMakeTxArgs { if ethsign { let (tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; return Ok(()); @@ -137,7 +143,9 @@ impl BatchMakeTxArgs { }; let signed_tx = if let Some(ref access_key) = tempo_access_key { - let (tx, _) = tx_builder.build(access_key.wallet_address).await?; + let (tx, _) = + tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = tx .sign_with_access_key( &provider, @@ -151,6 +159,7 @@ impl BatchMakeTxArgs { } else { tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?; let (tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let envelope = tx.build(&EthereumWallet::new(signer)).await?; alloy_primitives::hex::encode(envelope.encoded_2718()) }; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index ec7254b08e2f5..33128ae897898 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -9,14 +9,14 @@ use crate::{ cmd::send::{cast_send, cast_send_with_access_key}, tx::{self, CastTxBuilder, SendTxOpts}, }; -use alloy_network::EthereumWallet; +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::TransactionOpts, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::provider::ProviderBuilder; use std::time::Duration; @@ -50,7 +50,7 @@ pub struct BatchSendArgs { impl BatchSendArgs { pub async fn run(self) -> Result<()> { - let Self { calls, send_tx, tx, unlocked } = self; + let Self { calls, send_tx, mut tx, unlocked } = self; if calls.is_empty() { return Err(eyre!("No calls specified. Use --call to specify at least one call.")); @@ -59,6 +59,10 @@ impl BatchSendArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -93,14 +97,14 @@ impl BatchSendArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as unlocked. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Access the inner tx and set calls builder.tx.calls = tempo_calls; @@ -116,6 +120,7 @@ impl BatchSendArgs { if unlocked { let (tx, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; cast_send( provider, tx, @@ -132,7 +137,12 @@ impl BatchSendArgs { }; if let Some(ref access_key) = tempo_access_key { - let (tx_request, _) = builder.build(access_key.wallet_address).await?; + let (tx_request, _) = + builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; cast_send_with_access_key( &provider, tx_request, @@ -146,6 +156,10 @@ impl BatchSendArgs { } else { tx::validate_from_address(send_tx.eth.wallet.from, Signer::address(&signer))?; let (tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() .wallet(wallet) diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 3637f166a53df..63ea17f707e03 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -33,10 +33,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork}, }, executors::TracingExecutor, opts::EvmOpts, @@ -222,18 +224,19 @@ impl CallArgs { return self.run_curl().await; } if self.tx.tempo.is_tempo() { - self.run_with_network::().await - } else { - let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); - let mut evm_opts = figment.extract::()?; - evm_opts.infer_network_from_fork().await; + return self.run_with_network::().await; + } - if evm_opts.networks.is_optimism() { - self.run_with_network::().await - } else { - self.run_with_network::().await - } + let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); + let mut evm_opts = figment.extract::()?; + evm_opts.infer_network_from_fork().await; + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_network::().await; } + + self.run_with_network::().await } pub async fn run_with_network(self) -> Result<()> diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index 8b7d80786dfad..897a01c39202a 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -1,9 +1,12 @@ use alloy_ens::NameOrAddress; -use alloy_network::EthereumWallet; +use std::time::Duration; + +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_primitives::{Address, U256, hex, keccak256}; -use alloy_provider::ProviderBuilder as AlloyProviderBuilder; +use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use alloy_sol_types::SolCall; +use alloy_transport::TransportError; use chrono::DateTime; use clap::Parser; use eyre::Result; @@ -12,11 +15,16 @@ use foundry_cli::{ utils::LoadConfig, }; use foundry_common::{ + FoundryTransactionBuilder, provider::ProviderBuilder, - shell, - tempo::{self, KeyType, KeysFile, WalletType, read_tempo_keys_file, tempo_keys_path}, + sh_warn, shell, + tempo::{ + self, KeyType, KeysFile, TEMPO_BROWSER_GAS_BUFFER, WalletType, read_tempo_keys_file, + tempo_keys_path, + }, }; use foundry_evm::hardfork::TempoHardfork; +use serde::Deserialize; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_contracts::precompiles::{ ACCOUNT_KEYCHAIN_ADDRESS, IAccountKeychain, @@ -24,13 +32,16 @@ use tempo_contracts::precompiles::{ CallScope, KeyInfo, KeyRestrictions, LegacyTokenLimit, SelectorRule, SignatureType, TokenLimit, }, + ITIP20, PATH_USD_ADDRESS, account_keychain::{authorizeKeyCall, legacyAuthorizeKeyCall}, }; use yansi::Paint; +use foundry_cli::utils::{maybe_print_resolved_lane, resolve_lane}; + use crate::{ - cmd::send::{cast_send, cast_send_with_access_key}, - tx::{CastTxBuilder, SendTxOpts}, + cmd::send::cast_send, + tx::{CastTxBuilder, CastTxSender, SendTxOpts}, }; /// Tempo keychain management commands. @@ -62,6 +73,19 @@ pub enum KeychainSubcommand { rpc: RpcOpts, }, + /// Inspect an access key policy using the local key registry and on-chain state. + Inspect { + /// The key address to inspect. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + #[command(flatten)] + rpc: RpcOpts, + }, + /// Authorize a new key on-chain via the AccountKeychain precompile. #[command(visible_alias = "auth")] Authorize { @@ -183,8 +207,92 @@ pub enum KeychainSubcommand { #[command(flatten)] send_tx: SendTxOpts, }, + + /// Read or edit TIP-1011 access-key permissions. + Policy { + #[command(subcommand)] + command: KeychainPolicySubcommand, + }, +} + +/// Higher-level access-key policy editing commands. +#[derive(Debug, Parser)] +pub enum KeychainPolicySubcommand { + /// Add or widen an allowed call rule for a target contract. + AddCall { + /// The key address to update. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + /// Target contract address. + #[arg(long)] + target: Address, + + /// Function selector, full signature, or known TIP-20 shorthand. + #[arg(long, value_parser = parse_selector_arg)] + selector: SelectorArg, + + /// Optional recipient/spender restrictions for selector calls. + #[arg(long, value_delimiter = ',')] + recipients: Vec
, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Update a token spending limit amount for a key. + SetLimit { + /// The key address to update. + key_address: Address, + + /// Token address, numeric TIP-20 token id, or PathUSD. + #[arg(long, value_parser = parse_policy_token)] + token: Address, + + /// New raw token-denominated limit. + #[arg(long)] + amount: U256, + + /// Limit period such as 7d, 24h, or 3600s. + /// + /// The current AccountKeychain update entrypoint cannot change periods, so non-zero + /// values are rejected. + #[arg(long, value_parser = parse_period)] + period: Option, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Remove all allowed-call rules for a target contract. + RemoveTarget { + /// The key address to update. + key_address: Address, + + /// Target contract address to remove. + #[arg(long)] + target: Address, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, } +#[derive(Debug, Clone, Copy)] +pub struct SelectorArg([u8; 4]); + fn parse_signature_type(s: &str) -> Result { match s.to_lowercase().as_str() { "secp256k1" => Ok(SignatureType::Secp256k1), @@ -203,6 +311,15 @@ const fn signature_type_name(t: &SignatureType) -> &'static str { } } +const fn signature_type_label(t: &SignatureType) -> &'static str { + match t { + SignatureType::Secp256k1 => "Secp256k1", + SignatureType::P256 => "P256", + SignatureType::WebAuthn => "WebAuthn", + _ => "unknown", + } +} + const fn key_type_name(t: &KeyType) -> &'static str { match t { KeyType::Secp256k1 => "secp256k1", @@ -211,6 +328,14 @@ const fn key_type_name(t: &KeyType) -> &'static str { } } +const fn key_type_label(t: &KeyType) -> &'static str { + match t { + KeyType::Secp256k1 => "Secp256k1", + KeyType::P256 => "P256", + KeyType::WebAuthn => "WebAuthn", + } +} + const fn wallet_type_name(t: &WalletType) -> &'static str { match t { WalletType::Local => "local", @@ -332,6 +457,48 @@ fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> { } } +fn parse_selector_arg(s: &str) -> Result { + parse_selector_bytes(s).map(SelectorArg) +} + +fn parse_policy_token(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "pathusd" | "path_usd" | "path-usd" | "usd" => Ok(PATH_USD_ADDRESS), + _ => foundry_cli::utils::parse_fee_token_address(s).map_err(|e| e.to_string()), + } +} + +fn parse_period(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err("period cannot be empty".to_string()); + } + + let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()); + if split == 0 { + return Err(format!( + "invalid period '{s}': expected a number followed by s, m, h, d, or w" + )); + } + + let value: u64 = + s[..split].parse().map_err(|e| format!("invalid period value '{}': {e}", &s[..split]))?; + let multiplier = match &s[split..].to_ascii_lowercase()[..] { + "" | "s" => 1, + "m" => 60, + "h" => 60 * 60, + "d" => 24 * 60 * 60, + "w" => 7 * 24 * 60 * 60, + unit => { + return Err(format!( + "invalid period unit '{unit}' in '{s}' (expected s, m, h, d, or w)" + )); + } + }; + + value.checked_mul(multiplier).ok_or_else(|| format!("period '{s}' is too large")) +} + /// Represents a single scope entry in JSON format for `--scopes`. #[derive(serde::Deserialize)] struct JsonCallScope { @@ -402,6 +569,9 @@ impl KeychainSubcommand { Self::Check { wallet_address, key_address, rpc } => { run_check(wallet_address, key_address, rpc).await } + Self::Inspect { key_address, root_account, rpc } => { + run_inspect(key_address, root_account, rpc).await + } Self::Authorize { key_address, key_type, @@ -443,6 +613,40 @@ impl KeychainSubcommand { Self::RemoveScope { key_address, target, tx, send_tx } => { run_remove_scope(key_address, target, tx, send_tx).await } + Self::Policy { command } => command.run().await, + } + } +} + +impl KeychainPolicySubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::AddCall { + key_address, + root_account, + target, + selector, + recipients, + tx, + send_tx, + } => { + run_policy_add_call( + key_address, + root_account, + target, + selector.0, + recipients, + tx, + send_tx, + ) + .await + } + Self::SetLimit { key_address, token, amount, period, tx, send_tx } => { + run_policy_set_limit(key_address, token, amount, period, tx, send_tx).await + } + Self::RemoveTarget { key_address, target, tx, send_tx } => { + run_remove_scope(key_address, target, tx, send_tx).await + } } } } @@ -500,6 +704,143 @@ fn run_show(wallet_address: Address) -> Result<()> { Ok(()) } +#[derive(Debug, Clone)] +struct LocalLimitMetadata { + token: Address, + amount: String, +} + +#[derive(Debug, Clone)] +struct KeyMetadata { + root_account: Address, + key_type: Option, + limits: Vec, +} + +#[derive(Debug, Clone)] +struct InspectedLimit { + token: Address, + configured_amount: Option, + remaining: U256, + period_end: Option, +} + +#[derive(Debug, Clone)] +enum AllowedCallsView { + Unsupported, + Unrestricted, + Scoped(Vec), +} + +/// `cast keychain inspect ` — inspect on-chain key policy. +async fn run_inspect( + key_address: Address, + root_account: Option
, + rpc: RpcOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let info: KeyInfo = provider.get_keychain_key(metadata.root_account, key_address).await?; + let provisioned = info.keyId != Address::ZERO; + let is_t3 = is_tempo_hardfork_active(&provider, TempoHardfork::T3).await?; + + let mut limits = Vec::new(); + if info.enforceLimits { + for local_limit in &metadata.limits { + let (remaining, period_end) = if is_t3 { + let limit = provider + .get_keychain_remaining_limit_with_period( + metadata.root_account, + key_address, + local_limit.token, + ) + .await?; + (limit.remaining, Some(limit.periodEnd)) + } else { + let remaining = provider + .account_keychain() + .getRemainingLimit(metadata.root_account, key_address, local_limit.token) + .call() + .await?; + (remaining, None) + }; + + limits.push(InspectedLimit { + token: local_limit.token, + configured_amount: Some(local_limit.amount.clone()), + remaining, + period_end, + }); + } + } + + let allowed_calls = if is_t3 { + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + if allowed.isScoped { + AllowedCallsView::Scoped(allowed.scopes) + } else { + AllowedCallsView::Unrestricted + } + } else { + AllowedCallsView::Unsupported + }; + + if shell::is_json() { + let key_type = if provisioned { + signature_type_name(&info.signatureType).to_string() + } else { + metadata + .key_type + .map(|key_type| key_type_name(&key_type).to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }; + let json = serde_json::json!({ + "root_account": metadata.root_account.to_string(), + "key_id": key_address.to_string(), + "provisioned": provisioned, + "type": key_type, + "expiry": provisioned.then_some(info.expiry), + "expiry_human": provisioned.then(|| format_expiry_for_inspect(info.expiry)), + "enforce_limits": info.enforceLimits, + "is_revoked": info.isRevoked, + "limits": limits.iter().map(inspected_limit_to_json).collect::>(), + "allowed_calls": allowed_calls_to_json(&allowed_calls), + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + return Ok(()); + } + + let key_type = if provisioned { + signature_type_label(&info.signatureType) + } else { + metadata.key_type.map(|key_type| key_type_label(&key_type)).unwrap_or("unknown") + }; + + sh_println!("Root account: {}", metadata.root_account)?; + sh_println!("Key id: {key_address}")?; + sh_println!("Type: {key_type}")?; + + if info.isRevoked { + sh_println!("Status: revoked")?; + } else if !provisioned { + sh_println!("Status: not provisioned")?; + } else { + sh_println!("Status: active")?; + sh_println!("Expiry: {}", format_expiry_for_inspect(info.expiry))?; + } + + print_inspected_limits(info.enforceLimits, &limits)?; + print_allowed_calls(&allowed_calls)?; + + Ok(()) +} + /// `cast keychain check` / `cast keychain info` — query on-chain key status. async fn run_check(wallet_address: Address, key_address: Address, rpc: RpcOpts) -> Result<()> { let config = rpc.load_config()?; @@ -584,7 +925,7 @@ async fn run_authorize( let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let calldata = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let calldata = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { // T3+ authorizeKey(address,SignatureType,KeyRestrictions) let restrictions = KeyRestrictions { expiry, @@ -634,7 +975,7 @@ async fn run_remaining_limit( let config = rpc.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let remaining: U256 = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let remaining: U256 = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { provider.get_keychain_remaining_limit(wallet_address, key_address, token).await? } else { // Pre-T3: use the legacy getRemainingLimit(address,address,address) @@ -646,7 +987,7 @@ async fn run_remaining_limit( }; if shell::is_json() { - sh_println!("{}", serde_json::to_string(&remaining.to_string())?)?; + sh_println!("{}", serde_json::json!({ "remaining": remaining.to_string() }))?; } else { sh_println!("{remaining}")?; } @@ -695,6 +1036,88 @@ async fn run_remove_scope( send_keychain_tx(calldata, tx_opts, &send_tx).await } +/// `cast keychain policy add-call` — merge a selector rule into a target scope. +async fn run_policy_add_call( + key_address: Address, + root_account: Option
, + target: Address, + selector: [u8; 4], + recipients: Vec
, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = send_tx.eth.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + if !is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { + eyre::bail!("allowed-call policy editing requires the Tempo T3 hardfork"); + } + + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + + let new_rule = SelectorRule { selector: selector.into(), recipients }; + let existing_target = allowed + .isScoped + .then(|| allowed.scopes.into_iter().find(|scope| scope.target == target)) + .flatten(); + + let (target_scope, changed) = match existing_target { + Some(mut scope) => { + if scope.selectorRules.is_empty() { + sh_warn!( + "Allowed calls for {} already allow any selector; leaving wildcard scope unchanged", + address_label_with_address(target) + )?; + } + let changed = add_selector_rule_to_scope(&mut scope, new_rule); + (scope, changed) + } + None => (CallScope { target, selectorRules: vec![new_rule] }, true), + }; + + if !changed { + if shell::is_json() { + sh_println!( + "{}", + serde_json::json!({ "status": "already_present", "target": target.to_string() }) + )?; + } else { + sh_println!("Allowed call already present for {}", address_label_with_address(target))?; + } + return Ok(()); + } + + let calldata = + IAccountKeychain::setAllowedCallsCall { keyId: key_address, scopes: vec![target_scope] } + .abi_encode(); + send_keychain_tx(calldata, tx_opts, &send_tx).await +} + +/// `cast keychain policy set-limit` — update a spending limit amount. +async fn run_policy_set_limit( + key_address: Address, + token: Address, + amount: U256, + period: Option, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + if period.is_some_and(|period| period != 0) { + eyre::bail!( + "--period is not supported by the current AccountKeychain updateSpendingLimit \ + precompile; periods can only be set when authorizing a key" + ); + } + + // updateSpendingLimit authorizes against msg.sender; the root account is not part of calldata. + run_update_limit(key_address, token, amount, tx_opts, send_tx).await +} + /// Shared helper to send a keychain precompile transaction. async fn send_keychain_tx( calldata: Vec, @@ -702,16 +1125,22 @@ async fn send_keychain_tx( send_tx: &SendTxOpts, ) -> Result<()> { let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; + let tempo_sponsor = + if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? }; let config = send_tx.eth.load_config()?; let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); let provider = ProviderBuilder::::from_config(&config)?.build()?; - // Inject key_id for correct gas estimation with keychain signature overhead. - if let Some(ref ak) = tempo_access_key { - tx_opts.tempo.key_id = Some(ak.key_address); + if let Some(interval) = send_tx.poll_interval { + provider.client().set_poll_interval(Duration::from_secs(interval)); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx_opts.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx_opts.tempo, &config.root)?; + let builder = CastTxBuilder::new(&provider, tx_opts, &config) .await? .with_to(Some(NameOrAddress::Address(ACCOUNT_KEYCHAIN_ADDRESS))) @@ -719,27 +1148,70 @@ async fn send_keychain_tx( .with_code_sig_and_args(None, Some(hex::encode_prefixed(&calldata)), vec![]) .await?; - if let Some(ref ak) = tempo_access_key { - let signer = - signer.as_ref().ok_or_else(|| eyre::eyre!("signer required for access key"))?; - let (tx, _) = builder.build(ak.wallet_address).await?; - cast_send_with_access_key( - &provider, - tx, - signer, - ak, - send_tx.cast_async, - send_tx.confirmations, - timeout, - ) - .await?; + // Keychain management calls are authorized by the root account. Access keys can use their + // permissions, but cannot mutate their own key policy. + let browser = send_tx.browser.run::().await?; + + if print_sponsor_hash { + let from = if let Some(ref browser) = browser { + browser.address() + } else { + signer + .as_ref() + .ok_or_else(|| { + eyre::eyre!( + "--tempo.print-sponsor-hash requires a root account signer, such as \ + --browser, --private-key, or --keystore" + ) + })? + .address() + }; + + let (tx, _) = builder.build(from).await?; + let hash = tx + .compute_sponsor_hash(from) + .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?; + if shell::is_json() { + sh_println!("{}", serde_json::json!({ "sponsor_hash": format!("{hash:?}") }))?; + } else { + sh_println!("{hash:?}")?; + } + return Ok(()); + } + + if let Some(browser) = browser { + let chain = builder.chain(); + let (mut tx, _) = builder.build(browser.address()).await?; + if chain.is_tempo() + && let Some(gas) = tx.gas_limit() + { + tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); + } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } + + let tx_hash = browser.send_transaction_via_browser(tx).await?; + CastTxSender::new(&provider) + .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout) + .await?; + } else if tempo_access_key.is_some() { + eyre::bail!( + "keychain policy changes must be signed by the root account; the selected `--from` \ + resolved to a Tempo access key. Use `--browser` for passkey roots, or pass a root \ + account signer with `--private-key`, `--keystore`, Ledger, Trezor, AWS, GCP, or Turnkey." + ); } else { let signer = match signer { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (mut tx, _) = builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() @@ -753,6 +1225,361 @@ async fn send_keychain_tx( Ok(()) } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AnvilNodeInfo { + hard_fork: Option, + network: Option, +} + +async fn is_tempo_hardfork_active

(provider: &P, hardfork: TempoHardfork) -> Result +where + P: Provider, +{ + match provider.is_hardfork_active(hardfork).await { + Ok(active) => Ok(active), + Err(err) if is_rpc_method_not_found(&err) => { + match anvil_tempo_hardfork_active(provider, hardfork).await { + Ok(Some(active)) => Ok(active), + _ => Err(err.into()), + } + } + Err(err) => Err(err.into()), + } +} + +async fn anvil_tempo_hardfork_active

( + provider: &P, + hardfork: TempoHardfork, +) -> Result, TransportError> +where + P: Provider, +{ + let info = provider.raw_request::<_, AnvilNodeInfo>("anvil_nodeInfo".into(), ()).await?; + Ok(active_from_anvil_node_info(&info, hardfork)) +} + +fn active_from_anvil_node_info(info: &AnvilNodeInfo, hardfork: TempoHardfork) -> Option { + (info.network.as_deref() == Some("tempo")).then(|| { + info.hard_fork + .as_deref() + .and_then(|active_hardfork| active_hardfork.parse::().ok()) + .is_some_and(|active_hardfork| active_hardfork >= hardfork) + }) +} + +fn is_rpc_method_not_found(err: &TransportError) -> bool { + err.as_error_resp().is_some_and(|payload| payload.code == -32601) +} + +fn resolve_key_metadata( + key_address: Address, + root_account: Option

, +) -> Result { + let keys_file = read_tempo_keys_file(); + + if let Some(root_account) = root_account { + if let Some(keys_file) = keys_file.as_ref() + && let Some(entry) = keys_file.keys.iter().find(|entry| { + entry.wallet_address == root_account + && key_entry_effective_key(entry) == key_address + }) + { + return Ok(key_metadata_from_entry(entry)); + } + + return Ok(KeyMetadata { root_account, key_type: None, limits: Vec::new() }); + } + + let Some(keys_file) = keys_file.as_ref() else { + eyre::bail!( + "key {key_address} was not found because the local keys file could not be read at {}; pass --root-account", + tempo_keys_path_display() + ); + }; + + let matches: Vec<_> = keys_file + .keys + .iter() + .filter(|entry| key_entry_effective_key(entry) == key_address) + .collect(); + + if matches.is_empty() { + eyre::bail!( + "key {key_address} was not found in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let root_account = matches[0].wallet_address; + if matches.iter().any(|entry| entry.wallet_address != root_account) { + eyre::bail!( + "key {key_address} matches multiple root accounts in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let entry = + matches.iter().copied().find(|entry| !entry.limits.is_empty()).unwrap_or(matches[0]); + Ok(key_metadata_from_entry(entry)) +} + +fn key_entry_effective_key(entry: &tempo::KeyEntry) -> Address { + entry.key_address.unwrap_or(entry.wallet_address) +} + +fn key_metadata_from_entry(entry: &tempo::KeyEntry) -> KeyMetadata { + KeyMetadata { + root_account: entry.wallet_address, + key_type: Some(entry.key_type), + limits: entry + .limits + .iter() + .map(|limit| LocalLimitMetadata { token: limit.currency, amount: limit.limit.clone() }) + .collect(), + } +} + +fn tempo_keys_path_display() -> String { + tempo_keys_path() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "(unknown)".to_string()) +} + +fn add_selector_rule_to_scope(scope: &mut CallScope, rule: SelectorRule) -> bool { + if scope.selectorRules.is_empty() { + return false; + } + + let Some(existing_rule) = + scope.selectorRules.iter_mut().find(|existing| existing.selector == rule.selector) + else { + scope.selectorRules.push(rule); + return true; + }; + + if existing_rule.recipients.is_empty() { + return false; + } + + if rule.recipients.is_empty() { + existing_rule.recipients = Vec::new(); + return true; + } + + let mut changed = false; + for recipient in rule.recipients { + if !existing_rule.recipients.contains(&recipient) { + existing_rule.recipients.push(recipient); + changed = true; + } + } + changed +} + +fn inspected_limit_to_json(limit: &InspectedLimit) -> serde_json::Value { + serde_json::json!({ + "token": limit.token.to_string(), + "token_label": address_label(limit.token), + "configured_amount": limit.configured_amount.as_deref(), + "remaining": limit.remaining.to_string(), + "period_end": limit.period_end, + "period_end_human": limit.period_end.and_then(|period_end| { + (period_end != 0).then(|| format_period_end(period_end)) + }), + }) +} + +fn allowed_calls_to_json(allowed_calls: &AllowedCallsView) -> serde_json::Value { + match allowed_calls { + AllowedCallsView::Unsupported => serde_json::json!({ + "mode": "unsupported", + "scopes": [], + }), + AllowedCallsView::Unrestricted => serde_json::json!({ + "mode": "any", + "scopes": [], + }), + AllowedCallsView::Scoped(scopes) => serde_json::json!({ + "mode": if scopes.is_empty() { "none" } else { "scoped" }, + "scopes": scopes.iter().map(call_scope_to_json).collect::>(), + }), + } +} + +fn call_scope_to_json(scope: &CallScope) -> serde_json::Value { + serde_json::json!({ + "target": scope.target.to_string(), + "target_label": address_label(scope.target), + "selectors": scope.selectorRules.iter().map(selector_rule_to_json).collect::>(), + }) +} + +fn selector_rule_to_json(rule: &SelectorRule) -> serde_json::Value { + serde_json::json!({ + "selector": selector_hex(&rule.selector.0), + "signature": selector_signature(&rule.selector.0), + "recipients": rule.recipients.iter().map(ToString::to_string).collect::>(), + }) +} + +fn print_inspected_limits(enforce_limits: bool, limits: &[InspectedLimit]) -> Result<()> { + if !enforce_limits { + sh_println!("Limits: none")?; + return Ok(()); + } + + sh_println!("Limits:")?; + if limits.is_empty() { + sh_println!(" enforced, but no local limit metadata was found")?; + return Ok(()); + } + + for limit in limits { + let configured = limit.configured_amount.as_deref().unwrap_or("unknown"); + let period = limit + .period_end + .and_then(|period_end| { + (period_end != 0).then(|| format!(" ({})", format_period_end(period_end))) + }) + .unwrap_or_default(); + sh_println!( + " {}: {} / {} remaining{}", + address_label(limit.token), + limit.remaining, + configured, + period + )?; + } + + Ok(()) +} + +fn print_allowed_calls(allowed_calls: &AllowedCallsView) -> Result<()> { + match allowed_calls { + AllowedCallsView::Unsupported => sh_println!("Allowed calls: unsupported before T3")?, + AllowedCallsView::Unrestricted => sh_println!("Allowed calls: any")?, + AllowedCallsView::Scoped(scopes) if scopes.is_empty() => { + sh_println!("Allowed calls: none")?; + } + AllowedCallsView::Scoped(scopes) => { + sh_println!("Allowed calls:")?; + for scope in scopes { + sh_println!(" {}:", address_label_with_address(scope.target))?; + if scope.selectorRules.is_empty() { + sh_println!(" any selector")?; + continue; + } + + for rule in &scope.selectorRules { + sh_println!( + " {} -> {}", + format_selector(&rule.selector.0), + format_recipients(&rule.recipients) + )?; + } + } + } + } + + Ok(()) +} + +fn address_label(address: Address) -> String { + if address == PATH_USD_ADDRESS { "PathUSD".to_string() } else { address.to_string() } +} + +fn address_label_with_address(address: Address) -> String { + if address == PATH_USD_ADDRESS { format!("PathUSD ({address})") } else { address.to_string() } +} + +fn format_selector(selector: &[u8; 4]) -> String { + selector_signature(selector).map(str::to_string).unwrap_or_else(|| selector_hex(selector)) +} + +fn selector_signature(selector: &[u8; 4]) -> Option<&'static str> { + if selector == &ITIP20::transferCall::SELECTOR { + Some("transfer(address,uint256)") + } else if selector == &ITIP20::approveCall::SELECTOR { + Some("approve(address,uint256)") + } else if selector == &ITIP20::transferFromCall::SELECTOR { + Some("transferFrom(address,address,uint256)") + } else if selector == &ITIP20::transferWithMemoCall::SELECTOR { + Some("transferWithMemo(address,uint256,bytes32)") + } else if selector == &ITIP20::transferFromWithMemoCall::SELECTOR { + Some("transferFromWithMemo(address,address,uint256,bytes32)") + } else if selector == &ITIP20::mintCall::SELECTOR { + Some("mint(address,uint256)") + } else if selector == &ITIP20::burnCall::SELECTOR { + Some("burn(uint256)") + } else { + None + } +} + +fn selector_hex(selector: &[u8; 4]) -> String { + hex::encode_prefixed(selector) +} + +fn format_recipients(recipients: &[Address]) -> String { + if recipients.is_empty() { + return "any recipient".to_string(); + } + + let recipients = recipients.iter().map(ToString::to_string).collect::>().join(", "); + format!("recipients [{recipients}]") +} + +fn format_expiry_for_inspect(expiry: u64) -> String { + if expiry == u64::MAX { + return "never".to_string(); + } + + format!("{} ({})", format_timestamp_iso(expiry), format_relative_timestamp(expiry)) +} + +fn format_period_end(period_end: u64) -> String { + format!("period resets {}", format_relative_timestamp(period_end)) +} + +fn format_timestamp_iso(timestamp: u64) -> String { + DateTime::from_timestamp(timestamp as i64, 0) + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| timestamp.to_string()) +} + +fn format_relative_timestamp(timestamp: u64) -> String { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if timestamp == now { + "now".to_string() + } else if timestamp > now { + format!("in {}", format_duration_words(timestamp - now)) + } else { + format!("{} ago", format_duration_words(now - timestamp)) + } +} + +fn format_duration_words(seconds: u64) -> String { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + const DAY: u64 = 24 * HOUR; + + if seconds >= DAY { + let days = seconds / DAY; + if days == 1 { "1 day".to_string() } else { format!("{days} days") } + } else if seconds >= HOUR { + format!("{}h", seconds / HOUR) + } else if seconds >= MINUTE { + format!("{}m", seconds / MINUTE) + } else { + format!("{seconds}s") + } +} + fn format_expiry(expiry: u64) -> String { if expiry == u64::MAX { return "never".to_string(); @@ -842,6 +1669,7 @@ fn key_entry_to_json(entry: &tempo::KeyEntry) -> serde_json::Value { #[cfg(test)] mod tests { use super::*; + use alloy_json_rpc::ErrorPayload; use std::str::FromStr; #[test] @@ -967,4 +1795,144 @@ mod tests { let json = r#"[{"target":"0x20c0000000000000000000000000000000000001","selectors":[{"selector":"transfer","recipients":[],"bogus":true}]}]"#; assert!(parse_scopes_json(json).is_err()); } + + #[test] + fn test_parse_policy_token_path_usd() { + assert_eq!(parse_policy_token("PathUSD").unwrap(), PATH_USD_ADDRESS); + assert_eq!(parse_policy_token("path-usd").unwrap(), PATH_USD_ADDRESS); + } + + #[test] + fn test_parse_period_units() { + assert_eq!(parse_period("0").unwrap(), 0); + assert_eq!(parse_period("30s").unwrap(), 30); + assert_eq!(parse_period("5m").unwrap(), 300); + assert_eq!(parse_period("2h").unwrap(), 7200); + assert_eq!(parse_period("7d").unwrap(), 604800); + assert_eq!(parse_period("2w").unwrap(), 1209600); + assert!(parse_period("1mo").is_err()); + } + + #[test] + fn test_add_selector_rule_merges_recipients() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let second = Address::from_str("0x2222222222222222222222222222222222222222").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![second], + }, + ); + + assert!(changed); + assert_eq!(scope.selectorRules.len(), 1); + assert_eq!(scope.selectorRules[0].recipients, vec![first, second]); + } + + #[test] + fn test_add_selector_rule_empty_recipients_widens_to_any() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(changed); + assert!(scope.selectorRules[0].recipients.is_empty()); + } + + #[test] + fn test_add_selector_rule_target_wildcard_is_unchanged() { + let mut scope = CallScope { target: PATH_USD_ADDRESS, selectorRules: vec![] }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(!changed); + assert!(scope.selectorRules.is_empty()); + } + + #[test] + fn test_policy_set_limit_parses() { + let key = "0x1111111111111111111111111111111111111111"; + + let command = KeychainSubcommand::try_parse_from([ + "keychain", + "policy", + "set-limit", + key, + "--token", + "PathUSD", + "--amount", + "123", + ]) + .unwrap(); + + match command { + KeychainSubcommand::Policy { + command: + KeychainPolicySubcommand::SetLimit { key_address, token, amount, period, .. }, + } => { + assert_eq!(key_address, Address::from_str(key).unwrap()); + assert_eq!(token, PATH_USD_ADDRESS); + assert_eq!(amount, U256::from(123)); + assert_eq!(period, None); + } + other => panic!("unexpected command: {other:?}"), + } + } + + #[test] + fn test_active_from_anvil_node_info_requires_tempo_network() { + let tempo_t3 = + AnvilNodeInfo { network: Some("tempo".to_string()), hard_fork: Some("T3".to_string()) }; + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T2), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T3), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T4), Some(false)); + + let ethereum_t3 = AnvilNodeInfo { + network: Some("ethereum".to_string()), + hard_fork: Some("T3".to_string()), + }; + assert_eq!(active_from_anvil_node_info(ðereum_t3, TempoHardfork::T3), None); + } + + #[test] + fn test_rpc_method_not_found_detection() { + let method_missing: TransportError = + TransportError::ErrorResp(ErrorPayload::method_not_found()); + assert!(is_rpc_method_not_found(&method_missing)); + + let internal_error: TransportError = + TransportError::ErrorResp(ErrorPayload::internal_error()); + assert!(!is_rpc_method_not_found(&internal_error)); + + let transport_error = alloy_transport::TransportErrorKind::backend_gone(); + assert!(!is_rpc_method_not_found(&transport_error)); + } } diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index 8aaf6e97a1827..67178cd093d7b 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -2,7 +2,9 @@ use crate::tx::{self, CastTxBuilder}; use alloy_consensus::{SignableTransaction, Signed}; use alloy_eips::Encodable2718; use alloy_ens::NameOrAddress; -use alloy_network::{Ethereum, EthereumWallet, Network, NetworkTransactionBuilder}; +use alloy_network::{ + Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, TransactionBuilder, +}; use alloy_primitives::{Address, hex}; use alloy_provider::Provider; use alloy_signer::{Signature, Signer}; @@ -10,7 +12,7 @@ use clap::Parser; use eyre::Result; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::LoadConfig, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use std::{path::PathBuf, str::FromStr}; @@ -94,9 +96,13 @@ impl MakeTxArgs { N::UnsignedTx: SignableTransaction, N::TransactionRequest: FoundryTransactionBuilder, { - let Self { to, mut sig, mut args, command, tx, path, eth, raw_unsigned, ethsign } = self; + let Self { to, mut sig, mut args, command, mut tx, path, eth, raw_unsigned, ethsign } = + self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -117,6 +123,11 @@ impl MakeTxArgs { let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + // Must happen before `tx.clone()` so the cloned tx carries the resolved nonce_key. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config) .await? .with_to(to) @@ -139,6 +150,10 @@ impl MakeTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + if raw_unsigned { // Build unsigned raw tx // Check if nonce is provided when --from is not specified @@ -148,11 +163,20 @@ impl MakeTxArgs { "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce" ); } + if tempo_sponsor.is_some() && eth.wallet.from.is_none() { + eyre::bail!( + "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender" + ); + } // Use zero address as placeholder for unsigned transactions let from = eth.wallet.from.unwrap_or(Address::ZERO); - let (tx, _) = tx_builder.build(from).await?; + let (mut tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -162,7 +186,11 @@ impl MakeTxArgs { if ethsign { // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has // unlocked accounts. - let (tx, _) = tx_builder.build(config.sender).await?; + let (mut tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, config.sender).await?; + } let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; @@ -176,7 +204,11 @@ impl MakeTxArgs { tx::validate_from_address(eth.wallet.from, from)?; - let (tx, _) = tx_builder.build(&signer).await?; + let (mut tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let tx = tx.build(&EthereumWallet::new(signer)).await?; diff --git a/crates/cast/src/cmd/mod.rs b/crates/cast/src/cmd/mod.rs index 6a9d11f5dc61d..0b1b26615694a 100644 --- a/crates/cast/src/cmd/mod.rs +++ b/crates/cast/src/cmd/mod.rs @@ -15,6 +15,7 @@ pub mod call; pub mod constructor_args; pub mod create2; pub mod creation_code; +#[cfg(feature = "optimism")] pub mod da_estimate; pub mod erc20; pub mod estimate; @@ -28,7 +29,9 @@ pub mod rpc; pub mod run; pub mod send; pub mod storage; +pub mod tempo; pub mod tip20; pub mod trace; pub mod txpool; +pub mod vaddr; pub mod wallet; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 84699d6cd6956..7e52a9e265f25 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -29,10 +29,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock as _, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, }, executors::{EvmError, Executor, TracingExecutor}, hardforks::FoundryHardfork, @@ -123,12 +125,15 @@ impl RunArgs { evm_opts.infer_network_from_fork().await; if evm_opts.networks.is_tempo() { - self.run_with_evm::().await - } else if evm_opts.networks.is_optimism() { - self.run_with_evm::().await - } else { - self.run_with_evm::().await + return self.run_with_evm::().await; + } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_evm::().await; } + + self.run_with_evm::().await } async fn run_with_evm(self) -> Result<()> { diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 2d6e248cc7a73..421aae1f2153e 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -8,7 +8,10 @@ use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::{Signature, Signer}; use clap::Parser; use eyre::{Result, eyre}; -use foundry_cli::{opts::TransactionOpts, utils::LoadConfig}; +use foundry_cli::{ + opts::TransactionOpts, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, +}; use foundry_common::{ FoundryTransactionBuilder, fmt::{UIfmt, UIfmtReceiptExt}, @@ -119,7 +122,9 @@ impl SendTxArgs { self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; - let sponsor_signature = tx.tempo.sponsor_signature; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -183,6 +188,8 @@ impl SendTxArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -202,13 +209,19 @@ impl SendTxArgs { // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit. if print_sponsor_hash { - // Use the pre-resolved signer to derive the actual sender address, since the - // sponsor hash commits to the sender. - let signer = pre_resolved_signer.as_ref().ok_or_else(|| { - eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") - })?; - let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (tx, from) = if let Some(ref ak) = access_key { + let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?; + (tx, ak.wallet_address) + } else { + // Use the pre-resolved signer to derive the actual sender address, since the + // sponsor hash commits to the sender. + let signer = pre_resolved_signer.as_ref().ok_or_else(|| { + eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") + })?; + let from = signer.address(); + let (tx, _) = builder.build(from).await?; + (tx, from) + }; let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?; @@ -216,6 +229,10 @@ impl SendTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); // Launch browser signer if `--browser` flag is set @@ -245,11 +262,18 @@ impl SendTxArgs { } } - let (tx, _) = builder.build(config.sender).await?; + let (mut tx_request, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, config.sender).await?; + } cast_send( provider, - tx, + tx_request, send_tx.cast_async, send_tx.sync, send_tx.confirmations, @@ -261,6 +285,10 @@ impl SendTxArgs { } else if let Some(browser) = browser { let chain = builder.chain(); let (mut tx_request, _) = builder.build(browser.address()).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which // costs more gas for signature verification on Tempo chains. Add a @@ -270,6 +298,9 @@ impl SendTxArgs { { tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, browser.address()).await?; + } let tx_hash = browser.send_transaction_via_browser(tx_request).await?; @@ -283,7 +314,14 @@ impl SendTxArgs { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; - let (tx_request, _) = builder.build(ak.wallet_address).await?; + let (mut tx_request, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, ak.wallet_address).await?; + } cast_send_with_access_key( &provider, tx_request, @@ -308,11 +346,13 @@ impl SendTxArgs { tx::validate_from_address(send_tx.eth.wallet.from, from)?; let (mut tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; - // Apply sponsor signature after gas estimation so the estimate is - // consistent with what `--tempo.print-sponsor-hash` computes. - if let Some(sig) = sponsor_signature { - tx_request.set_fee_payer_signature(sig); + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, from).await?; } let wallet = EthereumWallet::from(signer); diff --git a/crates/cast/src/cmd/tempo.rs b/crates/cast/src/cmd/tempo.rs new file mode 100644 index 0000000000000..6744e6b73c039 --- /dev/null +++ b/crates/cast/src/cmd/tempo.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use eyre::Result; +use foundry_common::tempo::{EnsureAccessKeyConfig, ensure_access_key}; + +/// Tempo wallet integration commands. +#[derive(Debug, Parser)] +pub enum TempoSubcommand { + /// Authorize a new access key against your Tempo wallet via wallet.tempo. + /// + /// Persists the key to `$TEMPO_HOME/wallet/keys.toml` (default + /// `~/.tempo/wallet/keys.toml`). Also runs automatically on a 402 from a + /// Tempo RPC when no local key is configured. + /// + /// Env: `TEMPO_HOME`, `TEMPO_CLI_AUTH_URL` (override auth service). + Login { + /// Chain ID to authorize the key for. Defaults to Tempo mainnet (4217). + #[arg(long, default_value_t = 4217)] + chain_id: u64, + + /// Print the authorization URL to stderr instead of opening a browser. + #[arg(long)] + no_browser: bool, + }, +} + +impl TempoSubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::Login { chain_id, no_browser } => { + let mut cfg = EnsureAccessKeyConfig::from_env(chain_id); + if no_browser { + cfg.no_browser = true; + } + let outcome = ensure_access_key(cfg).await?; + let _ = foundry_common::sh_println!( + "Authorized key {} for wallet {} on chain {}", + outcome.key_address, + outcome.wallet_address, + outcome.chain_id, + ); + Ok(()) + } + } + } +} diff --git a/crates/cast/src/cmd/tip20/mine.rs b/crates/cast/src/cmd/tip20/mine.rs index a5f9062482a01..0367450a19b06 100644 --- a/crates/cast/src/cmd/tip20/mine.rs +++ b/crates/cast/src/cmd/tip20/mine.rs @@ -20,11 +20,11 @@ use tempo_primitives::{MasterId, TempoAddressExt, UserTag}; const POW_BYTES: usize = 4; -pub(super) struct Output { - pub(super) salt: B256, - pub(super) registration_hash: B256, - pub(super) master_id: MasterId, - pub(super) zero_tag_virtual_address: Address, +pub(crate) struct Output { + pub(crate) salt: B256, + pub(crate) registration_hash: B256, + pub(crate) master_id: MasterId, + pub(crate) zero_tag_virtual_address: Address, } pub(super) fn run( @@ -127,7 +127,12 @@ pub(super) async fn register( Ok(()) } -fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Result { +pub(crate) fn mine( + master: Address, + salt: B256, + n_threads: usize, + pow_bytes: usize, +) -> Result { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); @@ -144,7 +149,7 @@ fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Resu .ok_or_else(|| eyre::eyre!("virtual master mining failed: all threads panicked")) } -fn derive(master: Address, salt: B256) -> Output { +pub(crate) fn derive(master: Address, salt: B256) -> Output { let registration_hash = registration_hash(master, salt); let master_id = MasterId::from_slice(®istration_hash[4..8]); let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO); @@ -152,14 +157,14 @@ fn derive(master: Address, salt: B256) -> Output { Output { salt, registration_hash, master_id, zero_tag_virtual_address } } -fn registration_hash(master: Address, salt: B256) -> B256 { +pub(crate) fn registration_hash(master: Address, salt: B256) -> B256 { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); packed[20..].copy_from_slice(salt.as_slice()); keccak256(packed) } -fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { +pub(crate) fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { registration_hash[..pow_bytes].iter().all(|byte| *byte == 0) } diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index e3c39b2c9bb18..edb4c3b7a57b3 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; mod create; pub(crate) use create::iso4217_warning_message; -mod mine; +pub(crate) mod mine; /// TIP-20 token operations (Tempo). #[derive(Debug, Parser, Clone)] diff --git a/crates/cast/src/cmd/vaddr/create.rs b/crates/cast/src/cmd/vaddr/create.rs new file mode 100644 index 0000000000000..0563b09601323 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/create.rs @@ -0,0 +1,181 @@ +use crate::{ + cmd::{ + erc20::build_provider_with_signer, + send::{cast_send, cast_send_with_access_key}, + tip20::mine, + }, + tx::{SendTxOpts, TxParams}, +}; +use alloy_primitives::{Address, B256}; +use alloy_signer::Signer; +use eyre::Result; +use foundry_cli::utils::{LoadConfig, get_chain}; +use foundry_common::{provider::ProviderBuilder, shell}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; +use serde_json::json; +use std::time::Instant; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; +use tempo_primitives::{TempoAddressExt, UserTag}; + +const POW_BYTES: usize = 4; + +#[allow(clippy::too_many_arguments)] +pub(super) async fn run( + owner: Address, + salt: Option, + tag: u64, + count: u32, + threads: Option, + seed: Option, + no_random: bool, + no_register: bool, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + if count == 0 { + // no virtual addresses to compute + return Ok(()); + } + + if !owner.is_valid_master() { + eyre::bail!( + "invalid owner address {owner}; see https://docs.tempo.xyz/protocol/tips/tip-1022" + ); + } + + let output = if let Some(salt) = salt { + let output = mine::derive(owner, salt); + if !mine::has_pow(&output.registration_hash, POW_BYTES) { + eyre::bail!( + "provided salt does not satisfy TIP-1022 proof of work: {}", + output.registration_hash + ); + } + output + } else { + let mut n_threads = threads.unwrap_or(0); + if n_threads == 0 { + n_threads = std::thread::available_parallelism().map_or(1, |n| n.get()); + } + + let mut start_salt = B256::ZERO; + if !no_random { + let mut rng = match seed { + Some(seed) => StdRng::from_seed(seed.0), + None => StdRng::from_os_rng(), + }; + rng.fill_bytes(&mut start_salt[..]); + } + + if !shell::is_json() { + sh_println!("Mining TIP-1022 salt for {owner} with {n_threads} threads...")?; + } + let timer = Instant::now(); + let output = mine::mine(owner, start_salt, n_threads, POW_BYTES)?; + if !shell::is_json() { + sh_println!("Found salt in {:?}", timer.elapsed())?; + } + output + }; + + const MAX_USER_TAG: u64 = 0x0000_FFFF_FFFF_FFFF; + let mut virtual_addresses = Vec::with_capacity(count as usize); + for i in 0..count { + let tag_value = tag + .checked_add(i as u64) + .filter(|&t| t <= MAX_USER_TAG) + .ok_or_else(|| eyre::eyre!("tag overflow: tag + count exceeds the 6-byte user tag range (max {MAX_USER_TAG:#x})"))?; + let raw = tag_value.to_be_bytes(); + let user_tag = UserTag::new(raw[2..].try_into().expect("slice is 6 bytes")); + let vaddr = Address::new_virtual(output.master_id, user_tag); + virtual_addresses.push((user_tag, vaddr)); + } + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "salt": format!("{}", output.salt), + "registration_hash": format!("{}", output.registration_hash), + "master_id": format!("{}", output.master_id), + "virtual_addresses": virtual_addresses.iter().map(|(tag, addr)| json!({ + "tag": format!("{tag}"), + "address": format!("{addr}"), + })).collect::>(), + }))? + )?; + } else { + sh_println!( + "Salt: {} +Registration hash: {} +Master ID: {}", + output.salt, + output.registration_hash, + output.master_id, + )?; + sh_println!("\nVirtual addresses:")?; + for (tag, vaddr) in &virtual_addresses { + sh_println!(" tag={tag} {vaddr}")?; + } + } + + if no_register { + return Ok(()); + } + + register(owner, output.salt, send_tx, tx_opts).await +} + +async fn register( + owner: Address, + salt: B256, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let signer = signer.ok_or_else(|| { + eyre::eyre!("cast vaddr create requires a signer (for example --private-key or --from)") + })?; + + let sender = + tempo_access_key.as_ref().map(|ak| ak.wallet_address).unwrap_or_else(|| signer.address()); + + if sender != owner { + eyre::bail!( + "signer mismatch: salt is for {owner}, but the configured signer would register as {sender}" + ); + } + + let config = send_tx.eth.load_config()?; + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider) + .registerVirtualMaster(salt) + .into_transaction_request(); + tx_opts.apply::(&mut tx, get_chain(config.chain, &provider).await?.is_legacy()); + + sh_println!("Submitting registerVirtualMaster({salt})...")?; + + if let Some(ref access_key) = tempo_access_key { + cast_send_with_access_key( + &provider, + tx, + &signer, + access_key, + send_tx.cast_async, + send_tx.confirmations, + timeout, + ) + .await?; + } else { + let provider = build_provider_with_signer::(&send_tx, signer)?; + cast_send(provider, tx, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout) + .await?; + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/mod.rs b/crates/cast/src/cmd/vaddr/mod.rs new file mode 100644 index 0000000000000..446d1e7ece5e2 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/mod.rs @@ -0,0 +1,131 @@ +use crate::tx::{SendTxOpts, TxParams}; +use alloy_primitives::{Address, B256}; +use clap::Parser; +use foundry_cli::opts::RpcOpts; + +mod create; +mod resolve; +mod watch; + +/// TIP-1022 virtual address registry operations (Tempo). +/// +/// Virtual addresses are deterministic 20-byte aliases (masterId || VIRTUAL_MAGIC || userTag) +/// that auto-forward TIP-20 deposits to a registered master wallet at the protocol level, +/// with no on-chain sweep transaction required. +/// +/// See: +#[derive(Debug, Parser, Clone)] +pub enum VaddrSubcommand { + /// Mine a TIP-1022 proof-of-work salt, register as a virtual address master, and print + /// derived virtual addresses for the given owner. + #[command(visible_alias = "c")] + Create { + /// The master (owner) address that will control all virtual addresses under this + /// registration. Must not be the zero address, a virtual address, or a TIP-20 token. + #[arg(long, value_name = "ADDRESS")] + owner: Address, + + /// Use this salt directly instead of mining one. Must satisfy the 32-bit PoW requirement. + #[arg(long, conflicts_with_all = ["seed", "no_random"], value_name = "HEX")] + salt: Option, + + /// Starting user tag for the derived virtual address output (hex-encoded 6 bytes). + #[arg(long, default_value = "0", value_name = "U64")] + tag: u64, + + /// Number of virtual addresses to derive and print. + #[arg(long, default_value = "1", value_name = "N")] + count: u32, + + /// Number of threads to use for mining. Defaults to number of logical cores. + #[arg(long, short = 'j', visible_alias = "jobs")] + threads: Option, + + /// Seed for the random number generator used to initialize the salt search. + #[arg(long, value_name = "HEX")] + seed: Option, + + /// Start salt search from zero instead of a random value. + #[arg(long, conflicts_with = "seed")] + no_random: bool, + + /// Mine and print the salt and derived virtual addresses without submitting the + /// registerVirtualMaster transaction. + #[arg(long)] + no_register: bool, + + #[command(flatten)] + send_tx: Box, + + #[command(flatten)] + tx: Box, + }, + + /// Resolve a virtual address to its registered master and decode its components. + #[command(visible_alias = "r")] + Resolve { + /// The virtual address to resolve. + #[arg(value_name = "ADDRESS")] + addr: Address, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Watch (tail) incoming TIP-20 transfers to a virtual address. + #[command(visible_alias = "w")] + Watch { + /// The virtual address to monitor. + #[arg(value_name = "ADDRESS")] + addr: Address, + + /// Filter on a specific TIP-20 token address. Watches all tokens if omitted. + #[arg(long, value_name = "ADDRESS")] + token: Option
, + + /// Block number to start from. Defaults to the current latest block. + #[arg(long, value_name = "BLOCK")] + from_block: Option, + + #[command(flatten)] + rpc: RpcOpts, + }, +} + +impl VaddrSubcommand { + pub async fn run(self) -> eyre::Result<()> { + match self { + Self::Create { + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + send_tx, + tx, + } => { + create::run( + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + *send_tx, + *tx, + ) + .await? + } + Self::Resolve { addr, rpc } => resolve::run(addr, rpc).await?, + Self::Watch { addr, token, from_block, rpc } => { + watch::run(addr, token, from_block, rpc).await? + } + } + Ok(()) + } +} diff --git a/crates/cast/src/cmd/vaddr/resolve.rs b/crates/cast/src/cmd/vaddr/resolve.rs new file mode 100644 index 0000000000000..96936f4fe4713 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/resolve.rs @@ -0,0 +1,52 @@ +use alloy_primitives::{Address, hex}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; + +pub(super) async fn run(addr: Address, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let registry = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider); + + let decode_builder = registry.decodeVirtualAddress(addr); + let resolve_builder = registry.resolveVirtualAddress(addr); + let (decoded, master) = tokio::try_join!(decode_builder.call(), resolve_builder.call())?; + + if !decoded.isVirtual { + sh_println!("{addr} is not a virtual address")?; + return Ok(()); + } + + let master_id = decoded.masterId; + let user_tag = decoded.userTag; + let master: Address = master; + + if shell::is_json() { + let master_address = if master.is_zero() { None } else { Some(format!("{master}")) }; + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "address": format!("{addr}"), + "master_id": format!("0x{}", hex::encode(master_id)), + "user_tag": format!("0x{}", hex::encode(user_tag)), + "master_address": master_address, + }))? + )?; + } else { + sh_println!("Virtual address: {addr}")?; + sh_println!("Master ID: 0x{}", hex::encode(master_id))?; + sh_println!("User tag: 0x{}", hex::encode(user_tag))?; + if master.is_zero() { + sh_println!("Master address: (unregistered)")?; + } else { + sh_println!("Master address: {master}")?; + } + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/watch.rs b/crates/cast/src/cmd/vaddr/watch.rs new file mode 100644 index 0000000000000..dc159d5e37c8e --- /dev/null +++ b/crates/cast/src/cmd/vaddr/watch.rs @@ -0,0 +1,108 @@ +use alloy_primitives::{Address, B256, keccak256}; +use alloy_provider::Provider; +use alloy_rpc_types::{BlockNumberOrTag, Filter}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use std::sync::LazyLock; +use tempo_alloy::TempoNetwork; +use tempo_primitives::TempoAddressExt; + +static TRANSFER_TOPIC: LazyLock = + LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)")); + +pub(super) async fn run( + addr: Address, + token: Option
, + from_block: Option, + rpc: RpcOpts, +) -> Result<()> { + if !addr.is_virtual() { + eyre::bail!("{addr} is not a virtual address"); + } + + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + // Transfer(address indexed from, address indexed to, uint256 value) + // topic[0] = event sig, topic[1] = from, topic[2] = to + let to_topic: B256 = { + let mut buf = [0u8; 32]; + buf[12..].copy_from_slice(addr.as_slice()); + buf.into() + }; + + let start = from_block.map(BlockNumberOrTag::Number).unwrap_or(BlockNumberOrTag::Latest); + + let mut filter = + Filter::new().event_signature(*TRANSFER_TOPIC).topic2(to_topic).from_block(start); + + if let Some(tok) = token { + filter = filter.address(tok); + } + + if !shell::is_json() { + sh_println!("Watching transfers to {addr}... (Ctrl-C to stop)")?; + } + + // Fetch logs from the requested start block (historical when from_block is set) + let logs = provider.get_logs(&filter).await?; + for log in &logs { + print_transfer_log(log)?; + } + + // Poll for new logs + let mut last_block = provider.get_block_number().await?; + loop { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let current = provider.get_block_number().await?; + if current > last_block { + let poll_filter = filter.clone().from_block(last_block + 1).to_block(current); + let new_logs = provider.get_logs(&poll_filter).await?; + for log in &new_logs { + print_transfer_log(log)?; + } + last_block = current; + } + } +} + +fn print_transfer_log(log: &alloy_rpc_types::Log) -> Result<()> { + let block = log.block_number.unwrap_or(0); + let tx = log.transaction_hash.unwrap_or_default(); + let token = log.address(); + + // Decode topics: topic[1]=from, topic[2]=to + let from = log.topics().get(1).map(|t| { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&t[12..]); + Address::from(addr) + }); + + // Decode amount from data + let amount = if log.data().data.len() >= 32 { + alloy_primitives::U256::from_be_slice(&log.data().data[..32]) + } else { + alloy_primitives::U256::ZERO + }; + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string(&json!({ + "block": block, + "tx": format!("{tx}"), + "token": format!("{token}"), + "from": from.map(|a| format!("{a}")).unwrap_or_default(), + "amount": amount.to_string(), + }))? + )?; + } else { + sh_println!( + "block={block} tx={tx} token={token} from={} amount={amount}", + from.map(|a| a.to_string()).unwrap_or_default(), + )?; + } + Ok(()) +} diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 8e0dd8dd3ed8c..b2378d8bfdc58 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -779,8 +779,7 @@ flag to set your key via: )?; let address = wallet.address(); let success_message = format!( - "`{}` keystore was saved successfully. Address: {:?}", - &account_name, address, + "`{account_name}` keystore was saved successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } @@ -815,7 +814,7 @@ flag to set your key via: format!("Failed to remove keystore file at {}", keystore_path.display()) })?; - let success_message = format!("`{}` keystore was removed successfully.", &name); + let success_message = format!("`{name}` keystore was removed successfully."); sh_println!("{}", success_message.green())?; } Self::PrivateKey { @@ -886,8 +885,7 @@ flag to set your key via: let private_key = B256::from_slice(&wallet.credential().to_bytes()); - let success_message = - format!("{}'s private key is: {}", &account_name, private_key); + let success_message = format!("{account_name}'s private key is: {private_key}"); sh_println!("{}", success_message.green())?; } @@ -945,10 +943,9 @@ flag to set your key via: Some(&account_name), )?; + let address = wallet.address(); let success_message = format!( - "Password for keystore `{}` was changed successfully. Address: {:?}", - &account_name, - wallet.address(), + "Password for keystore `{account_name}` was changed successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index ce5572acebc13..2b1b03486bf04 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -40,6 +40,7 @@ use foundry_common::{ use foundry_config::Chain; use foundry_evm::core::bytecode::InstIter; use futures::{FutureExt, StreamExt, future::Either}; +#[cfg(feature = "optimism")] use op_alloy_consensus as _; use rayon::prelude::*; @@ -60,6 +61,7 @@ pub use foundry_evm::*; pub mod args; pub mod cmd; pub mod opts; +pub mod tempo; pub mod base; pub mod call_spec; @@ -246,7 +248,7 @@ impl + Clone + Unpin, N: Network> Cast { let mut s = vec![format!("gas used: {}", access_list.gas_used), "access list:".to_string()]; for al in access_list.access_list.0 { - s.push(format!("- address: {}", &al.address.to_checksum(None))); + s.push(format!("- address: {}", al.address.to_checksum(None))); if !al.storage_keys.is_empty() { s.push(" keys:".to_string()); for key in al.storage_keys { diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index effc081e8072a..763eb132ddb5c 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -1,11 +1,13 @@ +#[cfg(feature = "optimism")] +use crate::cmd::da_estimate::DAEstimateArgs; use crate::cmd::{ access_list::AccessListArgs, artifact::ArtifactArgs, b2e_payload::B2EPayloadArgs, batch_mktx::BatchMakeTxArgs, batch_send::BatchSendArgs, bind::BindArgs, call::CallArgs, constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs, - da_estimate::DAEstimateArgs, erc20::Erc20Subcommand, estimate::EstimateArgs, - find_block::FindBlockArgs, interface::InterfaceArgs, keychain::KeychainSubcommand, - logs::LogsArgs, mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, - storage::StorageArgs, tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, + erc20::Erc20Subcommand, estimate::EstimateArgs, find_block::FindBlockArgs, + interface::InterfaceArgs, keychain::KeychainSubcommand, logs::LogsArgs, mktx::MakeTxArgs, + rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, tempo::TempoSubcommand, + tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, vaddr::VaddrSubcommand, wallet::WalletSubcommands, }; use alloy_ens::NameOrAddress; @@ -1163,6 +1165,7 @@ pub enum CastSubcommand { command: TxPoolSubcommands, }, /// Estimates the data availability size of a given opstack block. + #[cfg(feature = "optimism")] #[command(name = "da-estimate")] DAEstimate(DAEstimateArgs), @@ -1186,6 +1189,20 @@ pub enum CastSubcommand { #[command(subcommand)] command: KeychainSubcommand, }, + + /// Tempo wallet integration (login, etc.). + Tempo { + #[command(subcommand)] + command: TempoSubcommand, + }, + + /// TIP-1022 virtual address registry operations (Tempo). + #[command(visible_alias = "vaddr")] + VirtualAddress { + #[command(subcommand)] + command: VaddrSubcommand, + }, + #[command(name = "trace")] Trace(TraceArgs), } diff --git a/crates/cast/src/tempo.rs b/crates/cast/src/tempo.rs new file mode 100644 index 0000000000000..737c33f5b70de --- /dev/null +++ b/crates/cast/src/tempo.rs @@ -0,0 +1,3 @@ +//! Tempo transaction helpers used by Cast-facing commands. + +pub use foundry_common::tempo::{TempoSponsor, TempoSponsorPreview, resolve_tempo_sponsor_signer}; diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 96f5fc5137575..b58136f4ae9de 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -20,7 +20,7 @@ use foundry_common::{ get_pretty_receipt_w_reason_attr, shell, }; use foundry_config::{Chain, Config}; -use foundry_wallets::{BrowserWalletOpts, WalletOpts, WalletSigner}; +use foundry_wallets::{BrowserWalletOpts, TempoAccessKeyConfig, WalletOpts, WalletSigner}; use itertools::Itertools; use serde_json::value::RawValue; use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration}; @@ -535,13 +535,29 @@ where sender: impl Into>, ) -> Result<(N::TransactionRequest, Option)> { let fill = self.fill; - self._build(sender, fill).await + self._build(sender, fill, None).await + } + + /// Builds a transaction that will be signed by a Tempo access key. + /// + /// The access-key id is set before gas estimation. If the access key needs on-chain + /// provisioning, its authorization is embedded before access-list/gas estimation and before + /// any sponsor digest can be computed. + pub async fn build_with_access_key( + mut self, + sender: impl Into>, + access_key: &TempoAccessKeyConfig, + ) -> Result<(N::TransactionRequest, Option)> { + self.tx.set_key_id(access_key.key_address); + let fill = self.fill; + self._build(sender, fill, Some(access_key)).await } async fn _build( mut self, sender: impl Into>, fill: bool, + access_key: Option<&TempoAccessKeyConfig>, ) -> Result<(N::TransactionRequest, Option)> { // prepare let sender = sender.into(); @@ -555,6 +571,16 @@ where // resolve let tx_nonce = self.resolve_nonce(sender.address(), fill).await?; self.resolve_auth(&sender, tx_nonce).await?; + if let Some(access_key) = access_key { + self.tx + .prepare_access_key_authorization( + &self.provider, + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.as_ref(), + ) + .await?; + } self.resolve_access_list().await?; // fill diff --git a/crates/cast/tests/cli/keychain.rs b/crates/cast/tests/cli/keychain.rs new file mode 100644 index 0000000000000..88e9e16983cc5 --- /dev/null +++ b/crates/cast/tests/cli/keychain.rs @@ -0,0 +1,76 @@ +//! CLI tests for `cast keychain` subcommands. + +use anvil::NodeConfig; +use foundry_test_utils::util::OutputExt; + +/// Anvil test accounts (standard mnemonic). +mod accounts { + pub const PK1: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + pub const ADDR1: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + pub const ADDR2: &str = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; + pub const TOKEN: &str = "0x20C000000000000000000000b9537d11c60E8b50"; // PathUSD +} + +// `cast keychain rl --json` must emit `{"remaining":""}`, not a bare string. +casttest!(keychain_rl_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "rl", + accounts::ADDR1, + accounts::ADDR2, + accounts::TOKEN, + "--rpc-url", + &rpc, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain rl --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + assert!( + parsed.get("remaining").is_some(), + "expected 'remaining' key in JSON output, got: {output}" + ); + // Must not be a bare string (old bug: `"0"`) + assert!(!parsed.is_string(), "JSON output must not be a bare string, got: {output}"); +}); + +// `cast keychain authorize --tempo.print-sponsor-hash --json` must emit +// `{"sponsor_hash":"0x..."}`, not a raw hex string. +casttest!(keychain_authorize_sponsor_hash_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "authorize", + accounts::ADDR2, // key to authorize + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + "--tempo.print-sponsor-hash", + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain authorize --tempo.print-sponsor-hash --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + let hash = parsed + .get("sponsor_hash") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("expected 'sponsor_hash' key in JSON output, got: {output}")); + assert!(hash.starts_with("0x"), "sponsor_hash should be 0x-prefixed, got: {hash}"); + assert_eq!(hash.len(), 66, "sponsor_hash should be 32-byte hex (66 chars), got: {hash}"); +}); diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index da33a34d849db..2f744efe4d4f0 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,6 +1,7 @@ //! Contains various tests for checking cast commands use alloy_chains::NamedChain; +use alloy_eips::Decodable2718; use alloy_hardforks::EthereumHardfork; use alloy_network::{TransactionBuilder, TransactionResponse}; use alloy_primitives::{B256, Bytes, U256, address, b256, hex}; @@ -20,11 +21,13 @@ use foundry_test_utils::{ }; use serde_json::json; use std::{fs, path::Path, str::FromStr}; +use tempo_primitives::TempoTxEnvelope; #[macro_use] extern crate foundry_test_utils; mod erc20; +mod keychain; mod selectors; casttest!(print_short_version, |_prj, cmd| { @@ -2055,6 +2058,55 @@ casttest!(mktx_ethsign, async |_prj, cmd| { ]]); }); +// tests that `cast mktx --tempo.lane ` resolves the lane against a `tempo.lanes.toml` file at +// the project root, sets the corresponding `nonce_key` on the produced Tempo AA transaction. +casttest!(mktx_tempo_lane_resolves_nonce_key, |prj, cmd| { + // Write a shared lanes file at the project root. + let lanes_path = prj.root().join("tempo.lanes.toml"); + fs::write(&lanes_path, "deploy = 1\nops = 2\npayments = 42\n").unwrap(); + + let output = cmd + .current_dir(prj.root()) + .args([ + "mktx", + "--tempo.lane", + "payments", + "--private-key", + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--chain", + "1", + "--nonce", + "0", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + ]) + .assert_success() + .get_output() + .clone(); + + // The resolved-lane breadcrumb is printed to stderr so it doesn't pollute stdout + // (which carries the raw signed transaction). + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("lane: payments (nonce_key=42, nonce=0)"), + "expected lane breadcrumb on stderr, got: {stderr}", + ); + + // Decode the produced signed Tempo AA transaction and verify it carries the + // resolved 2D nonce key. + let stdout = String::from_utf8_lossy(&output.stdout); + let raw_hex = stdout.trim().trim_start_matches("0x"); + let raw = hex::decode(raw_hex).expect("decode hex output"); + let envelope = TempoTxEnvelope::decode_2718(&mut raw.as_slice()).expect("decode tempo tx"); + assert!(envelope.is_aa(), "expected Tempo AA transaction, got: {envelope:?}"); + assert_eq!(envelope.nonce_key(), Some(U256::from(42_u64))); +}); + // tests that the raw encoded transaction is returned casttest!(tx_raw, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); @@ -4024,6 +4076,7 @@ Warning: Contract code is empty }); // +#[cfg(feature = "optimism")] casttest!(tx_raw_opstack_deposit, |_prj, cmd| { cmd.args([ "tx", @@ -5020,6 +5073,7 @@ casttest!(cast_decode_tx_network_flag_short_and_long_equivalent, |_prj, cmd| { // Test that `--network optimism` and `-n optimism` produce identical output for decode-tx. // Uses a known OP-stack deposit transaction (same tx as tx_raw_opstack_deposit test). +#[cfg(feature = "optimism")] casttest!(cast_decode_tx_network_optimism_short_and_long_equivalent, |_prj, cmd| { let tx = "0x7ef90207a0cbde10ec697aff886f95d2514bab434e455620627b9bb8ba33baaaa4d537d62794d45955f4de64f1840e5686e64278da901e263031944200000000000000000000000000000000000007872386f26fc10000872386f26fc1000083096c4980b901a4d764ad0b0001000000000000000000000000000000000000000000000000000000065132000000000000000000000000fd0bf71f60660e2f608ed56e1659c450eb1131200000000000000000000000004200000000000000000000000000000000000010000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a41635f5fd000000000000000000000000ca11bde05977b3631167028862be2a173976ca110000000000000000000000005703b26fe5a7be820db1bf34c901a79da1a46ba4000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -5076,3 +5130,68 @@ casttest!(run_evm_version_updates_gas_params, |_prj, cmd| { "expected Spurious Dragon gas (177241), got: {sd_output}" ); }); + +// Tests for `cast vaddr` JSON output +casttest!(vaddr_create_json_output, |_prj, cmd| { + // Use a pre-computed salt that satisfies the 4-byte PoW requirement for this owner. + // Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 + // Owner: 0x1234567890123456789012345678901234567890 + let out = cmd + .args([ + "--json", + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + "--count", + "2", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let v: serde_json::Value = serde_json::from_str(out.trim()).expect("valid JSON"); + assert_eq!(v["salt"], "0x0000000000000000000000000000000000000000000000003ee0a78d00000000"); + assert_eq!( + v["registration_hash"], + "0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396" + ); + assert_eq!(v["master_id"], "0x2f51c0c4"); + let addrs = v["virtual_addresses"].as_array().expect("array"); + assert_eq!(addrs.len(), 2); + assert_eq!(addrs[0]["tag"], "0x000000000000"); + assert_eq!( + addrs[0]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000000" + ); + assert_eq!(addrs[1]["tag"], "0x000000000001"); + assert_eq!( + addrs[1]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000001" + ); +}); + +casttest!(vaddr_create_plain_output, |_prj, cmd| { + cmd.args([ + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + ]) + .assert_success() + .stdout_eq(str![[r#" +Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 +Registration hash: 0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396 +Master ID: 0x2f51c0c4 + +Virtual addresses: + tag=0x000000000000 [..] + +"#]]); +}); diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 659fec7f1a333..0eab12331be04 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -68,3 +68,13 @@ tracing.workspace = true walkdir.workspace = true proptest.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", + "forge-script-sequence/optimism", +] diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 94974301df8ac..01de77b9c95fd 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -5447,7 +5447,7 @@ { "func": { "id": "expectEmit_0", - "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).", + "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external;", "visibility": "external", "mutability": "", @@ -5487,7 +5487,7 @@ { "func": { "id": "expectEmit_2", - "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.", + "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit() external;", "visibility": "external", "mutability": "", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 7c2e0741704b3..12cfd19017770 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1082,6 +1082,9 @@ interface Vm { /// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.). /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data (as specified by the booleans). + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; @@ -1093,6 +1096,9 @@ interface Vm { /// Prepare an expected log with all topic and data checks enabled. /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data. + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit() external; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index b22f76714dd0f..27545c1b6cd33 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -856,42 +856,6 @@ impl Cheatcodes { } } - // Handle mocked calls - if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { - let ctx = MockCallDataContext { - calldata: call.input.bytes(ecx), - value: call.transfer_value(), - }; - - if let Some(return_data_queue) = match mocks.get_mut(&ctx) { - Some(queue) => Some(queue), - None => mocks - .iter_mut() - .find(|(mock, _)| { - call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) - && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) - }) - .map(|(_, v)| v), - } && let Some(return_data) = if return_data_queue.len() == 1 { - // If the mocked calls stack has a single element in it, don't empty it - return_data_queue.front().map(|x| x.to_owned()) - } else { - // Else, we pop the front element - return_data_queue.pop_front() - } { - return Some(CallOutcome { - result: InterpreterResult { - result: return_data.ret_type, - output: return_data.data, - gas, - }, - memory_offset: call.return_memory_offset.clone(), - was_precompile_called: true, - precompile_call_logs: vec![], - }); - } - } - // Apply our prank if let Some(prank) = &self.get_prank(curr_depth) { // Apply delegate call, `call.caller`` will not equal `prank.prank_caller` @@ -932,6 +896,72 @@ impl Cheatcodes { } } + // Handle mocked calls + if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { + let ctx = MockCallDataContext { + calldata: call.input.bytes(ecx), + value: call.transfer_value(), + }; + + if let Some(return_data_queue) = match mocks.get_mut(&ctx) { + Some(queue) => Some(queue), + None => mocks + .iter_mut() + .find(|(mock, _)| { + call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) + && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) + }) + .map(|(_, v)| v), + } && let Some(return_data) = return_data_queue.front().map(|x| x.to_owned()) + { + if let Some(value) = call.transfer_value() { + let checkpoint = ecx.journal_mut().checkpoint(); + match ecx.journal_mut().transfer_loaded( + call.transfer_from(), + call.transfer_to(), + value, + ) { + None => { + if return_data.ret_type.is_ok() { + ecx.journal_mut().checkpoint_commit(); + } else { + ecx.journal_mut().checkpoint_revert(checkpoint); + } + } + Some(err) => { + ecx.journal_mut().checkpoint_revert(checkpoint); + return Some(CallOutcome { + result: InterpreterResult { + result: err.into(), + output: Bytes::new(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: false, + precompile_call_logs: vec![], + }); + } + } + } + + // If the mocked calls stack has a single element in it, don't empty it + if return_data_queue.len() > 1 { + return_data_queue.pop_front(); + } + + return Some(CallOutcome { + result: InterpreterResult { + result: return_data.ret_type, + output: return_data.data, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: true, + precompile_call_logs: vec![], + }); + } + } + // Apply EIP-2930 access list self.apply_accesslist(ecx); @@ -1497,6 +1527,21 @@ impl Inspector> for Cheatcode } } + // this will ensure we don't have false positives when trying to diagnose reverts in fork + // mode + let diag = self.fork_revert_diagnostic.take(); + + // If the call already reverted, preserve that primary failure and skip post-call + // expect* validation so it cannot overwrite the original revert. + if outcome.result.is_revert() { + // if there's a revert and a previous call was diagnosed as fork related revert then we + // can return a better error here + if let Some(err) = diag { + outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); + } + return; + } + // At the end of the call, // we need to check if we've found all the emits. // We know we've found all the expected emits in the right order @@ -1574,19 +1619,6 @@ impl Inspector> for Cheatcode self.expected_emits.clear() } - // this will ensure we don't have false positives when trying to diagnose reverts in fork - // mode - let diag = self.fork_revert_diagnostic.take(); - - // if there's a revert and a previous call was diagnosed as fork related revert then we can - // return a better error here - if outcome.result.is_revert() - && let Some(err) = diag - { - outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); - return; - } - // try to diagnose reverts in multi-fork mode where a call is made to an address that does // not exist if let TxKind::Call(test_contract) = ecx.tx().kind() { @@ -1867,10 +1899,23 @@ impl Inspector> for Cheatcode } // Handle expected reverts - if let Some(expected_revert) = &self.expected_revert + if let Some(expected_revert) = &mut self.expected_revert && curr_depth <= expected_revert.depth && matches!(expected_revert.kind, ExpectedRevertKind::Default) { + // Mirror the logic in `call_end`: when an expected reverter address is set + // and we don't yet have one (or we're matching multiple reverts), record the + // would-be deployed address as the reverter. revm guarantees `outcome.address` + // is `Some(_)` whenever the constructor actually ran (including the revert + // case); it is only `None` for pre-frame rejection (depth/balance/nonce), + // for which a reverter address is meaningless. + if outcome.result.is_revert() + && expected_revert.reverter.is_some() + && (expected_revert.reverted_by.is_none() || expected_revert.count > 1) + && let Some(addr) = outcome.address + { + expected_revert.reverted_by = Some(addr); + } let mut expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); return match revert_handlers::handle_expect_revert( false, diff --git a/crates/cheatcodes/src/test/assert.rs b/crates/cheatcodes/src/test/assert.rs index 608f20f0c4e32..12d625768a0c9 100644 --- a/crates/cheatcodes/src/test/assert.rs +++ b/crates/cheatcodes/src/test/assert.rs @@ -164,7 +164,7 @@ impl EqRelAssertionError { format_units_uint(&f.left, decimals), format_units_uint(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } @@ -179,7 +179,7 @@ impl EqRelAssertionError { format_units_int(&f.left, decimals), format_units_int(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } diff --git a/crates/cheatcodes/src/version.rs b/crates/cheatcodes/src/version.rs index fb722c2814baa..2b8f81518a621 100644 --- a/crates/cheatcodes/src/version.rs +++ b/crates/cheatcodes/src/version.rs @@ -20,7 +20,14 @@ impl Cheatcode for foundryVersionAtLeastCall { } fn foundry_version_cmp(version: &str) -> Result { - version_cmp(SEMVER_VERSION.split('-').next().unwrap(), version) + version_cmp(strip_semver_metadata(SEMVER_VERSION), version) +} + +/// Strips pre-release (e.g. `-nightly`, `-dev`) and build metadata +/// (e.g. `+..`) from a version string +/// so we compare on `MAJOR.MINOR.PATCH` only. +fn strip_semver_metadata(version: &str) -> &str { + version.split(['-', '+']).next().unwrap() } fn version_cmp(version_a: &str, version_b: &str) -> Result { @@ -42,3 +49,61 @@ fn parse_version(version: &str) -> Result { } Ok(version) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_build_metadata_only() { + // Tagged release: `1.7.1+..` + assert_eq!(strip_semver_metadata("1.7.1+abc1234567.1737036656.release"), "1.7.1"); + } + + #[test] + fn strips_pre_release_and_build_metadata() { + // Nightly: `1.7.1-nightly+..` + assert_eq!(strip_semver_metadata("1.7.1-nightly+abc1234567.1737036656.release"), "1.7.1"); + // Dev: `1.7.1-dev+..` + assert_eq!(strip_semver_metadata("1.7.1-dev+abc1234567.1737036656.debug"), "1.7.1"); + } + + #[test] + fn strips_plain_version() { + assert_eq!(strip_semver_metadata("1.7.1"), "1.7.1"); + } + + #[test] + fn version_cmp_orders_correctly() { + assert_eq!(version_cmp("1.7.1", "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp("1.7.1", "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "1.7.2").unwrap(), Ordering::Less); + assert_eq!(version_cmp("1.7.1", "0.0.1").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "99.0.0").unwrap(), Ordering::Less); + } + + #[test] + fn parse_version_rejects_pre_release_and_build_metadata() { + // User-supplied versions must be plain `MAJOR.MINOR.PATCH`. + assert!(parse_version("1.7.1-nightly").is_err()); + assert!(parse_version("1.7.1+abc").is_err()); + assert!(parse_version("not-a-version").is_err()); + assert!(parse_version("1.7.1").is_ok()); + } + + #[test] + fn cmp_works_against_full_semver_version_strings() { + // Simulate comparing each shape of `SEMVER_VERSION` against a user-supplied version. + for current in [ + "1.7.1+abc1234567.1737036656.release", + "1.7.1-nightly+abc1234567.1737036656.release", + "1.7.1-dev+abc1234567.1737036656.debug", + "1.7.1", + ] { + let stripped = strip_semver_metadata(current); + assert_eq!(version_cmp(stripped, "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp(stripped, "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp(stripped, "1.7.2").unwrap(), Ordering::Less); + } + } +} diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index a9396b4208886..bb673c9219e10 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -49,7 +49,6 @@ itertools.workspace = true semver.workspace = true serde_json.workspace = true serde.workspace = true -solang-parser.workspace = true time = { version = "0.3", features = ["formatting"] } yansi.workspace = true tracing.workspace = true @@ -64,8 +63,13 @@ foundry-test-utils.workspace = true rexpect = "0.6" [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] tracy-allocator = ["foundry-cli/tracy-allocator"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index da2c7f4caff02..2ec057b8167c0 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -2,10 +2,7 @@ //! //! This module contains the execution logic for the [SessionSource]. -use crate::{ - prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}, - source::IntermediateOutput, -}; +use crate::prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_json_abi::EventParam; use alloy_primitives::{Address, B256, U256, hex}; @@ -15,7 +12,17 @@ use foundry_evm::{ backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder, inspectors::CheatsConfig, traces::TraceMode, }; -use solang_parser::pt; +use solar::{ + ast::{BinOpKind, ElementaryType, FunctionKind, LitKind, StateMutability, StrKind, UnOpKind}, + interface::Symbol, + sema::{ + hir::{ + ContractId, Event, Expr, ExprKind, Function, ItemId, Res, StmtKind, Type as HirType, + TypeKind, Visibility, + }, + ty::{Gcx, Ty, TyKind}, + }, +}; use std::ops::ControlFlow; use yansi::Paint; @@ -86,8 +93,10 @@ impl SessionSource { if let Some(err) = err { let output = source_without_inspector.build()?; - let formatted_event = - output.enter(|output| output.get_event(input).map(format_event_definition)); + let formatted_event = output.enter(|output| { + let gcx = output.gcx(); + output.get_event(input).map(|eid| format_event_definition(gcx, gcx.hir.event(eid))) + }); if let Some(formatted_event) = formatted_event { return Ok((ControlFlow::Break(()), Some(formatted_event?))); } @@ -122,30 +131,37 @@ impl SessionSource { // which was wrapped in `abi.encode`. let generated_output = source.build()?; - // If the expression is a variable declaration within the REPL contract, use its type; - // otherwise, attempt to infer the type. - let contract_expr = generated_output - .intermediate - .repl_contract_expressions - .get(input) - .or_else(|| source.infer_inner_expr_type()); + // Inside the compiler closure, infer the DynSolType of the inspected expression and + // determine whether the REPL should continue. + let res_ty = generated_output.enter(|out| -> Option<(bool, DynSolType)> { + let gcx = out.gcx(); - // If the current action is a function call, we get its return type - // otherwise it returns None - let function_call_return_type = - Type::get_function_return_type(contract_expr, &generated_output.intermediate); + // Try direct lookup of `input` as a named variable in the REPL contract. + if let Some(direct_ty) = lookup_named_variable_type(gcx, input) { + return Some((false, direct_ty)); + } - let (contract_expr, ty) = if let Some(function_call_return_type) = function_call_return_type - { - (function_call_return_type.0, function_call_return_type.1) - } else { - match contract_expr.and_then(|e| { - Type::ethabi(e, Some(&generated_output.intermediate)).map(|ty| (e, ty)) - }) { - Some(res) => res, - // this type was denied for inspection, continue - None => return Ok((ControlFlow::Continue(()), None)), + // Otherwise, find the appended `bytes memory inspectoor = abi.encode();` + // and pull out the first call argument. + let block = out.run_func_body(); + let last = block.last()?; + let StmtKind::DeclSingle(vid) = last.kind else { return None }; + let var = gcx.hir.variable(vid); + let init = var.initializer?; + let ExprKind::Call(_callee, args, _) = &init.kind else { return None }; + let inner_expr = args.exprs().next()?; + + // If the call is `func()` returning a single value, prefer the function return type. + if let Some(ty) = get_function_return_type(gcx, inner_expr) { + return Some((should_continue(inner_expr), ty)); } + + let ty = expr_to_dyn(gcx, inner_expr, true)?; + Some((should_continue(inner_expr), ty)) + }); + + let Some((cont, ty)) = res_ty else { + return Ok((ControlFlow::Continue(()), None)); }; // the file compiled correctly, thus the last stack item must be the memory offset of @@ -162,42 +178,10 @@ impl SessionSource { eyre::bail!("Failed to inspect last expression: could not retrieve data from memory") }; let token = ty.abi_decode(data).wrap_err("Could not decode inspected values")?; - let c = if should_continue(contract_expr) { - ControlFlow::Continue(()) - } else { - ControlFlow::Break(()) - }; + let c = if cont { ControlFlow::Continue(()) } else { ControlFlow::Break(()) }; Ok((c, Some(format_token(token)))) } - /// Gracefully attempts to extract the type of the expression within the `abi.encode(...)` - /// call inserted by the inspect function. - /// - /// ### Takes - /// - /// A reference to a [SessionSource] - /// - /// ### Returns - /// - /// Optionally, a [Type] - fn infer_inner_expr_type(&self) -> Option<&pt::Expression> { - let out = self.build().ok()?; - let run = out.run_func_body().ok()?.last(); - match run { - Some(pt::Statement::VariableDefinition( - _, - _, - Some(pt::Expression::FunctionCall(_, _, args)), - )) => { - // We can safely unwrap the first expression because this function - // will only be called on a session source that has just had an - // `inspectoor` variable appended to it. - Some(args.first().unwrap()) - } - _ => None, - } - } - async fn build_runner(&mut self, final_pc: usize) -> Result { let (evm_env, tx_env, fork_block) = self.config.evm_opts.env().await?; @@ -241,6 +225,51 @@ impl SessionSource { } } +/// Looks up `name` as a named variable in the REPL contract (state variables or run() locals) +/// and returns its type as a [`DynSolType`]. +/// +/// Only top-level statements of `run()` are scanned. Variables declared inside nested blocks +/// (`if`, `for`, `while`, `unchecked`, etc.) are not visible here; the caller falls back to +/// the `inspectoor`-based path for those cases. +fn lookup_named_variable_type(gcx: Gcx<'_>, name: &str) -> Option { + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + + // State variables. + for vid in repl.variables() { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + + // Locals declared in run(). + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + for stmt in body.stmts { + match stmt.kind { + StmtKind::DeclSingle(vid) => { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + StmtKind::DeclMulti(vids, _) => { + for vid in vids.iter().flatten() { + let var = hir.variable(*vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item((*vid).into())); + } + } + } + _ => {} + } + } + None +} + /// Formats a value into an inspection message // TODO: Verbosity option fn format_token(token: DynSolValue) -> String { @@ -343,49 +372,37 @@ fn format_token(token: DynSolValue) -> String { } } -/// Formats a [pt::EventDefinition] into an inspection message -/// -/// ### Takes -/// -/// An borrowed [pt::EventDefinition] -/// -/// ### Returns -/// -/// A formatted [pt::EventDefinition] for use in inspection output. +/// Formats an [`Event`] into an inspection message. // TODO: Verbosity option -fn format_event_definition(event_definition: &pt::EventDefinition) -> Result { - let event_name = event_definition.name.as_ref().expect("Event has a name").to_string(); - let inputs = event_definition - .fields +fn format_event_definition(gcx: Gcx<'_>, event: &Event<'_>) -> Result { + let event_name = event.name.as_str().to_string(); + let inputs = event + .parameters .iter() - .map(|param| { - let name = param - .name - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "".to_string()); - let kind = Type::from_expression(¶m.ty) - .and_then(Type::into_builtin) + .map(|&pid| { + let var = gcx.hir.variable(pid); + let name = + var.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "".into()); + let kind = solar_ty_to_dyn(gcx, gcx.type_of_item(pid.into())) .ok_or_else(|| eyre::eyre!("Invalid type in event {event_name}"))?; Ok(EventParam { name, ty: kind.to_string(), components: vec![], - indexed: param.indexed, + indexed: var.indexed, internal_type: None, }) }) .collect::>>()?; - let event = - alloy_json_abi::Event { name: event_name, inputs, anonymous: event_definition.anonymous }; + let event = alloy_json_abi::Event { name: event_name, inputs, anonymous: event.anonymous }; Ok(format!( "Type: {}\n├ Name: {}\n├ Signature: {:?}\n└ Selector: {:?}", "event".red(), SolidityHelper::new().highlight(&format!( "{}({})", - &event.name, - &event + event.name, + event .inputs .iter() .map(|param| format!( @@ -395,7 +412,7 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result>() @@ -411,844 +428,724 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result), - - /// (type, length) - FixedArray(Box, usize), +/// Converts an [`Expr`] directly to a [`DynSolType`] for ABI inspection. +/// +/// `lookup` controls whether user-defined type names are resolved via the HIR. +fn expr_to_dyn(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + match &expr.kind { + // Elementary type expression: `uint256`, `address`, etc. + ExprKind::Type(ty) => hir_ty_to_dyn(gcx, ty), + + // `type(T)`: only meaningful as the lhs of a member access. + ExprKind::TypeCall(_) => None, + + // Literals. + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Address(_) => Some(DynSolType::Address), + LitKind::Bool(_) => Some(DynSolType::Bool), + LitKind::Str(kind, _, _) => match kind { + StrKind::Hex => Some(DynSolType::Bytes), + StrKind::Str | StrKind::Unicode => Some(DynSolType::String), + }, + LitKind::Number(_) | LitKind::Rational(_) => Some(DynSolType::Uint(256)), + LitKind::Err(_) => None, + }, + + // Resolved identifier: `foo`. + ExprKind::Ident(reses) => { + let res = reses.first()?; + match *res { + Res::Item(ItemId::Variable(vid)) => { + solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) + } + Res::Item(ItemId::Struct(sid)) => { + // Struct reference used as a constructor produces a tuple of field types. + Some(DynSolType::Tuple( + gcx.struct_field_types(sid) + .iter() + .filter_map(|&t| solar_ty_to_dyn(gcx, t)) + .collect(), + )) + } + // Other items and builtins: handled by enclosing Call/Member expressions. + _ => None, + } + } - /// (type, index) - ArrayIndex(Box, Option), + // Index/access: `arr[i]`, `MyType[]`, `MyType[N]`. + ExprKind::Index(base, idx) => { + let base_ty = expr_to_dyn(gcx, base, lookup)?; + let num = + idx.and_then(|e| parse_number_literal(e)).and_then(|n| usize::try_from(n).ok()); + match &base.kind { + // Type-level indexing builds an array type expression. + ExprKind::Type(_) | ExprKind::TypeCall(_) => { + if let Some(n) = num { + Some(DynSolType::FixedArray(Box::new(base_ty), n)) + } else { + Some(DynSolType::Array(Box::new(base_ty))) + } + } + // Runtime indexing returns the element type. + _ => match base_ty { + DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => Some(*inner), + DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_) => { + Some(DynSolType::FixedBytes(1)) + } + other => Some(other), + }, + } + } - /// (types) - Tuple(Vec>), + // Slice: same type as the base. + ExprKind::Slice(base, _, _) => expr_to_dyn(gcx, base, lookup), - /// (name, params, returns) - Function(Box, Vec>, Vec>), + // Array literal `[a, b, c]`. + ExprKind::Array(values) => values + .first() + .and_then(|e| expr_to_dyn(gcx, e, lookup)) + .map(|ty| DynSolType::FixedArray(Box::new(ty), values.len())), - /// (lhs, rhs) - Access(Box, String), + // Tuple expression `(a, b, c)`. + ExprKind::Tuple(items) => Some(DynSolType::Tuple( + items.iter().filter_map(|opt| opt.and_then(|e| expr_to_dyn(gcx, e, lookup))).collect(), + )), - /// (types) - Custom(Vec), -} + // Member access `lhs.member`. + ExprKind::Member(_, _) => resolve_member(gcx, expr, lookup), -impl Type { - /// Convert a [pt::Expression] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Expression] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_expression(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::Type(_, ty) => Self::from_type(ty), - - pt::Expression::Variable(ident) => Some(Self::Custom(vec![ident.name.clone()])), - - // array - pt::Expression::ArraySubscript(_, expr, num) => { - // if num is Some then this is either an index operation (arr[]) - // or a FixedArray statement (new uint256[]) - Self::from_expression(expr).and_then(|ty| { - let boxed = Box::new(ty); - let num = num.as_deref().and_then(parse_number_literal).and_then(|n| { - usize::try_from(n).ok() - }); - match expr.as_ref() { - // statement - pt::Expression::Type(_, _) => { - if let Some(num) = num { - Some(Self::FixedArray(boxed, num)) - } else { - Some(Self::Array(boxed)) - } - } - // index - pt::Expression::Variable(_) => { - Some(Self::ArrayIndex(boxed, num)) - } - _ => None - } - }) - } - pt::Expression::ArrayLiteral(_, values) => { - values.first().and_then(Self::from_expression).map(|ty| { - Self::FixedArray(Box::new(ty), values.len()) - }) - } + // Function/constructor call. + ExprKind::Call(_, _, _) => resolve_call(gcx, expr, lookup), - // tuple - pt::Expression::List(_, params) => Some(Self::Tuple(map_parameters(params))), + // `new T`: produces a value of type T. + ExprKind::New(ty) => hir_ty_to_dyn(gcx, ty), - // . - pt::Expression::MemberAccess(_, lhs, rhs) => { - Self::from_expression(lhs).map(|lhs| { - Self::Access(Box::new(lhs), rhs.name.clone()) - }) - } + // `payable(addr)`. + ExprKind::Payable(_) => Some(DynSolType::Address), - // - pt::Expression::Parenthesis(_, inner) | // () - pt::Expression::New(_, inner) | // new - pt::Expression::UnaryPlus(_, inner) | // + - // ops - pt::Expression::BitwiseNot(_, inner) | // ~ - pt::Expression::ArraySlice(_, inner, _, _) | // [*start*:*end*] - // assign ops - pt::Expression::PreDecrement(_, inner) | // -- - pt::Expression::PostDecrement(_, inner) | // -- - pt::Expression::PreIncrement(_, inner) | // ++ - pt::Expression::PostIncrement(_, inner) | // ++ - pt::Expression::Assign(_, inner, _) | // = ... - pt::Expression::AssignAdd(_, inner, _) | // += ... - pt::Expression::AssignSubtract(_, inner, _) | // -= ... - pt::Expression::AssignMultiply(_, inner, _) | // *= ... - pt::Expression::AssignDivide(_, inner, _) | // /= ... - pt::Expression::AssignModulo(_, inner, _) | // %= ... - pt::Expression::AssignAnd(_, inner, _) | // &= ... - pt::Expression::AssignOr(_, inner, _) | // |= ... - pt::Expression::AssignXor(_, inner, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, inner, _) | // <<= ... - pt::Expression::AssignShiftRight(_, inner, _) // >>= ... - => Self::from_expression(inner), - - // *condition* ? : - pt::Expression::ConditionalOperator(_, _, if_true, if_false) => { - Self::from_expression(if_true).or_else(|| Self::from_expression(if_false)) - } + // Ternary: prefer truthy branch's type, fall back to else branch. + ExprKind::Ternary(_, t, e) => { + expr_to_dyn(gcx, t, lookup).or_else(|| expr_to_dyn(gcx, e, lookup)) + } - // address - pt::Expression::AddressLiteral(_, _) => Some(Self::Builtin(DynSolType::Address)), - pt::Expression::HexNumberLiteral(_, s, _) => { - match s.parse::
() { - Ok(addr) if *s == addr.to_checksum(None) => { - Some(Self::Builtin(DynSolType::Address)) + // Delete has no return type. + ExprKind::Delete(_) => None, + + // Unary operations. + ExprKind::Unary(op, inner) => match op.kind { + UnOpKind::Neg => expr_to_dyn(gcx, inner, lookup).map(|ty| match ty { + DynSolType::Uint(n) => DynSolType::Int(n), + DynSolType::Int(n) => DynSolType::Uint(n), + x => x, + }), + UnOpKind::Not => Some(DynSolType::Bool), + UnOpKind::BitNot + | UnOpKind::PreInc + | UnOpKind::PreDec + | UnOpKind::PostInc + | UnOpKind::PostDec => expr_to_dyn(gcx, inner, lookup), + }, + + // Binary operations. + ExprKind::Binary(lhs, op, rhs) => match op.kind { + BinOpKind::Lt + | BinOpKind::Le + | BinOpKind::Gt + | BinOpKind::Ge + | BinOpKind::Eq + | BinOpKind::Ne + | BinOpKind::And + | BinOpKind::Or => Some(DynSolType::Bool), + BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul | BinOpKind::Div => { + match (expr_to_dyn(gcx, lhs, false), expr_to_dyn(gcx, rhs, false)) { + (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) + | (Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { + Some(DynSolType::Int(256)) } - _ => Some(Self::Builtin(DynSolType::Uint(256))), + _ => Some(DynSolType::Uint(256)), } } + BinOpKind::Rem + | BinOpKind::Pow + | BinOpKind::BitAnd + | BinOpKind::BitOr + | BinOpKind::BitXor + | BinOpKind::Shl + | BinOpKind::Shr + | BinOpKind::Sar => Some(DynSolType::Uint(256)), + }, + + // Assignments: type of the lhs. + ExprKind::Assign(lhs, _, _) => expr_to_dyn(gcx, lhs, lookup), + + ExprKind::Err(_) => None, + } +} - // uint and int - // invert - pt::Expression::Negate(_, inner) => Self::from_expression(inner).map(Self::invert_int), - - // int if either operand is int - // TODO: will need an update for Solidity v0.8.18 user defined operators: - // https://github.com/ethereum/solidity/issues/13718#issuecomment-1341058649 - pt::Expression::Add(_, lhs, rhs) | - pt::Expression::Subtract(_, lhs, rhs) | - pt::Expression::Multiply(_, lhs, rhs) | - pt::Expression::Divide(_, lhs, rhs) => { - match (Self::ethabi(lhs, None), Self::ethabi(rhs, None)) { - (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) | -(Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { - Some(Self::Builtin(DynSolType::Int(256))) - } - _ => { - Some(Self::Builtin(DynSolType::Uint(256))) - } +/// Converts a [`HirType`] to a [`DynSolType`]. +fn hir_ty_to_dyn(gcx: Gcx<'_>, ty: &HirType<'_>) -> Option { + match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + TypeKind::Array(arr) => { + let elem = hir_ty_to_dyn(gcx, &arr.element)?; + if let Some(size) = arr.size { + let n = parse_number_literal(size).and_then(|n| usize::try_from(n).ok()); + if let Some(n) = n { + Some(DynSolType::FixedArray(Box::new(elem), n)) + } else { + Some(DynSolType::Array(Box::new(elem))) } + } else { + Some(DynSolType::Array(Box::new(elem))) } - - // always assume uint - pt::Expression::Modulo(_, _, _) | - pt::Expression::Power(_, _, _) | - pt::Expression::BitwiseOr(_, _, _) | - pt::Expression::BitwiseAnd(_, _, _) | - pt::Expression::BitwiseXor(_, _, _) | - pt::Expression::ShiftRight(_, _, _) | - pt::Expression::ShiftLeft(_, _, _) | - pt::Expression::NumberLiteral(_, _, _, _) => Some(Self::Builtin(DynSolType::Uint(256))), - - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(_, _, _, _, _) => { - Some(Self::Builtin(DynSolType::Uint(256))) + } + TypeKind::Function(f) => match f.returns.len() { + 0 => None, + 1 => { + let var = gcx.hir.variable(f.returns[0]); + hir_ty_to_dyn(gcx, &var.ty) } + _ => Some(DynSolType::Tuple( + f.returns + .iter() + .filter_map(|&pid| hir_ty_to_dyn(gcx, &gcx.hir.variable(pid).ty)) + .collect(), + )), + }, + TypeKind::Mapping(m) => hir_ty_to_dyn(gcx, &m.value), + TypeKind::Custom(item) => solar_ty_to_dyn(gcx, gcx.type_of_item(*item)), + TypeKind::Err(_) => None, + } +} - // bool - pt::Expression::BoolLiteral(_, _) | - pt::Expression::And(_, _, _) | - pt::Expression::Or(_, _, _) | - pt::Expression::Equal(_, _, _) | - pt::Expression::NotEqual(_, _, _) | - pt::Expression::Less(_, _, _) | - pt::Expression::LessEqual(_, _, _) | - pt::Expression::More(_, _, _) | - pt::Expression::MoreEqual(_, _, _) | - pt::Expression::Not(_, _) => Some(Self::Builtin(DynSolType::Bool)), - - // string - pt::Expression::StringLiteral(_) => Some(Self::Builtin(DynSolType::String)), - - // bytes - pt::Expression::HexLiteral(_) => Some(Self::Builtin(DynSolType::Bytes)), - - // function - pt::Expression::FunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(Self::from_expression).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } - pt::Expression::NamedFunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(|arg| Self::from_expression(&arg.expr)).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } +/// Resolves a member-access expression (`lhs.member`) to its [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Member`. +fn resolve_member(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Member(lhs, ident) = &expr.kind else { return None }; + let member = ident.name; + + // `type(T).member` — type introspection. + if let ExprKind::TypeCall(ty) = &lhs.kind { + return match member.as_str() { + "name" => Some(DynSolType::String), + "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), + "interfaceId" => Some(DynSolType::FixedBytes(4)), + // Only valid for integer types; custom types (enums) fall back to Uint(256). + "min" | "max" => match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + _ => Some(DynSolType::Uint(256)), + }, + _ => None, + }; + } - // explicitly None - pt::Expression::Delete(_, _) | pt::Expression::FunctionCallBlock(_, _, _) => None, - } + // Built-in namespace identifier: `block.timestamp`, `msg.sender`, `abi.encode`, etc. + if let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + && let Some(ty) = builtin_member(b.name().as_str(), member.as_str()) + { + return Some(ty); } - /// Convert a [pt::Type] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Type] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_type(ty: &pt::Type) -> Option { - let ty = match ty { - pt::Type::Address | pt::Type::AddressPayable | pt::Type::Payable => { - Self::Builtin(DynSolType::Address) - } - pt::Type::Bool => Self::Builtin(DynSolType::Bool), - pt::Type::String => Self::Builtin(DynSolType::String), - pt::Type::Int(size) => Self::Builtin(DynSolType::Int(*size as usize)), - pt::Type::Uint(size) => Self::Builtin(DynSolType::Uint(*size as usize)), - pt::Type::Bytes(size) => Self::Builtin(DynSolType::FixedBytes(*size as usize)), - pt::Type::DynamicBytes => Self::Builtin(DynSolType::Bytes), - pt::Type::Mapping { value, .. } => Self::from_expression(value)?, - pt::Type::Function { params, returns, .. } => { - let params = map_parameters(params); - let returns = returns - .as_ref() - .map(|(returns, _)| map_parameters(returns)) - .unwrap_or_default(); - Self::Function( - Box::new(Self::Custom(vec!["__fn_type__".to_string()])), - params, - returns, - ) - } - // TODO: Rational numbers - pt::Type::Rational => return None, + // Elementary type used as a namespace: `address.balance`, `bytes.concat`, etc. + if let ExprKind::Type(ty) = &lhs.kind + && let TypeKind::Elementary(et) = &ty.kind + { + return match et { + ElementaryType::Address(_) => match member.as_str() { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + ElementaryType::Bytes => match member.as_str() { + "concat" => Some(DynSolType::Bytes), + _ => None, + }, + ElementaryType::String => match member.as_str() { + "concat" => Some(DynSolType::String), + _ => None, + }, + _ => None, }; - Some(ty) } - /// Handle special expressions like [global variables](https://docs.soliditylang.org/en/latest/cheatsheet.html#global-variables) - /// - /// See: - fn map_special(self) -> Self { - if !matches!(self, Self::Function(_, _, _) | Self::Access(_, _) | Self::Custom(_)) { - return self; - } + // Members on a resolved DynSolType (`.length`, `.pop`, `.selector`, `.address`). + if let Some(lhs_ty) = expr_to_dyn(gcx, lhs, lookup) + && let Some(ty) = dyn_member(&lhs_ty, member.as_str()) + { + return Some(ty); + } - let mut types = Vec::with_capacity(5); - let mut args = None; - self.recurse(&mut types, &mut args); + // HIR lookup for user-defined type members. + if lookup && let Some(mut chain) = expr_name_chain(gcx, lhs) { + chain.insert(0, member); + return infer_custom_type(gcx, &mut chain, None).ok().flatten(); + } - let len = types.len(); - if len == 0 { - return self; - } + None +} + +/// Returns the type of `builtin_ns.member` for built-in global namespaces. +fn builtin_member(builtin: &str, member: &str) -> Option { + match builtin { + "block" => match member { + "coinbase" => Some(DynSolType::Address), + "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" | "chainid" + | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), + _ => None, + }, + "msg" => match member { + "sender" => Some(DynSolType::Address), + "gas" | "value" => Some(DynSolType::Uint(256)), + "data" => Some(DynSolType::Bytes), + "sig" => Some(DynSolType::FixedBytes(4)), + _ => None, + }, + "tx" => match member { + "origin" => Some(DynSolType::Address), + "gasprice" => Some(DynSolType::Uint(256)), + _ => None, + }, + "address" => match member { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + _ => None, + } +} + +/// Returns the type of `ty.member` for a known [`DynSolType`]. +fn dyn_member(ty: &DynSolType, member: &str) -> Option { + match member { + "length" => match ty { + DynSolType::Array(_) + | DynSolType::FixedArray(_, _) + | DynSolType::Bytes + | DynSolType::String + | DynSolType::FixedBytes(_) => Some(DynSolType::Uint(256)), + _ => None, + }, + "pop" => match ty { + DynSolType::Array(inner) => Some(*inner.clone()), + _ => None, + }, + // Address members. + "balance" => match ty { + DynSolType::Address => Some(DynSolType::Uint(256)), + _ => None, + }, + "code" => match ty { + DynSolType::Address => Some(DynSolType::Bytes), + _ => None, + }, + "codehash" => match ty { + DynSolType::Address => Some(DynSolType::FixedBytes(32)), + _ => None, + }, + "send" => match ty { + DynSolType::Address => Some(DynSolType::Bool), + _ => None, + }, + // External function members. + "selector" => Some(DynSolType::FixedBytes(4)), + "address" => Some(DynSolType::Address), + _ => None, + } +} - // Type members, like array, bytes etc - #[expect(clippy::single_match)] - #[allow(clippy::collapsible_match)] - match &self { - Self::Access(inner, access) => { - if let Some(ty) = inner.as_ref().clone().try_as_ethabi(None) { - // Array / bytes members - let ty = Self::Builtin(ty); - match access.as_str() { - "length" if ty.is_dynamic() || ty.is_array() || ty.is_fixed_bytes() => { - return Self::Builtin(DynSolType::Uint(256)); +/// Resolves a call expression to its return [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Call`. +fn resolve_call(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Call(callee, args, _named) = &expr.kind else { return None }; + + // Type cast: `uint256(x)`, `address(y)`, etc. + if let ExprKind::Type(ty) = &callee.kind { + return hir_ty_to_dyn(gcx, ty); + } + + // Member call: `ns.method(...)`. + if let ExprKind::Member(lhs, method) = &callee.kind + && let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + { + match b.name().as_str() { + "abi" => { + return match method.as_str() { + "decode" => { + let last = args.exprs().last()?; + match expr_to_dyn(gcx, last, false)? { + DynSolType::Tuple(tys) => Some(DynSolType::Tuple(tys)), + ty => Some(DynSolType::Tuple(vec![ty])), } - "pop" if ty.is_dynamic_array() => return ty, - _ => {} } - } + s if s.starts_with("encode") => Some(DynSolType::Bytes), + _ => None, + }; } + "string" if method.as_str() == "concat" => return Some(DynSolType::String), + "bytes" if method.as_str() == "concat" => return Some(DynSolType::Bytes), _ => {} } + } - let this = { - let name = types.last().unwrap().as_str(); - match len { - 0 => unreachable!(), - 1 => match name { + // Simple identifier call: built-in global functions and HIR function calls. + if let ExprKind::Ident(reses) = &callee.kind { + match reses.first() { + Some(Res::Builtin(b)) => { + return match b.name().as_str() { "gasleft" | "addmod" | "mulmod" => Some(DynSolType::Uint(256)), "keccak256" | "sha256" | "blockhash" => Some(DynSolType::FixedBytes(32)), "ripemd160" => Some(DynSolType::FixedBytes(20)), "ecrecover" => Some(DynSolType::Address), _ => None, - }, - 2 => { - let access = types.first().unwrap().as_str(); - match name { - "block" => match access { - "coinbase" => Some(DynSolType::Address), - "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" - | "chainid" | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), - _ => None, - }, - "msg" => match access { - "sender" => Some(DynSolType::Address), - "gas" => Some(DynSolType::Uint(256)), - "value" => Some(DynSolType::Uint(256)), - "data" => Some(DynSolType::Bytes), - "sig" => Some(DynSolType::FixedBytes(4)), - _ => None, - }, - "tx" => match access { - "origin" => Some(DynSolType::Address), - "gasprice" => Some(DynSolType::Uint(256)), - _ => None, - }, - "abi" => match access { - "decode" => { - // args = Some([Bytes(_), Tuple(args)]) - // unwrapping is safe because this is first compiled by solc so - // it is guaranteed to be a valid call - let mut args = args.unwrap(); - let last = args.pop().unwrap(); - match last { - Some(ty) => { - return match ty { - Self::Tuple(_) => ty, - ty => Self::Tuple(vec![Some(ty)]), - }; - } - None => None, - } - } - s if s.starts_with("encode") => Some(DynSolType::Bytes), - _ => None, - }, - "address" => match access { - "balance" => Some(DynSolType::Uint(256)), - "code" => Some(DynSolType::Bytes), - "codehash" => Some(DynSolType::FixedBytes(32)), - "send" => Some(DynSolType::Bool), - _ => None, - }, - "type" => match access { - "name" => Some(DynSolType::String), - "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), - "interfaceId" => Some(DynSolType::FixedBytes(4)), - "min" | "max" => Some( - // Either a builtin or an enum - (|| args?.pop()??.into_builtin())() - .unwrap_or(DynSolType::Uint(256)), - ), - _ => None, - }, - "string" => match access { - "concat" => Some(DynSolType::String), - _ => None, - }, - "bytes" => match access { - "concat" => Some(DynSolType::Bytes), - _ => None, - }, - _ => None, - } - } - _ => None, - } - }; - - this.map(Self::Builtin).unwrap_or_else(|| match types.last().unwrap().as_str() { - "this" | "super" => Self::Custom(types), - _ => match self { - Self::Custom(_) | Self::Access(_, _) => Self::Custom(types), - Self::Function(_, _, _) => self, - _ => unreachable!(), - }, - }) - } - - /// Recurses over itself, appending all the idents and function arguments in the order that they - /// are found - fn recurse(&self, types: &mut Vec, args: &mut Option>>) { - match self { - Self::Builtin(ty) => types.push(ty.to_string()), - Self::Custom(tys) => types.extend(tys.clone()), - Self::Access(expr, name) => { - types.push(name.clone()); - expr.recurse(types, args); + }; } - Self::Function(fn_name, fn_args, _fn_ret) => { - if args.is_none() && !fn_args.is_empty() { - *args = Some(fn_args.clone()); + Some(Res::Item(ItemId::Function(fid))) if lookup => { + let func = gcx.hir.function(*fid); + if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) { + return None; } - fn_name.recurse(types, args); + let ret_id = *func.returns.first()?; + return solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())); } _ => {} } } - /// Infers a custom type's true type by recursing up the parse tree - /// - /// ### Takes - /// - A reference to the [IntermediateOutput] - /// - An array of custom types generated by the `MemberAccess` arm of [Self::from_expression] - /// - An optional contract name. This should always be `None` when this function is first - /// called. - /// - /// ### Returns - /// - /// If successful, an `Ok(Some(DynSolType))` variant. - /// If gracefully failed, an `Ok(None)` variant. - /// If failed, an `Err(e)` variant. - fn infer_custom_type( - intermediate: &IntermediateOutput, - custom_type: &mut Vec, - contract_name: Option, - ) -> Result> { - if let Some("this" | "super") = custom_type.last().map(String::as_str) { - custom_type.pop(); + // Fall back to the callee's resolved type. + expr_to_dyn(gcx, callee, lookup) +} + +/// Extracts a name chain from a member-access expression tree for HIR lookup. +/// +/// The chain is ordered outermost-first so `a.b.c` produces `["c", "b", "a"]` with the root +/// identifier at the back. This matches the convention expected by [`infer_custom_type`]. +fn expr_name_chain(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option> { + match &expr.kind { + ExprKind::Ident(reses) => { + let res = reses.first()?; + let name = match *res { + Res::Item(ItemId::Variable(vid)) => gcx.hir.variable(vid).name?.name, + Res::Item(ItemId::Function(fid)) => gcx.hir.function(fid).name?.name, + Res::Item(ItemId::Contract(cid)) => gcx.hir.contract(cid).name.name, + Res::Builtin(b) => b.name(), + _ => return None, + }; + Some(vec![name]) } - if custom_type.is_empty() { - return Ok(None); + ExprKind::Member(lhs, ident) => { + let mut chain = expr_name_chain(gcx, lhs)?; + chain.insert(0, ident.name); + Some(chain) } + _ => None, + } +} - // If a contract exists with the given name, check its definitions for a match. - // Otherwise look in the `run` - if let Some(contract_name) = contract_name { - let intermediate_contract = intermediate - .intermediate_contracts - .get(&contract_name) - .ok_or_else(|| eyre::eyre!("Could not find intermediate contract!"))?; - - let cur_type = custom_type.last().unwrap(); - if let Some(func) = intermediate_contract.function_definitions.get(cur_type) { - // Check if the custom type is a function pointer member access - if let res @ Some(_) = func_members(func, custom_type) { - return Ok(res); - } - - // Because tuple types cannot be passed to `abi.encode`, we will only be - // receiving functions that have 0 or 1 return parameters here. - if func.returns.is_empty() { - eyre::bail!( - "This call expression does not return any values to inspect. Insert as statement." - ) - } +/// Infers a custom type's true type by recursing through the HIR. +/// +/// `custom_type` is a name chain ordered outermost-first (root at back). This is mutated during +/// resolution. `contract_id` narrows the search to a specific contract scope. +fn infer_custom_type( + gcx: Gcx<'_>, + custom_type: &mut Vec, + contract_id: Option, +) -> Result> { + if let Some(last) = custom_type.last() + && (last.as_str() == "this" || last.as_str() == "super") + { + custom_type.pop(); + } + if custom_type.is_empty() { + return Ok(None); + } - // Empty return types check is done above - let (_, param) = func.returns.first().unwrap(); - // Return type should always be present - let return_ty = ¶m.as_ref().unwrap().ty; - - // If the return type is a variable (not a type expression), re-enter the recursion - // on the same contract for a variable / struct search. It could be a contract, - // struct, array, etc. - if let pt::Expression::Variable(ident) = return_ty { - custom_type.push(ident.name.clone()); - return Self::infer_custom_type(intermediate, custom_type, Some(contract_name)); - } + if let Some(cid) = contract_id { + let hir = &gcx.hir; + let contract = hir.contract(cid); - // Check if our final function call alters the state. If it does, we bail so that it - // will be inserted normally without inspecting. If the state mutability was not - // expressly set, the function is inferred to alter state. - if let Some(pt::FunctionAttribute::Mutability(_mut)) = func - .attributes - .iter() - .find(|attr| matches!(attr, pt::FunctionAttribute::Mutability(_))) - { - if let pt::Mutability::Payable(_) = _mut { - eyre::bail!("This function mutates state. Insert as a statement.") - } - } else { - eyre::bail!("This function mutates state. Insert as a statement.") - } + let cur_name = *custom_type.last().unwrap(); + let cur = cur_name.as_str(); - Ok(Self::ethabi(return_ty, Some(intermediate))) - } else if let Some(var) = intermediate_contract.variable_definitions.get(cur_type) { - Self::infer_var_expr(&var.ty, Some(intermediate), custom_type) - } else if let Some(strukt) = intermediate_contract.struct_definitions.get(cur_type) { - let inner_types = strukt - .fields - .iter() - .map(|var| { - Self::ethabi(&var.ty, Some(intermediate)) - .ok_or_else(|| eyre::eyre!("Struct `{cur_type}` has invalid fields")) - }) - .collect::>>()?; - Ok(Some(DynSolType::Tuple(inner_types))) - } else { - eyre::bail!( - "Could not find any definition in contract \"{contract_name}\" for type: {custom_type:?}" - ) - } - } else { - // Check if the custom type is a variable or function within the REPL contract before - // anything. If it is, we can stop here. - if let Ok(res) = Self::infer_custom_type(intermediate, custom_type, Some("REPL".into())) - { + // Function? + if let Some(fid) = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + let func = hir.function(fid); + if let res @ Some(_) = func_members(func, custom_type) { return Ok(res); } - // Check if the first element of the custom type is a known contract. If it is, begin - // our recursion on that contract's definitions. - let name = custom_type.last().unwrap(); - let contract = intermediate.intermediate_contracts.get(name); - if contract.is_some() { - let contract_name = custom_type.pop(); - return Self::infer_custom_type(intermediate, custom_type, contract_name); + if func.returns.is_empty() { + eyre::bail!( + "This call expression does not return any values to inspect. Insert as statement." + ) } - // See [`Type::infer_var_expr`] - let name = custom_type.last().unwrap(); - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - return Self::infer_var_expr(expr, Some(intermediate), custom_type); + let sm = func.state_mutability; + if !matches!(sm, StateMutability::View | StateMutability::Pure) { + eyre::bail!("This function mutates state. Insert as a statement.") } - // The first element of our custom type was neither a variable or a function within the - // REPL contract, move on to globally available types gracefully. - Ok(None) + let ret_id = func.returns[0]; + let ret_var = hir.variable(ret_id); + return Ok(solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) + .or_else(|| hir_ty_to_dyn(gcx, &ret_var.ty))); } - } - /// Infers the type from a variable's type - fn infer_var_expr( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - custom_type: &mut Vec, - ) -> Result> { - // Resolve local (in `run` function) or global (in the `REPL` or other contract) variable - let res = match &expr { - // Custom variable handling - pt::Expression::Variable(ident) => { - let name = &ident.name; - - if let Some(intermediate) = intermediate { - // expression in `run` - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - Self::infer_var_expr(expr, Some(intermediate), custom_type) - } else if intermediate.intermediate_contracts.contains_key(name) { - if custom_type.len() > 1 { - // There is still some recursing left to do: jump into the contract. - custom_type.pop(); - Self::infer_custom_type(intermediate, custom_type, Some(name.clone())) - } else { - // We have no types left to recurse: return the address of the contract. - Ok(Some(DynSolType::Address)) - } - } else { - Err(eyre::eyre!("Could not infer variable type")) - } - } else { - Ok(None) - } - } - other_expr => Ok(Self::ethabi(other_expr, intermediate)), - }; - // re-run everything with the resolved variable in case we're accessing a builtin member - // for example array or bytes length etc - match res { - Ok(Some(ty)) => { - let box_ty = Box::new(Self::Builtin(ty.clone())); - let access = Self::Access(box_ty, custom_type.drain(..).next().unwrap_or_default()); - if let Some(mapped) = access.map_special().try_as_ethabi(intermediate) { - Ok(Some(mapped)) - } else { - Ok(Some(ty)) + // Variable? + if let Some(vid) = contract + .variables() + .find(|&v| hir.variable(v).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + if let Some(ty) = solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) { + custom_type.pop(); + if custom_type.is_empty() { + return Ok(Some(ty)); } + let next_member = custom_type.drain(..).next().unwrap_or(Symbol::DUMMY); + return Ok(dyn_member(&ty, next_member.as_str()).or(Some(ty))); } - res => res, - } - } - - /// Attempt to convert this type into a [DynSolType] - /// - /// ### Takes - /// An immutable reference to an [IntermediateOutput] - /// - /// ### Returns - /// Optionally, a [DynSolType] - fn try_as_ethabi(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - Self::Tuple(types) => Some(DynSolType::Tuple(types_to_parameters(types, intermediate))), - Self::Array(inner) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::Array(Box::new(inner))), - }, - Self::FixedArray(inner, size) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::FixedArray(Box::new(inner), size)), - }, - ty @ Self::ArrayIndex(_, _) => ty.into_array_index(intermediate), - Self::Function(ty, _, _) => ty.try_as_ethabi(intermediate), - // should have been mapped to `Custom` in previous steps - Self::Access(_, _) => None, - Self::Custom(mut types) => { - // Cover any local non-state-modifying function call expressions - intermediate.and_then(|intermediate| { - Self::infer_custom_type(intermediate, &mut types, None).ok().flatten() - }) - } + let var = hir.variable(vid); + return infer_var_ty(gcx, &var.ty, custom_type); } - } - - /// Equivalent to `Type::from_expression` + `Type::map_special` + `Type::try_as_ethabi` - fn ethabi( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - ) -> Option { - Self::from_expression(expr) - .map(Self::map_special) - .and_then(|ty| ty.try_as_ethabi(intermediate)) - } - /// Get the return type of a function call expression. - fn get_function_return_type<'a>( - contract_expr: Option<&'a pt::Expression>, - intermediate: &IntermediateOutput, - ) -> Option<(&'a pt::Expression, DynSolType)> { - let function_call = match contract_expr? { - pt::Expression::FunctionCall(_, function_call, _) => function_call, - _ => return None, - }; - let (contract_name, function_name) = match function_call.as_ref() { - pt::Expression::MemberAccess(_, contract_name, function_name) => { - (contract_name, function_name) + // Struct? + if let Some(sid) = contract.items.iter().find_map(|i| { + if let ItemId::Struct(sid) = i + && hir.strukt(*sid).name.as_str() == cur + { + Some(*sid) + } else { + None } - _ => return None, - }; - let contract_name = match contract_name.as_ref() { - pt::Expression::Variable(contract_name) => contract_name.to_owned(), - _ => return None, - }; - - let pt::Expression::Variable(contract_name) = - intermediate.repl_contract_expressions.get(&contract_name.name)? - else { - return None; - }; - - let contract = intermediate - .intermediate_contracts - .get(&contract_name.name)? - .function_definitions - .get(&function_name.name)?; - let return_parameter = contract.as_ref().returns.first()?.to_owned().1?; - Self::ethabi(&return_parameter.ty, Some(intermediate)).map(|p| (contract_expr.unwrap(), p)) - } - - /// Inverts Int to Uint and vice-versa. - fn invert_int(self) -> Self { - match self { - Self::Builtin(DynSolType::Uint(n)) => Self::Builtin(DynSolType::Int(n)), - Self::Builtin(DynSolType::Int(n)) => Self::Builtin(DynSolType::Uint(n)), - x => x, + }) { + let inner = gcx + .struct_field_types(sid) + .iter() + .map(|&t| { + solar_ty_to_dyn(gcx, t) + .ok_or_else(|| eyre::eyre!("Struct `{cur}` has invalid fields")) + }) + .collect::>>()?; + return Ok(Some(DynSolType::Tuple(inner))); } - } - /// Returns the `DynSolType` contained by `Type::Builtin` - #[inline] - fn into_builtin(self) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - _ => None, - } + eyre::bail!( + "Could not find any definition in contract \"{}\" for type: {custom_type:?}", + contract.name.as_str() + ) } - /// Returns the resulting `DynSolType` of indexing self - fn into_array_index(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Array(inner) | Self::FixedArray(inner, _) | Self::ArrayIndex(inner, _) => { - match inner.try_as_ethabi(intermediate) { - Some(DynSolType::Array(inner) | DynSolType::FixedArray(inner, _)) => { - Some(*inner) - } - Some(DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_)) => { - Some(DynSolType::FixedBytes(1)) - } - ty => ty, - } - } - _ => None, - } + let repl_id = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == "REPL").then_some(cid)); + if let Some(repl_id) = repl_id + && let Ok(res) = infer_custom_type(gcx, custom_type, Some(repl_id)) + { + return Ok(res); } - /// Returns whether this type is dynamic - #[inline] - const fn is_dynamic(&self) -> bool { - match self { - // TODO: Note, this is not entirely correct. Fixed arrays of non-dynamic types are - // not dynamic, nor are tuples of non-dynamic types. - Self::Builtin(DynSolType::Bytes | DynSolType::String | DynSolType::Array(_)) => true, - Self::Array(_) => true, - _ => false, - } + let last_name = *custom_type.last().unwrap(); + let last = last_name.as_str(); + let contract_match = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == last).then_some(cid)); + if let Some(cid) = contract_match { + custom_type.pop(); + return infer_custom_type(gcx, custom_type, Some(cid)); } - /// Returns whether this type is an array - #[inline] - const fn is_array(&self) -> bool { - matches!( - self, - Self::Array(_) - | Self::FixedArray(_, _) - | Self::Builtin(DynSolType::Array(_) | DynSolType::FixedArray(_, _)) - ) - } + Ok(None) +} - /// Returns whether this type is a dynamic array (can call push, pop) - #[inline] - const fn is_dynamic_array(&self) -> bool { - matches!(self, Self::Array(_) | Self::Builtin(DynSolType::Array(_))) +/// Infers the type from a variable's HIR type, optionally accessing a named member. +fn infer_var_ty( + gcx: Gcx<'_>, + ty: &HirType<'_>, + custom_type: &mut Vec, +) -> Result> { + let Some(ty) = hir_ty_to_dyn(gcx, ty) else { return Ok(None) }; + let next_member = custom_type.drain(..).next(); + if let Some(m) = next_member { + Ok(dyn_member(&ty, m.as_str()).or(Some(ty))) + } else { + Ok(Some(ty)) } +} - const fn is_fixed_bytes(&self) -> bool { - matches!(self, Self::Builtin(DynSolType::FixedBytes(_))) - } +/// Get the return type of a contract method call `receiver.method()`. +fn get_function_return_type(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option { + let ExprKind::Call(callee, _, _) = &expr.kind else { return None }; + let ExprKind::Member(obj, fn_ident) = &callee.kind else { return None }; + let ExprKind::Ident(reses) = &obj.kind else { return None }; + let res = reses.first()?; + let var_id = match res { + Res::Item(ItemId::Variable(vid)) => *vid, + _ => return None, + }; + let var_ty = gcx.type_of_item(var_id.into()).peel_refs(); + let cid = match var_ty.kind { + TyKind::Contract(cid) => cid, + _ => return None, + }; + + let hir = &gcx.hir; + let contract = hir.contract(cid); + let fid = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some(fn_ident.as_str()))?; + let func = hir.function(fid); + let ret_id = *func.returns.first()?; + solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) } -/// Returns Some if the custom type is a function member access +/// Returns Some if the custom type is a function member access. /// /// Ref: #[inline] -fn func_members(func: &pt::FunctionDefinition, custom_type: &[String]) -> Option { - if !matches!(func.ty, pt::FunctionTy::Function) { +fn func_members(func: &Function<'_>, custom_type: &[Symbol]) -> Option { + if !matches!(func.kind, FunctionKind::Function) { return None; } - - let vis = func.attributes.iter().find_map(|attr| match attr { - pt::FunctionAttribute::Visibility(vis) => Some(vis), - _ => None, - }); - match vis { - Some(pt::Visibility::External(_) | pt::Visibility::Public(_)) => { - match custom_type.first().unwrap().as_str() { - "address" => Some(DynSolType::Address), - "selector" => Some(DynSolType::FixedBytes(4)), - _ => None, - } - } + if !matches!(func.visibility, Visibility::External | Visibility::Public) { + return None; + } + match custom_type.first().unwrap().as_str() { + "address" => Some(DynSolType::Address), + "selector" => Some(DynSolType::FixedBytes(4)), _ => None, } } -/// Whether execution should continue after inspecting this expression +/// Whether execution should continue after inspecting this expression. #[inline] -fn should_continue(expr: &pt::Expression) -> bool { - match expr { - // assignments - pt::Expression::PreDecrement(_, _) | // -- - pt::Expression::PostDecrement(_, _) | // -- - pt::Expression::PreIncrement(_, _) | // ++ - pt::Expression::PostIncrement(_, _) | // ++ - pt::Expression::Assign(_, _, _) | // = ... - pt::Expression::AssignAdd(_, _, _) | // += ... - pt::Expression::AssignSubtract(_, _, _) | // -= ... - pt::Expression::AssignMultiply(_, _, _) | // *= ... - pt::Expression::AssignDivide(_, _, _) | // /= ... - pt::Expression::AssignModulo(_, _, _) | // %= ... - pt::Expression::AssignAnd(_, _, _) | // &= ... - pt::Expression::AssignOr(_, _, _) | // |= ... - pt::Expression::AssignXor(_, _, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, _, _) | // <<= ... - pt::Expression::AssignShiftRight(_, _, _) // >>= ... - => { - true - } - +fn should_continue(expr: &Expr<'_>) -> bool { + match &expr.kind { + // assignments and compound assignments + ExprKind::Assign(_, _, _) => true, + // ++/-- pre/post operations + ExprKind::Unary(op, _) => matches!( + op.kind, + UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec + ), // Array.pop() - pt::Expression::FunctionCall(_, lhs, _) => { - match lhs.as_ref() { - pt::Expression::MemberAccess(_, _inner, access) => access.name == "pop", - _ => false - } - } - - _ => false + ExprKind::Call(callee, _, _) => match &callee.kind { + ExprKind::Member(_, ident) => ident.as_str() == "pop", + _ => false, + }, + _ => false, } } -fn map_parameters(params: &[(pt::Loc, Option)]) -> Vec> { - params - .iter() - .map(|(_, param)| param.as_ref().and_then(|param| Type::from_expression(¶m.ty))) - .collect() +/// Parses an [`Expr`] number/hex literal into a `U256`. Returns `None` if the expression +/// is not a numeric literal. +/// +/// SubDenominations are already applied to numeric literals in solar's HIR. +const fn parse_number_literal(expr: &Expr<'_>) -> Option { + match &expr.kind { + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Number(n) => Some(*n), + _ => None, + }, + _ => None, + } } -fn types_to_parameters( - types: Vec>, - intermediate: Option<&IntermediateOutput>, -) -> Vec { - types.into_iter().filter_map(|ty| ty.and_then(|ty| ty.try_as_ethabi(intermediate))).collect() +/// Maps a solar [`ElementaryType`] to a [`DynSolType`]. +const fn elementary_to_dyn(et: ElementaryType) -> Option { + Some(match et { + ElementaryType::Address(_) => DynSolType::Address, + ElementaryType::Bool => DynSolType::Bool, + ElementaryType::String => DynSolType::String, + ElementaryType::Bytes => DynSolType::Bytes, + ElementaryType::Int(size) => DynSolType::Int(size.bits() as usize), + ElementaryType::UInt(size) => DynSolType::Uint(size.bits() as usize), + ElementaryType::FixedBytes(size) => DynSolType::FixedBytes(size.bytes() as usize), + // Fixed-point numbers are not yet representable as DynSolType. + ElementaryType::Fixed(_, _) | ElementaryType::UFixed(_, _) => return None, + }) } -fn parse_number_literal(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::NumberLiteral(_, num, exp, unit) => { - let num = num.parse::().unwrap_or(U256::ZERO); - let exp = exp.parse().unwrap_or(0u32); - if exp > 77 { - None +/// Maps a solar [`Ty`] to a [`DynSolType`]. +fn solar_ty_to_dyn<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> Option { + match ty.kind { + TyKind::Elementary(et) => elementary_to_dyn(et), + TyKind::Ref(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Array(elem, n) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + let size: usize = n.try_into().ok()?; + Some(DynSolType::FixedArray(Box::new(inner), size)) + } + TyKind::DynArray(elem) | TyKind::Slice(elem) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + Some(DynSolType::Array(Box::new(inner))) + } + TyKind::Tuple(tys) => { + Some(DynSolType::Tuple(tys.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect())) + } + TyKind::Mapping(_, _) => None, + TyKind::Struct(sid) => Some(DynSolType::Tuple( + gcx.struct_field_types(sid).iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + TyKind::Enum(_) => Some(DynSolType::Uint(8)), + TyKind::Udvt(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Contract(_) => Some(DynSolType::Address), + // For a function-pointer type we return the ABI type of what the call *produces*, not a + // representation of the pointer itself. This is intentional: chisel inspects values, so + // the interesting type is the returned value. A zero-return function pointer has no + // inspectable value, so we return `None`. + TyKind::FnPtr(f) => match f.returns.len() { + 0 => None, + 1 => solar_ty_to_dyn(gcx, f.returns[0]), + _ => Some(DynSolType::Tuple( + f.returns.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + }, + TyKind::Type(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::Meta(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::IntLiteral(neg, size) => { + let bits = (size.bits() as usize).max(8); + // Round up to the nearest multiple of 8 bits, capped at 256. + let bits = bits.div_ceil(8) * 8; + let bits = bits.min(256); + if neg { + Some(DynSolType::Int(bits.max(8))) } else { - let exp = U256::from(10usize.pow(exp)); - let unit_mul = unit_multiplier(unit).ok()?; - Some(num * exp * unit_mul) + Some(DynSolType::Uint(bits.max(8))) } } - pt::Expression::HexNumberLiteral(_, num, unit) => { - let unit_mul = unit_multiplier(unit).ok()?; - num.parse::().map(|num| num * unit_mul).ok() + TyKind::StringLiteral(valid_utf8, _) => { + if valid_utf8 { + Some(DynSolType::String) + } else { + Some(DynSolType::Bytes) + } } - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(..) => None, + TyKind::Module(_) + | TyKind::BuiltinModule(_) + | TyKind::Error(_, _) + | TyKind::Event(_, _) + | TyKind::Err(_) => None, _ => None, } } -#[inline] -fn unit_multiplier(unit: &Option) -> Result { - if let Some(unit) = unit { - let mul = match unit.name.as_str() { - "seconds" => 1, - "minutes" => 60, - "hours" => 60 * 60, - "days" => 60 * 60 * 24, - "weeks" => 60 * 60 * 24 * 7, - "wei" => 1, - "gwei" => 10_usize.pow(9), - "ether" => 10_usize.pow(18), - other => eyre::bail!("unknown unit: {other}"), - }; - Ok(U256::from(mul)) - } else { - Ok(U256::from(1)) - } -} - #[cfg(test)] mod tests { use super::*; use foundry_compilers::{error::SolcError, solc::Solc}; + use solar::sema::Compiler; use std::sync::Mutex; #[test] @@ -1558,46 +1455,66 @@ mod tests { DynSolType::FixedArray(Box::new(ty), len) } - fn parse(s: &mut SessionSource, input: &str, clear: bool) -> IntermediateOutput { + /// Lowers the given snippet appended to the REPL contract via solar's HIR pipeline (without + /// invoking solc) and returns the resulting `DynSolType` of the last expression statement in + /// the run() body. + /// + /// Tests bypass `SessionSource::build` (which routes through foundry-compilers + solc) so that + /// inputs which are syntactically valid but semantically rejected by solc (e.g. + /// `abi.decode(bytes, (uint8[13]))` or `a[0:3]` on a memory array) can still exercise the + /// HIR-based type-inference engine. + fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { if clear { s.clear(); } + // Always declare a sample enum so `Enum1` is available for `type(Enum1)` tests. *s = s.clone_with_new_line("enum Enum1 { A }".into()).unwrap().0; let input = format!("{};", input.trim_end().trim_end_matches(';')); - let (mut _s, _) = s.clone_with_new_line(input).unwrap(); - *s = _s.clone(); - let s = &mut _s; - - if let Err(e) = s.parse() { - let source = s.to_repl_source(); - panic!("{e}\n\ncould not parse input:\n{source}") - } - s.generate_intermediate_output().expect("could not generate intermediate output") - } - - fn expr(stmts: &[pt::Statement]) -> pt::Expression { - match stmts.last().expect("no statements") { - pt::Statement::Expression(_, e) => e.clone(), - s => panic!("Not an expression: {s:?}"), - } - } - - fn get_type( - s: &mut SessionSource, - input: &str, - clear: bool, - ) -> (Option, IntermediateOutput) { - let intermediate = parse(s, input, clear); - let run_func_body = intermediate.run_func_body().expect("no run func body"); - let expr = expr(run_func_body); - (Type::from_expression(&expr).map(Type::map_special), intermediate) - } + let (new_source, _) = s.clone_with_new_line(input).unwrap(); + *s = new_source.clone(); + + let src = new_source.to_repl_source(); + let sess = + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(); + let mut compiler = Compiler::new(sess); + + compiler.enter_mut(|c| -> Option { + // Stage 1: parse + lower (mutable access required). + let lowered = { + let mut pcx = c.parse(); + let file = c + .sess() + .source_map() + .new_source_file( + std::path::PathBuf::from(new_source.file_name.clone()), + src.clone(), + ) + .ok()?; + pcx.add_file(file); + pcx.parse(); + matches!(c.lower_asts(), Ok(ControlFlow::Continue(()))) + }; + if !lowered { + return None; + } - fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { - let (ty, intermediate) = get_type(s, input, clear); - ty.and_then(|ty| ty.try_as_ethabi(Some(&intermediate))) + // Stage 2: walk HIR (immutable access). + let gcx = c.gcx(); + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + let last = body.last()?; + let expr = match last.kind { + StmtKind::Expr(e) => e, + _ => return None, + }; + expr_to_dyn(gcx, expr, true) + }) } fn generic_type_test<'a, T, I>(s: &mut SessionSource, input: I) diff --git a/crates/chisel/src/source.rs b/crates/chisel/src/source.rs index 8133de364c1da..90c0bad874622 100644 --- a/crates/chisel/src/source.rs +++ b/crates/chisel/src/source.rs @@ -5,7 +5,6 @@ //! execution helpers. use eyre::Result; -use foundry_common::fs; use foundry_compilers::{ Artifact, ProjectCompileOutput, artifacts::{ConfigurableContractArtifact, Source, Sources}, @@ -16,9 +15,16 @@ use foundry_config::{Config, SolcReq}; use foundry_evm::{backend::Backend, core::bytecode::InstIter, opts::EvmOpts}; use semver::Version; use serde::{Deserialize, Serialize}; -use solang_parser::pt::{self, CodeLocation}; -use solar::interface::diagnostics::EmittedDiagnostics; -use std::{cell::OnceCell, collections::HashMap, fmt, path::PathBuf}; +use solar::{ + ast::{ItemKind, StmtKind as AstStmtKind, yul}, + interface::{Span, diagnostics::EmittedDiagnostics}, + sema::{ + CompilerRef, + hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind}, + ty::Gcx, + }, +}; +use std::{cell::OnceCell, fmt}; use walkdir::WalkDir; /// The minimum Solidity version of the `Vm` interface. @@ -30,41 +36,8 @@ static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol"); /// [`SessionSource`] build output. pub struct GeneratedOutput { output: ProjectCompileOutput, - pub(crate) intermediate: IntermediateOutput, -} - -pub struct GeneratedOutputRef<'a> { - output: &'a ProjectCompileOutput, - // compiler: &'b solar::sema::CompilerRef<'c>, - pub(crate) intermediate: &'a IntermediateOutput, -} - -/// Intermediate output for the compiled [SessionSource] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IntermediateOutput { - /// All expressions within the REPL contract's run function and top level scope. - pub repl_contract_expressions: HashMap, - /// Intermediate contracts - pub intermediate_contracts: IntermediateContracts, -} - -/// A refined intermediate parse tree for a contract that enables easy lookups -/// of definitions. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IntermediateContract { - /// All function definitions within the contract - pub function_definitions: HashMap>, - /// All event definitions within the contract - pub event_definitions: HashMap>, - /// All struct definitions within the contract - pub struct_definitions: HashMap>, - /// All variable definitions within the top level scope of the contract - pub variable_definitions: HashMap>, } -/// A defined type for a map of contract names to [IntermediateContract]s -type IntermediateContracts = HashMap; - impl fmt::Debug for GeneratedOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("GeneratedOutput").finish_non_exhaustive() @@ -72,158 +45,25 @@ impl fmt::Debug for GeneratedOutput { } impl GeneratedOutput { - pub fn enter(&self, f: impl FnOnce(GeneratedOutputRef<'_>) -> T + Send) -> T { - // TODO(dani): once intermediate is removed - // self.output - // .parser() - // .solc() - // .compiler() - // .enter(|compiler| f(GeneratedOutputRef { output: &self.output, compiler })) - f(GeneratedOutputRef { output: &self.output, intermediate: &self.intermediate }) - } -} - -impl GeneratedOutputRef<'_> { - pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> { - self.output.find_first("REPL") - } -} - -impl std::ops::Deref for GeneratedOutput { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - &self.intermediate - } -} -impl std::ops::Deref for GeneratedOutputRef<'_> { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - self.intermediate + /// Enters the solar compiler context, providing access to the HIR and `Gcx`. + pub fn enter( + &self, + f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send, + ) -> R { + self.output + .parser() + .solc() + .compiler() + .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c })) } } -impl IntermediateOutput { - pub fn get_event(&self, input: &str) -> Option<&pt::EventDefinition> { - self.intermediate_contracts - .get("REPL") - .and_then(|contract| contract.event_definitions.get(input).map(std::ops::Deref::deref)) - } - - pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { - let deployed_bytecode = contract - .get_deployed_bytecode() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - let deployed_bytecode_bytes = deployed_bytecode - .bytes() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - - let run_func_statements = self.run_func_body()?; - - // Record loc of first yul block return statement (if any). - // This is used to decide which is the final statement within the `run()` method. - // see . - let last_yul_return = run_func_statements.iter().find_map(|statement| { - if let pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } = statement - && let Some(statement) = block.statements.last() - && let pt::YulStatement::FunctionCall(yul_call) = statement - && yul_call.id.name == "return" - { - return Some(statement.loc()); - } - None - }); - - // Find the last statement within the "run()" method and get the program - // counter via the source map. - let Some(final_statement) = run_func_statements.last() else { return Ok(None) }; - - // If the final statement is some type of block (assembly, unchecked, or regular), - // we need to find the final statement within that block. Otherwise, default to - // the source loc of the final statement of the `run()` function's block. - // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. - let mut source_loc = match final_statement { - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(statement) = last_statement { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the asm block. Because we use saturating sub to get the second - // to last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - pt::Statement::Block { loc: _, unchecked: _, statements } => { - if let Some(statement) = statements.last() { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - _ => final_statement.loc(), - }; - - // Consider yul return statement as final statement (if it's loc is lower) . - if let Some(yul_return) = last_yul_return - && yul_return.end() < source_loc.start() - { - source_loc = yul_return; - } - - // Map the source location of the final statement of the `run()` function to its - // corresponding runtime program counter - let final_pc = { - let offset = source_loc.start() as u32; - let length = (source_loc.end() - source_loc.start()) as u32; - trace!(%offset, %length, "find pc"); - contract - .get_source_map_deployed() - .unwrap() - .unwrap() - .into_iter() - .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) - .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, pc)| pc) - .max() - }; - trace!(?final_pc); - Ok(final_pc) - } - - pub fn run_func_body(&self) -> Result<&Vec> { - match self - .intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find REPL intermediate contract!"))? - .function_definitions - .get("run") - .ok_or_else(|| eyre::eyre!("Could not find run function definition in REPL contract!"))? - .body - .as_ref() - .ok_or_else(|| eyre::eyre!("Could not find run function body!"))? - { - pt::Statement::Block { statements, .. } => Ok(statements), - _ => eyre::bail!("Could not find statements within run function body!"), - } - } +/// A scoped reference to a [`GeneratedOutput`] together with an entered solar compiler. +pub struct GeneratedOutputRef<'a, 'b, 'gcx> { + output: &'a ProjectCompileOutput, + pub(crate) compiler: &'b CompilerRef<'gcx>, } -// TODO(dani): further migration blocked on upstream work -#[cfg(false)] impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { pub fn gcx(&self) -> Gcx<'gcx> { self.compiler.gcx() @@ -233,8 +73,35 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { self.output.find_first("REPL") } - pub fn get_event(&self, input: &str) -> Option { - self.gcx().hir.events_enumerated().find(|(_, e)| e.name.as_str() == input).map(|(id, _)| id) + /// Looks up the REPL contract in the HIR. + pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> { + self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + } + + /// Returns the body block of the REPL `run()` function. + pub fn run_func_body(&self) -> Block<'gcx> { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); + let f = c + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) + .expect("`run()` function not found in REPL contract"); + hir.function(f).body.expect("`run()` function does not have a body") + } + + /// Returns the [`EventId`] of an event named `input` in the REPL contract, if any. + pub fn get_event(&self, input: &str) -> Option { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir()?; + c.items.iter().find_map(|id| { + if let ItemId::Event(eid) = id + && hir.event(*eid).name.as_str() == input + { + Some(*eid) + } else { + None + } + }) } pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { @@ -251,52 +118,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Record loc of first yul block return statement (if any). // This is used to decide which is the final statement within the `run()` method. // see . - let last_yul_return_span: Option = run_body.iter().find_map(|stmt| { - // TODO(dani): Yul is not yet lowered to HIR. - let _ = stmt; - /* - if let hir::StmtKind::Assembly { block, .. } = stmt { - if let Some(stmt) = block.last() { - if let pt::YulStatement::FunctionCall(yul_call) = stmt { - if yul_call.id.name == "return" { - return Some(stmt.loc()) - } - } - } - } - */ - None - }); + // + // Yul is not yet lowered to HIR (assembly statements appear as `StmtKind::Err`), + // so we walk the AST of the REPL source to find a top-level `return(...)` call + // inside any `assembly { ... }` block in `run()`. + let last_yul_return_span: Option = self.first_yul_return_span(); // Find the last statement within the "run()" method and get the program // counter via the source map. let Some(last_stmt) = run_body.last() else { return Ok(None) }; - // If the final statement is some type of block (assembly, unchecked, or regular), + // If the final statement is some type of block (unchecked or regular), // we need to find the final statement within that block. Otherwise, default to // the source loc of the final statement of the `run()` function's block. // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. + // Inline assembly blocks (lowered to `StmtKind::Err` in HIR in the pinned solar + // version) are handled separately via `trailing_assembly_last_stmt_span`, which + // walks the AST to recover the last meaningful Yul statement. let source_stmt = match &last_stmt.kind { - // TODO(dani): Yul is not yet lowered to HIR. - /* - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(stmt) = last_statement { - stmt - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - &run_body[run_body.len().saturating_sub(2)] - } - } - */ - hir::StmtKind::UncheckedBlock(stmts) | hir::StmtKind::Block(stmts) => { + HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => { if let Some(stmt) = stmts.last() { stmt } else { @@ -308,9 +148,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { } _ => last_stmt, }; - let mut source_span = self.stmt_span_without_semicolon(source_stmt); + // If the trailing statement is an assembly block, prefer the last meaningful + // (non-`let`) Yul statement's span as the source location for `final_pc`. + // See . + // + // Two guards are required: + // 1. `StmtKind::Err`, assembly lowers to an error node in the current pinned solar + // version; this ensures we don't apply the AST fallback to properly-lowered stmts. + // 2. `trailing_assembly_last_stmt_span` returning `Some`, verifies via the AST that the + // failing HIR node actually corresponds to an assembly block (not some other lowering + // failure), and supplies the concrete span to use. + let mut source_span = if matches!(last_stmt.kind, HirStmtKind::Err(_)) + && let Some(span) = self.trailing_assembly_last_stmt_span() + { + span + } else { + self.stmt_span_without_semicolon(source_stmt) + }; - // Consider yul return statement as final statement (if it's loc is lower) . + // Consider yul return statement as final statement (if it's loc is lower). if let Some(yul_return_span) = last_yul_return_span && yul_return_span.hi() < source_span.lo() { @@ -319,26 +175,32 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Map the source location of the final statement of the `run()` function to its // corresponding runtime program counter - let (_sf, range) = self.compiler.sess().source_map().span_to_source(source_span).unwrap(); - dbg!(source_span, &range, &_sf.src[range.clone()]); + let result = self + .compiler + .sess() + .source_map() + .span_to_source(source_span) + .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?; + let range = result.data; let offset = range.start as u32; let length = range.len() as u32; - let final_pc = deployed_bytecode - .source_map() + trace!(%offset, %length, "find pc"); + let final_pc = contract + .get_source_map_deployed() .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))?? .into_iter() - .zip(InstructionIter::new(deployed_bytecode_bytes)) + .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, i)| i.pc) - .max() - .unwrap_or_default(); - Ok(Some(final_pc)) + .map(|(_, pc)| pc) + .max(); + trace!(?final_pc); + Ok(final_pc) } /// Statements' ranges in the solc source map do not include the semicolon. - fn stmt_span_without_semicolon(&self, stmt: &hir::Stmt<'_>) -> Span { + fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span { match stmt.kind { - hir::StmtKind::DeclSingle(id) => { + HirStmtKind::DeclSingle(id) => { let decl = self.gcx().hir.variable(id); if let Some(expr) = decl.initializer { stmt.span.with_hi(expr.span.hi()) @@ -346,23 +208,65 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { stmt.span } } - hir::StmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), - hir::StmtKind::Expr(expr) => expr.span, + HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), + HirStmtKind::Expr(expr) => expr.span, _ => stmt.span, } } - fn run_func_body(&self) -> hir::Block<'_> { - let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); - let f = c - .functions() - .find(|&f| self.gcx().hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) - .expect("`run()` function not found in REPL contract"); - self.gcx().hir.function(f).body.expect("`run()` function does not have a body") + /// Returns the AST `run()` body of the REPL contract, if any. + /// + /// Yul/assembly is not yet lowered to HIR in the pinned solar version, so we + /// keep around the AST to be able to inspect inline assembly blocks. + fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> { + let contract = self.repl_contract_hir()?; + let source = self.gcx().sources.get(contract.source)?; + let ast = source.ast.as_ref()?; + + let contract_ast = ast.items.iter().find_map(|i| match &i.kind { + ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c), + _ => None, + })?; + contract_ast.body.iter().find_map(|i| match &i.kind { + ItemKind::Function(f) if f.header.name.is_some_and(|n| n.as_str() == "run") => { + f.body.as_ref() + } + _ => None, + }) } - fn repl_contract_hir(&self) -> Option<&hir::Contract<'_>> { - self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + /// Returns the span of the first top-level `return(...)` call inside any + /// `assembly { ... }` block in the REPL `run()` function, if any. + fn first_yul_return_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + for stmt in run_body.stmts.iter() { + let AstStmtKind::Assembly(asm) = &stmt.kind else { continue }; + for ystmt in asm.block.stmts.iter() { + if let yul::StmtKind::Expr(e) = &ystmt.kind + && let yul::ExprKind::Call(call) = &e.kind + && call.name.as_str() == "return" + { + return Some(ystmt.span); + } + } + } + None + } + + /// If the last statement of the REPL `run()` function is an `assembly { ... }` block, + /// returns the span of its last non-`let` (i.e. non-VarDecl) Yul statement. + /// + /// This mirrors the legacy behavior used to pick a meaningful end-of-function PC when + /// the trailing statement is inline assembly. + fn trailing_assembly_last_stmt_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None }; + asm.block + .stmts + .iter() + .rev() + .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _))) + .map(|s| s.span) } } @@ -584,8 +488,7 @@ impl SessionSource { return Ok(output); } let output = self.compile()?; - let intermediate = self.generate_intermediate_output()?; - let output = GeneratedOutput { output, intermediate }; + let output = GeneratedOutput { output }; Ok(self.output.get_or_init(|| output)) } @@ -602,12 +505,11 @@ impl SessionSource { eyre::bail!("{output}"); } - // TODO(dani): re-enable - if cfg!(false) { - output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { - let _ = c.lower_asts(); - }); - } + // Drive HIR lowering and analysis so that subsequent `enter` queries can use them. + output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { + let _ = c.lower_asts(); + let _ = c.analysis(); + }); Ok(output) } @@ -632,53 +534,6 @@ impl SessionSource { sources } - /// Generate intermediate contracts for all contract definitions in the compilation source. - /// - /// ### Returns - /// - /// Optionally, a map of contract names to a vec of [IntermediateContract]s. - pub fn generate_intermediate_contracts(&self) -> Result> { - let mut res_map = HashMap::default(); - let parsed_map = self.get_sources(); - for source in parsed_map.values() { - Self::get_intermediate_contract(&source.content, &mut res_map); - } - Ok(res_map) - } - - /// Generate intermediate output for the REPL contract - pub fn generate_intermediate_output(&self) -> Result { - // Parse generate intermediate contracts - let intermediate_contracts = self.generate_intermediate_contracts()?; - - // Construct variable definitions - let variable_definitions = intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find intermediate REPL contract!"))? - .variable_definitions - .clone() - .into_iter() - .map(|(k, v)| (k, v.ty)) - .collect::>(); - // Construct intermediate output - let mut intermediate_output = IntermediateOutput { - repl_contract_expressions: variable_definitions, - intermediate_contracts, - }; - - // Add all statements within the run function to the repl_contract_expressions map - for (key, val) in intermediate_output - .run_func_body()? - .clone() - .iter() - .flat_map(Self::get_statement_definitions) - { - intermediate_output.repl_contract_expressions.insert(key, val); - } - - Ok(intermediate_output) - } - /// Construct the REPL source. pub fn to_repl_source(&self) -> String { let Self { @@ -741,108 +596,6 @@ contract {contract_name} {{ }); sess.dcx.emitted_errors().unwrap() } - - /// Gets the [IntermediateContract] for a Solidity source string and inserts it into the - /// passed `res_map`. In addition, recurses on any imported files as well. - /// - /// ### Takes - /// - `content` - A Solidity source string - /// - `res_map` - A mutable reference to a map of contract names to [IntermediateContract]s - pub fn get_intermediate_contract( - content: &str, - res_map: &mut HashMap, - ) { - if let Ok((pt::SourceUnit(source_unit_parts), _)) = solang_parser::parse(content, 0) { - let func_defs = source_unit_parts - .into_iter() - .filter_map(|sup| match sup { - pt::SourceUnitPart::ImportDirective(i) => match i { - pt::Import::Plain(s, _) - | pt::Import::Rename(s, _, _) - | pt::Import::GlobalSymbol(s, _, _) => { - let s = match s { - pt::ImportPath::Filename(s) => s.string, - pt::ImportPath::Path(p) => p.to_string(), - }; - let path = PathBuf::from(s); - - match fs::read_to_string(path) { - Ok(source) => { - Self::get_intermediate_contract(&source, res_map); - None - } - Err(_) => None, - } - } - }, - pt::SourceUnitPart::ContractDefinition(cd) => { - let mut intermediate = IntermediateContract::default(); - - cd.parts.into_iter().for_each(|part| match part { - pt::ContractPart::FunctionDefinition(def) => { - // Only match normal function definitions here. - if matches!(def.ty, pt::FunctionTy::Function) { - intermediate - .function_definitions - .insert(def.name.clone().unwrap().name, def); - } - } - pt::ContractPart::EventDefinition(def) => { - let event_name = def.name.as_ref().unwrap().name.clone(); - intermediate.event_definitions.insert(event_name, def); - } - pt::ContractPart::StructDefinition(def) => { - let struct_name = def.name.as_ref().unwrap().name.clone(); - intermediate.struct_definitions.insert(struct_name, def); - } - pt::ContractPart::VariableDefinition(def) => { - let var_name = def.name.as_ref().unwrap().name.clone(); - intermediate.variable_definitions.insert(var_name, def); - } - _ => {} - }); - Some((cd.name.as_ref().unwrap().name.clone(), intermediate)) - } - _ => None, - }) - .collect::>(); - res_map.extend(func_defs); - } - } - - /// Helper to deconstruct a statement - /// - /// ### Takes - /// - /// A reference to a [pt::Statement] - /// - /// ### Returns - /// - /// A vector containing tuples of the inner expressions' names, types, and storage locations. - pub fn get_statement_definitions(statement: &pt::Statement) -> Vec<(String, pt::Expression)> { - match statement { - pt::Statement::VariableDefinition(_, def, _) => { - vec![(def.name.as_ref().unwrap().name.clone(), def.ty.clone())] - } - pt::Statement::Expression(_, pt::Expression::Assign(_, left, _)) => { - if let pt::Expression::List(_, list) = left.as_ref() { - list.iter() - .filter_map(|(_, param)| { - param.as_ref().and_then(|param| { - param - .name - .as_ref() - .map(|name| (name.name.clone(), param.ty.clone())) - }) - }) - .collect() - } else { - Vec::default() - } - } - _ => Vec::default(), - } - } } /// A Parse Tree Fragment diff --git a/crates/chisel/tests/it/repl/mod.rs b/crates/chisel/tests/it/repl/mod.rs index 704d30405eed9..338b7d2043809 100644 --- a/crates/chisel/tests/it/repl/mod.rs +++ b/crates/chisel/tests/it/repl/mod.rs @@ -153,6 +153,26 @@ assembly { repl.expect("[0x00:0x20]"); }); +// Assembly as the final statement with a return — exercises the path where both +// `first_yul_return_span` and `trailing_assembly_last_stmt_span` resolve to the same `return(...)` +// span (no subsequent Solidity statement after the assembly block). +repl_test!(assembly_return_final, |repl| { + repl.sendln("uint x = 0xbeef;"); + repl.sendln("assembly { mstore(0x0, sload(0)) return(0x0, 0x20) }"); + repl.sendln("!md"); + repl.expect("[0x00:0x20]"); +}); + +// Assembly block without a `return(...)` call as an intermediate statement, exercises +// `first_yul_return_span` returning `None` while a subsequent Solidity statement is still evaluated +// correctly. +repl_test!(assembly_no_return_intermediate, |repl| { + repl.sendln("uint x = 1;"); + repl.sendln("assembly { x := add(x, 1) }"); + repl.sendln("x"); + repl.expect("Decimal: 2"); +}); + // Issue #5051, #8978: Test EVM version normalization. repl_test!(flaky_evm_version_normalization, "--use 0.7.6 --evm-version london", |repl| { repl.sendln("uint x;\nx"); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 37340f5f4cc3c..606b8291819e4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -52,6 +52,7 @@ rayon.workspace = true regex = { workspace = true, default-features = false } serde_json.workspace = true serde.workspace = true +toml.workspace = true strsim = "0.11" strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros"] } @@ -70,7 +71,13 @@ tempfile.workspace = true tikv-jemallocator = { workspace = true, optional = true } [features] +default = ["optimism"] tracy = ["dep:tracing-tracy"] tracy-allocator = ["tracy"] jemalloc = ["dep:tikv-jemallocator"] mimalloc = ["dep:mimalloc"] +optimism = [ + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index 87f14e2039606..4fc437c7232f8 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -307,6 +307,17 @@ mod tests { assert_eq!(val, &Value::from(1000u64)); } + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + use clap::CommandFactory; + + let command = EvmArgs::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + #[test] fn can_parse_chain_id() { let args = EvmArgs { diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 8c37860446683..f846da5002354 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -66,8 +66,20 @@ impl figment::Provider for RpcOpts { impl RpcOpts { /// Returns the RPC endpoint. pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>> { + self.url_with_env(config, std::env::var("ETH_RPC_URL").ok()) + } + + fn url_with_env<'a>( + &'a self, + config: Option<&'a Config>, + env_url: Option, + ) -> Result>> { if self.flashbots { Ok(Some(Cow::Borrowed(FLASHBOTS_URL))) + } else if let Some(url) = self.common.rpc_url.as_deref() { + Ok(Some(Cow::Borrowed(url))) + } else if let Some(url) = env_url { + Ok(Some(Cow::Owned(url))) } else { self.common.url(config) } @@ -85,8 +97,10 @@ impl RpcOpts { pub fn dict(&self) -> Dict { let mut dict = self.common.dict(); - if self.flashbots { - dict.insert("eth_rpc_url".into(), FLASHBOTS_URL.into()); + // `self.url(None)` already accounts for `flashbots` and the `ETH_RPC_URL` env var, + // so a single insert here covers both. + if let Ok(Some(url)) = self.url(None) { + dict.insert("eth_rpc_url".into(), url.into_owned().into()); } if let Ok(Some(jwt)) = self.jwt(None) { dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into()); @@ -199,6 +213,7 @@ impl figment::Provider for EthereumOpts { #[cfg(test)] mod tests { use super::*; + use clap::CommandFactory; #[test] fn parse_etherscan_opts() { @@ -223,4 +238,41 @@ mod tests { let id: u64 = chain_id.deserialize().expect("chain_id should deserialize as u64"); assert_eq!(id, 9745); } + + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + let command = RpcOpts::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + + #[test] + fn rpc_url_resolves_eth_rpc_url_env() { + let args = RpcOpts::default(); + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8545"); + } + + #[test] + fn explicit_rpc_url_takes_precedence_over_eth_rpc_url_env() { + let args = RpcOpts { + common: RpcCommonOpts { + rpc_url: Some("http://127.0.0.1:8546".to_string()), + ..Default::default() + }, + ..Default::default() + }; + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8546"); + } } diff --git a/crates/cli/src/opts/rpc_common.rs b/crates/cli/src/opts/rpc_common.rs index 05b98582fa88f..6a5fe5ed4e9e4 100644 --- a/crates/cli/src/opts/rpc_common.rs +++ b/crates/cli/src/opts/rpc_common.rs @@ -17,10 +17,15 @@ use std::borrow::Cow; /// This struct holds fields that both [`super::RpcOpts`] (cast) and /// [`super::EvmArgs`] (forge/script) need, eliminating duplication and /// making the two structs composable. +/// +/// Note: `ETH_RPC_URL` is intentionally **not** bound here as a clap env +/// fallback; otherwise it would be inherited by `EvmArgs` and silently +/// fork all `forge test` runs. Cast resolves `ETH_RPC_URL` explicitly +/// at the call site (see [`super::RpcOpts::url`]). #[derive(Clone, Debug, Default, Serialize, Parser)] pub struct RpcCommonOpts { /// The RPC endpoint. - #[arg(short, long, visible_alias = "fork-url", env = "ETH_RPC_URL")] + #[arg(short, long, visible_alias = "fork-url", value_name = "URL")] #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] pub rpc_url: Option, diff --git a/crates/cli/src/opts/tempo.rs b/crates/cli/src/opts/tempo.rs index 8c2a12e661e18..88119f163b325 100644 --- a/crates/cli/src/opts/tempo.rs +++ b/crates/cli/src/opts/tempo.rs @@ -1,16 +1,26 @@ use alloy_network::{Network, TransactionBuilder}; use alloy_primitives::{Address, ruint::aliases::U256}; -use alloy_signer::Signature; +use alloy_signer::{Signature, Signer}; use clap::Parser; -use foundry_common::FoundryTransactionBuilder; -use std::{num::NonZeroU64, str::FromStr}; +use eyre::Result; +use foundry_common::{ + FoundryTransactionBuilder, + tempo::{TempoSponsor, resolve_tempo_sponsor_signer}, +}; +use std::{ + num::NonZeroU64, + path::PathBuf, + str::FromStr, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::utils::parse_fee_token_address; -/// CLI options for Tempo transactions. +/// CLI options common to Tempo transactions across commands. #[derive(Clone, Debug, Default, Parser)] #[command(next_help_heading = "Tempo")] -pub struct TempoOpts { +pub struct TempoCommonOpts { /// Fee token address for Tempo transactions. /// /// When set, builds a Tempo (type 0x76) transaction that pays gas fees @@ -21,6 +31,40 @@ pub struct TempoOpts { #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] pub fee_token: Option
, + /// Opt into TIP-1009 expiring-nonce mode with a validity window. + /// + /// Convenience flag that combines `--tempo.expiring-nonce` with a relative + /// `--tempo.valid-before`. Sets nonce_key = U256::MAX, nonce = 0, and valid_before = now + + /// seconds. + /// + /// Maximum value is 30 seconds. The transaction must be mined before the deadline or it + /// becomes permanently invalid, giving safe retry semantics: retries produce a fresh tx hash + /// and the old tx can never land late. + #[arg(long = "tempo.expires", value_name = "SECONDS", value_parser = parse_expires_seconds)] + pub expires: Option, +} + +impl TempoCommonOpts { + /// Returns `true` if any Tempo-specific option is set. + pub const fn is_tempo(&self) -> bool { + self.fee_token.is_some() || self.expires.is_some() + } + + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + let secs = self.expires?; + let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards"); + Some(now.as_secs() + secs) + } +} + +/// CLI options for Tempo transactions. +#[derive(Clone, Debug, Default, Parser)] +#[command(next_help_heading = "Tempo")] +pub struct TempoOpts { + #[command(flatten)] + pub common: TempoCommonOpts, + /// Nonce key for Tempo parallelizable nonces. /// /// When set, builds a Tempo (type 0x76) transaction with the specified nonce key, @@ -28,21 +72,69 @@ pub struct TempoOpts { /// to be executed in parallel. If not set, the protocol nonce key (0) will be used. /// /// For more information see . - #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY")] + #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY", conflicts_with = "lane")] pub nonce_key: Option, + /// Named nonce lane for Tempo parallelizable nonces. + /// + /// Resolves a friendly lane name (e.g. `deploy`, `payments`) to a `nonce_key` via a + /// shared lanes file (default: `tempo.lanes.toml` at the project root). The lanes file + /// is a TOML map of `name = ` entries, e.g.: + /// + /// ```toml + /// deploy = 1 + /// ops = 2 + /// payments = 3 + /// ``` + /// + /// Mutually exclusive with `--tempo.nonce-key`. + #[arg(long = "tempo.lane", value_name = "NAME")] + pub lane: Option, + + /// Path to the Tempo lanes file used by `--tempo.lane`. + /// + /// Defaults to `tempo.lanes.toml` at the project root. + #[arg(long = "tempo.lanes-file", value_name = "PATH")] + pub lanes_file: Option, + + /// Sponsor (fee payer) address for Tempo sponsored transactions. + #[arg(long = "tempo.sponsor", value_name = "ADDRESS")] + pub sponsor: Option
, + + /// Sign Tempo sponsor digests in-band with the given signer URI. + /// + /// Supported forms include `env://VAR`, `keystore://PATH`, `account://NAME`, + /// `ledger://`, `trezor://`, `aws://`, `gcp://`, `turnkey://`, and + /// `private-key://KEY`. + #[arg( + long = "tempo.sponsor-signer", + value_name = "SIGNER", + requires = "sponsor", + conflicts_with = "sponsor_sig" + )] + pub sponsor_signer: Option, + /// Sponsor (fee payer) signature for Tempo sponsored transactions. /// /// The sponsor signs the `fee_payer_signature_hash` to commit to paying gas fees /// on behalf of the sender. Provide as a hex-encoded signature. - #[arg(long = "tempo.sponsor-signature", value_parser = parse_signature)] - pub sponsor_signature: Option, + #[arg( + long = "tempo.sponsor-sig", + alias = "tempo.sponsor-signature", + value_parser = parse_signature, + requires = "sponsor", + conflicts_with = "sponsor_signer" + )] + pub sponsor_sig: Option, /// Print the sponsor signature hash and exit. /// /// Computes the `fee_payer_signature_hash` for the transaction so that a sponsor /// knows what hash to sign. The transaction is not sent. - #[arg(long = "tempo.print-sponsor-hash")] + #[arg( + long = "tempo.print-sponsor-hash", + conflicts_with_all = &["sponsor", "sponsor_signer", "sponsor_sig"] + )] pub print_sponsor_hash: bool, /// Access key ID for Tempo Keychain signature transactions. @@ -56,14 +148,14 @@ pub struct TempoOpts { /// /// Sets nonce to 0 and nonce_key to U256::MAX, enabling time-bounded transaction /// validity via `--tempo.valid-before` and `--tempo.valid-after`. - #[arg(long = "tempo.expiring-nonce", requires = "valid_before")] + #[arg(long = "tempo.expiring-nonce", requires = "valid_before", conflicts_with = "expires")] pub expiring_nonce: bool, /// Upper bound timestamp for Tempo expiring nonce transactions. /// /// The transaction is only valid before this unix timestamp. /// Requires `--tempo.expiring-nonce`. - #[arg(long = "tempo.valid-before")] + #[arg(long = "tempo.valid-before", conflicts_with = "expires")] pub valid_before: Option, /// Lower bound timestamp for Tempo expiring nonce transactions. @@ -77,9 +169,12 @@ pub struct TempoOpts { impl TempoOpts { /// Returns `true` if any Tempo-specific option is set. pub const fn is_tempo(&self) -> bool { - self.fee_token.is_some() + self.common.is_tempo() || self.nonce_key.is_some() - || self.sponsor_signature.is_some() + || self.lane.is_some() + || self.sponsor.is_some() + || self.sponsor_signer.is_some() + || self.sponsor_sig.is_some() || self.print_sponsor_hash || self.key_id.is_some() || self.expiring_nonce @@ -87,6 +182,58 @@ impl TempoOpts { || self.valid_after.is_some() } + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + self.common.expires_at() + } + + /// Resolves `--tempo.expires` into concrete expiring-nonce fields. + /// + /// This computes the relative deadline once so later calls to [`Self::apply`] reuse the same + /// `valid_before` timestamp instead of deriving a fresh one. + pub fn resolve_expires(&mut self) -> Option { + let ts = self.expires_at()?; + self.expiring_nonce = true; + self.valid_before = Some(ts); + self.common.expires = None; + Some(ts) + } + + /// Returns `true` if a sponsor signature should be attached before submission. + pub const fn has_sponsor_submission(&self) -> bool { + self.sponsor.is_some() || self.sponsor_signer.is_some() || self.sponsor_sig.is_some() + } + + /// Resolves sponsor CLI options into a reusable sponsor config for transaction submission. + pub async fn sponsor_config(&self) -> Result> { + let Some(sponsor) = self.sponsor else { + return Ok(None); + }; + + let signer = if let Some(spec) = &self.sponsor_signer { + Some(Arc::new(Box::pin(resolve_tempo_sponsor_signer(spec)).await?)) + } else { + None + }; + + if let Some(signer) = &signer { + let signer_address = signer.address(); + if signer_address != sponsor { + eyre::bail!( + "Tempo sponsor signer address {signer_address} does not match --tempo.sponsor {sponsor}" + ); + } + } + + if signer.is_none() && self.sponsor_sig.is_none() { + eyre::bail!( + "--tempo.sponsor requires either --tempo.sponsor-signer or --tempo.sponsor-sig" + ); + } + + Ok(Some(TempoSponsor::new(sponsor, signer, self.sponsor_sig))) + } + /// Applies Tempo-specific options to a transaction request. /// /// All setters are no-ops for non-Tempo networks, so this is safe to call unconditionally. @@ -94,8 +241,9 @@ impl TempoOpts { where N::TransactionRequest: FoundryTransactionBuilder, { - // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX - if self.expiring_nonce { + // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX. + // --tempo.expires is a convenience alias that also sets valid_before = now + duration. + if self.expiring_nonce || self.common.expires.is_some() { tx.set_nonce(0); tx.set_nonce_key(U256::MAX); } else { @@ -107,11 +255,14 @@ impl TempoOpts { } } - if let Some(fee_token) = self.fee_token { + if let Some(fee_token) = self.common.fee_token { tx.set_fee_token(fee_token); } - if let Some(valid_before) = self.valid_before + // --tempo.expires sets valid_before relative to now; --tempo.valid-before takes a raw + // unix timestamp. The two flags are mutually exclusive (enforced by clap). + let effective_valid_before = self.expires_at().or(self.valid_before); + if let Some(valid_before) = effective_valid_before && let Some(v) = NonZeroU64::new(valid_before) { tx.set_valid_before(v); @@ -131,8 +282,7 @@ impl TempoOpts { // gas estimation so that `--tempo.print-sponsor-hash` and // `--tempo.sponsor-signature` produce identical gas estimates. Callers // should call `set_fee_payer_signature` on the built tx request. - if (self.sponsor_signature.is_some() || self.print_sponsor_hash) && tx.nonce_key().is_none() - { + if (self.has_sponsor_submission() || self.print_sponsor_hash) && tx.nonce_key().is_none() { tx.set_nonce_key(U256::ZERO); } } @@ -142,11 +292,83 @@ fn parse_signature(s: &str) -> Result { Signature::from_str(s).map_err(|e| format!("invalid signature: {e}")) } +/// Parses a seconds value for `--tempo.expires`, capped at the protocol maximum of 30 seconds. +fn parse_expires_seconds(s: &str) -> Result { + let secs: u64 = s + .parse() + .map_err(|_| format!("invalid value '{s}': expected an integer number of seconds"))?; + if secs > 30 { + return Err(format!("expires must be at most 30 seconds (got {secs})")); + } + Ok(secs) +} + #[cfg(test)] mod tests { use super::*; use alloy_primitives::address; + #[test] + fn parses_lane_arg() { + let opts = TempoOpts::try_parse_from(["", "--tempo.lane", "deploy"]).unwrap(); + assert_eq!(opts.lane.as_deref(), Some("deploy")); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn lane_conflicts_with_nonce_key() { + let err = + TempoOpts::try_parse_from(["", "--tempo.lane", "deploy", "--tempo.nonce-key", "1"]) + .unwrap_err(); + assert!( + err.to_string().contains("cannot be used with"), + "expected clap conflict error, got: {err}", + ); + } + + #[test] + fn parse_expires_flag() { + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "30"]).unwrap(); + assert_eq!(opts.common.expires, Some(30)); + + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + assert_eq!(opts.common.expires, Some(10)); + + // exceeds 30s maximum + assert!(TempoOpts::try_parse_from(["", "--tempo.expires", "31"]).is_err()); + + // conflicts with --tempo.expiring-nonce + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.expires", + "30", + "--tempo.expiring-nonce", + "--tempo.valid-before", + "999" + ]) + .is_err() + ); + } + + #[test] + fn resolve_expires_materializes_valid_before() { + let before = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + let mut opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + + let resolved = opts.resolve_expires().unwrap(); + let after = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + + assert!(resolved >= before + 10); + assert!(resolved <= after + 10); + assert!(opts.expiring_nonce); + assert_eq!(opts.valid_before, Some(resolved)); + assert_eq!(opts.common.expires, None); + assert_eq!(opts.expires_at(), None); + } + #[test] fn parse_fee_token_id() { let opts = TempoOpts::try_parse_from([ @@ -155,13 +377,69 @@ mod tests { "0x20C0000000000000000000000000000000000002", ]) .unwrap(); - assert_eq!(opts.fee_token, Some(address!("0x20C0000000000000000000000000000000000002")),); + assert_eq!( + opts.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000002")), + ); // AlphaUSD token ID is 1u64 let opts_with_id = TempoOpts::try_parse_from(["", "--tempo.fee-token", "1"]).unwrap(); assert_eq!( - opts_with_id.fee_token, + opts_with_id.common.fee_token, Some(address!("0x20C0000000000000000000000000000000000001")), ); } + + #[test] + fn parse_sponsor_signer() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert_eq!(opts.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + assert!(opts.sponsor_sig.is_none()); + assert!(opts.is_tempo()); + assert!(opts.has_sponsor_submission()); + } + + #[test] + fn sponsor_signer_requires_sponsor() { + assert!( + TempoOpts::try_parse_from(["", "--tempo.sponsor-signer", "env://SPONSOR"]).is_err() + ); + } + + #[test] + fn parse_sponsor_signature_alias() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signature", + "0x0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert!(opts.sponsor_sig.is_some()); + } + + #[test] + fn print_sponsor_hash_conflicts_with_sponsor_submission() { + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.print-sponsor-hash", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + ]) + .is_err() + ); + } } diff --git a/crates/cli/src/utils/tempo.rs b/crates/cli/src/utils/tempo.rs index 647f52d316a6c..4b5715b9ebe08 100644 --- a/crates/cli/src/utils/tempo.rs +++ b/crates/cli/src/utils/tempo.rs @@ -1,8 +1,44 @@ -use std::str::FromStr; +//! Tempo utilities: fee token parsing and named nonce lanes (2D nonces). +//! +//! A "lane" is a friendly alias for a Tempo `nonce_key` (a [`U256`]). Lanes are defined in a +//! shared TOML file (default `tempo.lanes.toml` at the project root) so a team can reserve +//! independent sequential nonce streams for parallel scripts without coordinating on raw +//! `U256` selectors. +//! +//! Example `tempo.lanes.toml`: +//! +//! ```toml +//! deploy = 1 +//! ops = 2 +//! payments = 3 +//! ``` +//! +//! ```bash +//! cast erc20 transfer ... --tempo.lane payments +//! ``` -use alloy_primitives::Address; +use crate::opts::TempoOpts; +use alloy_primitives::{Address, U256}; +use eyre::{Result, eyre}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + str::FromStr, +}; use tempo_primitives::TempoAddressExt; +/// Default name of the lanes file at the project root. +pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml"; + +/// Result of resolving a `--tempo.lane ` argument against a lanes file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedLane { + /// The lane name as provided on the CLI. + pub name: String, + /// The `nonce_key` the lane resolved to. + pub nonce_key: U256, +} + /// Parses a fee token address. pub fn parse_fee_token_address(address_or_id: &str) -> eyre::Result
{ Address::from_str(address_or_id).or_else(|_| Ok(token_id_to_address(address_or_id.parse()?))) @@ -14,3 +50,156 @@ fn token_id_to_address(token_id: u64) -> Address { address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes()); Address::from(address_bytes) } + +/// Loads a TOML lanes file from `path`. +/// +/// Each top-level key is a lane name, and the value is the `nonce_key` (an integer or a +/// decimal/hex string parsed as [`U256`]). +pub fn load_lanes(path: &Path) -> Result> { + let contents = std::fs::read_to_string(path) + .map_err(|e| eyre!("failed to read tempo lanes file {}: {}", path.display(), e))?; + parse_lanes(&contents) + .map_err(|e| eyre!("failed to parse tempo lanes file {}: {}", path.display(), e)) +} + +fn parse_lanes(contents: &str) -> Result> { + let raw: BTreeMap = toml::from_str(contents)?; + let mut out = BTreeMap::new(); + for (name, value) in raw { + let nonce_key = match value { + toml::Value::Integer(n) => { + if n < 0 { + return Err(eyre!("invalid nonce_key for lane '{name}': must be non-negative")); + } + U256::from(n as u64) + } + toml::Value::String(s) => U256::from_str(s.trim()) + .map_err(|e| eyre!("invalid nonce_key for lane '{name}': {e}"))?, + other => { + return Err(eyre!( + "invalid nonce_key for lane '{name}': expected integer or string, got {}", + other.type_str(), + )); + } + }; + out.insert(name, nonce_key); + } + Ok(out) +} + +/// Resolves `opts.lane` against a lanes file and writes the resulting `nonce_key` to +/// `opts.nonce_key`. Returns the resolved lane (or `None` if no `--tempo.lane` was set). +/// +/// `root` is the project root used to locate the default lanes file +/// (`/tempo.lanes.toml`) when `--tempo.lanes-file` was not provided. +pub fn resolve_lane(opts: &mut TempoOpts, root: &Path) -> Result> { + let Some(lane_name) = opts.lane.clone() else { return Ok(None) }; + + let path: PathBuf = opts.lanes_file.clone().unwrap_or_else(|| root.join(DEFAULT_LANES_FILE)); + + if !path.exists() { + return Err(eyre!( + "tempo lanes file not found at {}\n\ + create it with `name = ` entries, e.g.:\n \ + deploy = 1\n \ + ops = 2\n \ + payments = 3", + path.display(), + )); + } + + let lanes = load_lanes(&path)?; + + let nonce_key = lanes.get(&lane_name).copied().ok_or_else(|| { + let mut known: Vec<&str> = lanes.keys().map(String::as_str).collect(); + known.sort_unstable(); + eyre!( + "lane '{lane_name}' not found in {} (known lanes: {})", + path.display(), + if known.is_empty() { "".to_string() } else { known.join(", ") }, + ) + })?; + + opts.nonce_key = Some(nonce_key); + Ok(Some(ResolvedLane { name: lane_name, nonce_key })) +} + +/// Prints `lane: (nonce_key=, nonce=)` to stderr (so it doesn't pollute +/// stdout for commands like `cast mktx` whose stdout is meant to be piped), giving +/// visibility into which 2D nonce lane was used. +pub fn maybe_print_resolved_lane(resolved: Option<&ResolvedLane>, nonce: u64) -> Result<()> { + if let Some(lane) = resolved { + sh_eprintln!("lane: {} (nonce_key={}, nonce={})", lane.name, lane.nonce_key, nonce)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_int_and_string_lane_values() { + let toml = r#" +deploy = 1 +ops = 2 +payments = "3" +big = "115792089237316195423570985008687907853269984665640564039457584007913129639935" +"#; + let lanes = parse_lanes(toml).unwrap(); + assert_eq!(lanes.get("deploy"), Some(&U256::from(1u64))); + assert_eq!(lanes.get("ops"), Some(&U256::from(2u64))); + assert_eq!(lanes.get("payments"), Some(&U256::from(3u64))); + assert_eq!(lanes.get("big"), Some(&U256::MAX)); + } + + #[test] + fn parse_lanes_rejects_invalid_string() { + let toml = "broken = \"not-a-number\""; + let err = parse_lanes(toml).unwrap_err(); + assert!(err.to_string().contains("invalid nonce_key for lane 'broken'")); + } + + #[test] + fn resolve_lane_sets_nonce_key_and_returns_resolved() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 7\npayments = 42\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let resolved = resolve_lane(&mut opts, dir.path()).unwrap().unwrap(); + assert_eq!(resolved.name, "payments"); + assert_eq!(resolved.nonce_key, U256::from(42u64)); + assert_eq!(opts.nonce_key, Some(U256::from(42u64))); + } + + #[test] + fn resolve_lane_returns_none_when_no_lane() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts::default(); + let resolved = resolve_lane(&mut opts, dir.path()).unwrap(); + assert!(resolved.is_none()); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn resolve_lane_errors_when_file_missing() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts { lane: Some("deploy".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + assert!(err.to_string().contains("tempo lanes file not found")); + } + + #[test] + fn resolve_lane_errors_when_lane_unknown() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 1\nops = 2\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("lane 'payments' not found")); + assert!(msg.contains("deploy, ops")); + } +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 7fd94c07242e8..a66a0fef2fe0a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -34,7 +34,7 @@ alloy-signer.workspace = true alloy-pubsub.workspace = true alloy-rpc-client.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "engine"] } -alloy-rpc-types-engine = { workspace = true, features = ["jwt"] } +alloy-rpc-types-engine = { workspace = true, features = ["jwt-aws-lc-rs"] } alloy-sol-types.workspace = true alloy-transport-ipc.workspace = true alloy-transport-ws.workspace = true @@ -43,8 +43,8 @@ alloy-transport.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true -op-alloy-network.workspace = true -op-alloy-rpc-types.workspace = true +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } revm.workspace = true @@ -86,6 +86,10 @@ mpp.workspace = true foundry-wallets = { workspace = true, features = ["browser", "tempo"] } tokio-tungstenite.workspace = true futures.workspace = true +alloy-signer-local.workspace = true +base64.workspace = true +sha2 = "0.10" +tempfile.workspace = true [build-dependencies] chrono.workspace = true @@ -95,4 +99,12 @@ vergen = { workspace = true, features = ["build", "emit_and_set"] } foundry-evm-hardforks.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } axum = { workspace = true } -tempfile.workspace = true +k256 = { workspace = true } + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "foundry-common-fmt/optimism", +] diff --git a/crates/common/build.rs b/crates/common/build.rs index d89e23be850f4..9afa01b5757ef 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -15,16 +15,13 @@ fn main() -> Result<(), Box> { let sha_short = &sha[..10]; let tag_name = try_env_var("TAG_NAME").unwrap_or_else(|| String::from("dev")); - let is_nightly = tag_name.contains("nightly"); - let version_suffix = if is_nightly { "nightly" } else { &tag_name }; + let version = release_version(&env_var("CARGO_PKG_VERSION"), &tag_name); + let is_nightly = tag_name.starts_with("nightly"); if is_nightly { println!("cargo:rustc-env=FOUNDRY_IS_NIGHTLY_VERSION=true"); } - let pkg_version = env_var("CARGO_PKG_VERSION"); - let version = format!("{pkg_version}-{version_suffix}"); - // `PROFILE` captures only release or debug. Get the actual name from the out directory. let out_dir = PathBuf::from(env_var("OUT_DIR")); let profile = out_dir.components().rev().nth(3).unwrap().as_os_str().to_str().unwrap(); @@ -87,6 +84,19 @@ fn env_var(name: &str) -> String { try_env_var(name).unwrap() } +fn release_version(pkg_version: &str, tag_name: &str) -> String { + if let Some(version) = tag_name.strip_prefix('v') { + return version.to_owned(); + } + + // Normalize `nightly-` to `nightly` so tarball and Docker nightly + // artifacts produce the same version string. The commit identifier is + // already included in the SemVer build metadata (after `+`). + let normalized = if tag_name.starts_with("nightly-") { "nightly" } else { tag_name }; + + format!("{pkg_version}-{normalized}") +} + fn try_env_var(name: &str) -> Option { println!("cargo:rerun-if-env-changed={name}"); std::env::var(name).ok() diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 2c8e16bccdcc6..179c71048da5b 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -20,10 +20,10 @@ eyre.workspace = true # ui alloy-consensus.workspace = true -op-alloy-consensus.workspace = true +op-alloy-consensus = { workspace = true, optional = true } alloy-network.workspace = true alloy-rpc-types = { workspace = true, features = ["eth"] } -op-alloy-rpc-types.workspace = true +op-alloy-rpc-types = { workspace = true, optional = true } alloy-serde.workspace = true serde.workspace = true serde_json.workspace = true @@ -38,3 +38,7 @@ tempo-alloy.workspace = true [dev-dependencies] foundry-macros.workspace = true similar-asserts.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:op-alloy-consensus", "dep:op-alloy-rpc-types"] diff --git a/crates/common/fmt/src/ui.rs b/crates/common/fmt/src/ui.rs index e883810dcda34..2087a85236154 100644 --- a/crates/common/fmt/src/ui.rs +++ b/crates/common/fmt/src/ui.rs @@ -18,6 +18,7 @@ use alloy_rpc_types::{ AccessListItem, Block, BlockTransactions, Header, Log, Transaction, TransactionReceipt, }; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{OpTxEnvelope, TxDeposit, TxPostExec}; use revm::context_interface::transaction::SignedAuthorization; use serde::Deserialize; @@ -448,6 +449,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxDeposit { fn pretty(&self) -> String { format!( @@ -472,6 +474,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxPostExec { fn pretty(&self) -> String { format!( @@ -606,6 +609,7 @@ type {:#x} } } +#[cfg(feature = "optimism")] impl UIfmt for OpTxEnvelope { fn pretty(&self) -> String { match self { @@ -651,6 +655,7 @@ effectiveGasPrice {} } } +#[cfg(feature = "optimism")] impl UIfmt for op_alloy_rpc_types::Transaction { fn pretty(&self) -> String { format!( @@ -786,6 +791,7 @@ impl UIfmtSignatureExt for AnyTxEnvelope { } } +#[cfg(feature = "optimism")] impl UIfmtSignatureExt for OpTxEnvelope { fn signature_pretty(&self) -> Option<(String, String, String)> { self.signature().map(|sig| { @@ -1135,6 +1141,7 @@ mod tests { assert_eq!(b.pretty(), b32.pretty()); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx() { let s = r#" @@ -1186,6 +1193,7 @@ yParity 1 ); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx_through_any() { let s = r#" diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 895b16b3b4532..95c7d4083f34e 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -383,16 +383,14 @@ impl ContractsByArtifact { &self, id: &str, ) -> Result>> { - let contracts = self - .iter() - .filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id) - .collect::>(); - - if contracts.len() > 1 { + let mut iter = + self.iter().filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id); + let first = iter.next(); + if first.is_some() && iter.next().is_some() { eyre::bail!("{id} has more than one implementation."); } - Ok(contracts.first().copied()) + Ok(first) } /// Finds abi by name or source path @@ -411,7 +409,7 @@ impl ContractsByArtifact { let mut funcs = BTreeMap::new(); let mut events = BTreeMap::new(); let mut errors_abi = JsonAbi::new(); - for (_name, contract) in self.iter() { + for contract in self.values() { for func in contract.abi.functions() { funcs.insert(func.selector(), func.clone()); } diff --git a/crates/common/src/provider/mpp/keys.rs b/crates/common/src/provider/mpp/keys.rs index 65640c48ab841..fa0fc80ed3d03 100644 --- a/crates/common/src/provider/mpp/keys.rs +++ b/crates/common/src/provider/mpp/keys.rs @@ -7,6 +7,7 @@ use crate::tempo::{TEMPO_PRIVATE_KEY_ENV, WalletType, read_tempo_keys_file}; use alloy_primitives::Address; +use std::env; use tracing::debug; /// Options for MPP key discovery filtering. @@ -55,7 +56,7 @@ pub fn discover_mpp_key() -> Option { /// target chain and the required currency. pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 1. Check TEMPO_PRIVATE_KEY env var (no keychain metadata available) - if let Ok(key) = std::env::var(TEMPO_PRIVATE_KEY_ENV) { + if let Ok(key) = env::var(TEMPO_PRIVATE_KEY_ENV) { let key = key.trim().to_string(); if !key.is_empty() { debug!("using MPP key from {TEMPO_PRIVATE_KEY_ENV} env var"); @@ -73,11 +74,17 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 2. Read $TEMPO_HOME/wallet/keys.toml (default: ~/.tempo/wallet/keys.toml) let keys_file = read_tempo_keys_file()?; + // `expiry == 0` means "no expiry" on the wire. + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // Pick primary key using the same deterministic order as // `Keystore::primary_key()` in tempo-common: // passkey > first entry with inline key > first entry // Only entries with a usable inline key can provide a signing key. - // Filter by chain_id and currency when provided. + // Filter by chain_id, currency, and freshness when provided. let candidates: Vec<_> = keys_file .keys .iter() @@ -86,6 +93,7 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { opts.currency .is_none_or(|cur| k.limits.is_empty() || k.limits.iter().any(|l| l.currency == cur)) }) + .filter(|k| k.expiry.is_none_or(|e| e == 0 || e > now)) .collect(); let primary = candidates @@ -135,6 +143,7 @@ mod tests { #[test] fn discover_from_tempo_home_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( r#" @@ -160,6 +169,7 @@ chain_id = 4217 #[test] fn discover_env_var_takes_priority_over_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let file_key = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; let env_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( @@ -187,6 +197,7 @@ key = "{file_key}" #[test] fn discover_returns_none_when_no_keys() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let (dir, _) = setup_keys_toml(""); unsafe { @@ -202,6 +213,7 @@ key = "{file_key}" #[test] fn discover_skips_entries_without_inline_key() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let toml_content = format!( r#" @@ -344,6 +356,7 @@ key = "0xthe_key" #[test] fn discover_filters_by_chain_id() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let mainnet_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let testnet_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; let toml_content = format!( @@ -416,6 +429,62 @@ chain_id = 4217 unsafe { std::env::remove_var("TEMPO_HOME") }; } + #[test] + fn discover_filters_expired_entries() { + // Expired entries must not be selected, so the next 402 re-triggers + // the device-code flow instead of returning a stale key. + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let expired_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let fresh_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + let toml_content = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 + +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000002" +key = "{fresh_key}" +chain_id = 4217 +expiry = 0 +"# + ); + let (dir, _) = setup_keys_toml(&toml_content); + unsafe { + std::env::set_var("TEMPO_HOME", dir.path()); + std::env::remove_var("TEMPO_PRIVATE_KEY"); + } + + // Even though the expired entry comes first, discovery skips it. + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert_eq!(config.as_ref().unwrap().key, fresh_key); + + // With only the expired entry present, discovery returns None so the + // 402 path can run `ensure_access_key` again. + let only_expired = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 +"# + ); + let (dir2, _) = setup_keys_toml(&only_expired); + unsafe { std::env::set_var("TEMPO_HOME", dir2.path()) }; + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert!(config.is_none(), "expired-only keys.toml must not yield a usable key"); + + unsafe { std::env::remove_var("TEMPO_HOME") }; + } + #[test] fn parse_keys_toml_unknown_fields_ignored() { let toml_str = r#" diff --git a/crates/common/src/provider/mpp/session.rs b/crates/common/src/provider/mpp/session.rs index 334166b844613..c3e87f8cf42b5 100644 --- a/crates/common/src/provider/mpp/session.rs +++ b/crates/common/src/provider/mpp/session.rs @@ -175,6 +175,16 @@ impl SessionProvider { self } + /// Address that funds payments for this provider. + pub fn funding_wallet_address(&self) -> Address { + self.signing_mode.from_address(self.signer.address()) + } + + /// Chain ID from the selected wallet key, when known. + pub const fn key_chain_id(&self) -> Option { + self.key_chain_id + } + /// Set the chain ID and currencies from the key entry used to initialize /// this provider. Used to reject challenges for incompatible chains/currencies. /// When `chain_id` is `None` (e.g. env var key), chain filtering is skipped. diff --git a/crates/common/src/provider/mpp/transport.rs b/crates/common/src/provider/mpp/transport.rs index 67354dc2bd60d..9e3b16dedd59e 100644 --- a/crates/common/src/provider/mpp/transport.rs +++ b/crates/common/src/provider/mpp/transport.rs @@ -4,6 +4,7 @@ //! handling via the MPP protocol. When the RPC endpoint returns a 402 response, //! this transport automatically pays the challenge and retries the request. +use alloy_chains::Chain; use alloy_json_rpc::{RequestPacket, ResponsePacket}; use alloy_transport::{TransportError, TransportErrorKind, TransportFut, TransportResult}; use mpp::{ @@ -16,12 +17,17 @@ use mpp::{ use reqwest::{StatusCode, header::HeaderMap}; use std::{ collections::HashMap, - fmt, - sync::{Mutex, OnceLock}, + env, fmt, io, + io::IsTerminal, + process::{Command, Stdio}, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicBool, Ordering}, + }, task, time::Duration, }; -use tokio::sync::OwnedMutexGuard; +use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard}; use tower::Service; use tracing::{Instrument, debug, debug_span, trace}; use url::Url; @@ -39,7 +45,27 @@ const MPP_RETRY_TIMEOUT: Duration = Duration::from_secs(120); /// Resolve the deposit amount from `MPP_DEPOSIT` env var or the default. fn default_deposit() -> u128 { - std::env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) + env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct FundingContext { + wallet_address: Option, + token: Option, + chain_id: Option, +} + +impl FundingContext { + fn token_line(&self) -> String { + self.token + .as_ref() + .map(|token| format!("Requested payment token: {token}\n\n")) + .unwrap_or_default() + } + + fn network(&self) -> Option { + self.chain_id.filter(|chain| chain.is_tempo()).map(|chain| chain.to_string()) + } } fn format_http_diagnostics(headers: &HeaderMap) -> String { @@ -60,12 +86,173 @@ fn format_http_diagnostics(headers: &HeaderMap) -> String { } } +fn tempo_wallet_fund_help(ctx: &FundingContext) -> String { + let mut command = "tempo wallet fund".to_string(); + if let Some(address) = ctx.wallet_address { + command.push_str(&format!(" --address {address}")); + } + if let Some(network) = ctx.network() { + command.push_str(&format!(" --network {network}")); + } + + let mut no_browser = command.clone(); + no_browser.push_str(" --no-browser"); + + format!( + "\n\nTempo wallet payment could not be funded for this paid RPC request.\n\n{}\ + Fund the wallet, then rerun the command:\n {command}\n\n\ + If this CLI is running on a remote or headless host, use:\n {no_browser}", + ctx.token_line() + ) +} + +/// Decide whether the interactive `tempo wallet fund` flow may be launched. +/// +/// Policy (library-safe): +/// - never run inside CI +/// - never run unless both stdin and stderr are real terminals +/// - `FOUNDRY_MPP_NO_AUTO_FUND` is honored as an opt-out; it must not bypass CI/TTY guards in +/// shared transport code that may be embedded inside long-running RPC daemons. +fn interactive_tempo_fund_allowed( + no_auto_fund: Option<&str>, + in_ci: bool, + stdin_is_terminal: bool, + stderr_is_terminal: bool, +) -> bool { + if no_auto_fund.is_some_and(|v| { + !(v == "0" || v.eq_ignore_ascii_case("false") || v.eq_ignore_ascii_case("off")) + }) { + return false; + } + + if in_ci { + return false; + } + + stdin_is_terminal && stderr_is_terminal +} + +fn can_run_interactive_tempo_fund() -> bool { + if cfg!(test) { + return false; + } + + interactive_tempo_fund_allowed( + std::env::var("FOUNDRY_MPP_NO_AUTO_FUND").ok().as_deref(), + std::env::var_os("CI").is_some(), + std::io::stdin().is_terminal(), + std::io::stderr().is_terminal(), + ) +} + +fn tempo_bin() -> String { + std::env::var("TEMPO_BIN").unwrap_or_else(|_| "tempo".to_string()) +} + +async fn run_interactive_tempo_fund(ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + + let tempo = tempo_bin(); + let mut args = vec!["wallet".to_string(), "fund".to_string()]; + if let Some(address) = ctx.wallet_address { + args.push("--address".to_string()); + args.push(address.to_string()); + } + if let Some(network) = ctx.network() { + args.push("--network".to_string()); + args.push(network); + } + + tracing::warn!( + token = ?ctx.token, + chain_id = ?ctx.chain_id, + "MPP payment could not be funded; opening `tempo wallet fund`" + ); + + let status = tokio::task::spawn_blocking(move || { + Command::new(tempo) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + }) + .await + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to join tempo wallet fund process: {e}" + ))) + })? + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to run `tempo wallet fund`: {e}{}", + tempo_wallet_fund_help(ctx) + ))) + })?; + + if status.success() { + Ok(true) + } else { + Err(TransportErrorKind::custom(std::io::Error::other(format!( + "`tempo wallet fund` exited with status {status}{}", + tempo_wallet_fund_help(ctx) + )))) + } +} + +/// Single-attempt guard around [`run_interactive_tempo_fund`]. +/// +/// Ensures that for one logical request we launch `tempo wallet fund` at most +/// once, regardless of how many recovery paths (`do_request`, `pay_and_retry`, +/// `handle_response_or_retry_after_fund`, ...) attempt it. +async fn maybe_auto_fund(used: &AtomicBool, ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + if used.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { + return Ok(false); + } + run_interactive_tempo_fund(ctx).await +} + +/// Returns true iff a 402 response carries a structured insufficient-balance +/// problem (RFC 9457 `PaymentErrorDetails`). +/// +/// We deliberately do **not** match on free-text body content or on generic +/// `verification-failed` problem types, as those have many non-funding causes +/// (bad signature, replay, expired challenge, clock skew, key provisioning, +/// malformed auth, ...). +fn should_suggest_tempo_fund(status: StatusCode, body: &[u8]) -> bool { + if status != StatusCode::PAYMENT_REQUIRED { + return false; + } + let Ok(problem) = serde_json::from_slice::(body) else { + return false; + }; + problem.problem_type.ends_with("/insufficient-balance") +} + +fn format_mpp_payment_failure( + error: impl fmt::Display, + ctx: &FundingContext, + suggest_fund: bool, +) -> String { + let message = error.to_string(); + if suggest_fund { + format!("MPP payment failed: {message}{}", tempo_wallet_fund_help(ctx)) + } else { + format!("MPP payment failed: {message}") + } +} + /// Process-wide payment serialization locks, keyed by origin URL. /// /// Created eagerly so the lock exists before the first provider init, /// preventing concurrent first-402 races. -static GLOBAL_PAY_LOCKS: OnceLock>>>> = - OnceLock::new(); +static GLOBAL_PAY_LOCKS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); /// Production transport: lazily discovers MPP keys from the Tempo wallet on /// first 402 response. @@ -75,24 +262,21 @@ pub type LazyMppHttpTransport = MppHttpTransport; /// Tempo wallet configuration on first use. #[derive(Clone, Debug)] pub struct LazySessionProvider { - inner: std::sync::Arc>>, + inner: Arc>>, /// Eagerly-created, process-wide payment serialization lock for this origin. - pay_lock: std::sync::Arc>, + pay_lock: Arc>, origin: String, } impl LazySessionProvider { pub(super) fn new(origin: String) -> Self { - let pay_lock = { - let global = GLOBAL_PAY_LOCKS.get_or_init(|| Mutex::new(HashMap::new())); - global - .lock() - .unwrap() - .entry(origin.clone()) - .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(()))) - .clone() - }; - Self { inner: std::sync::Arc::new(Mutex::new(None)), pay_lock, origin } + let pay_lock = GLOBAL_PAY_LOCKS + .lock() + .unwrap() + .entry(origin.clone()) + .or_insert_with(|| Arc::new(AsyncMutex::new(()))) + .clone(); + Self { inner: Arc::new(Mutex::new(None)), pay_lock, origin } } fn set_key_provisioned(&self, provisioned: bool) { @@ -125,6 +309,14 @@ impl LazySessionProvider { } } + /// Drop the cached `SessionProvider` so the next `get_or_init` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry, so a long-lived transport doesn't keep paying with + /// the superseded key. + fn invalidate(&self) { + *self.inner.lock().unwrap() = None; + } + pub(super) fn get_or_init(&self, opts: DiscoverOptions) -> TransportResult { let mut guard = self.inner.lock().unwrap(); if let Some(ref provider) = *guard { @@ -132,18 +324,20 @@ impl LazySessionProvider { } let config = discover_mpp_config(opts).ok_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "RPC endpoint returned HTTP 402 Payment Required. \ This endpoint requires payment via the Machine Payments Protocol (MPP).\n\n\ - To configure MPP, install the Tempo wallet CLI and create a key:\n\ - \n curl -sSL https://tempo.xyz/install.sh | bash\ - \n tempo wallet login\ + Authorize an access key against your Tempo wallet:\n\ + \n cast tempo login\ + \n\nIn headless environments, pass `--no-browser` to print the authorization \ + URL instead of launching a browser:\n\ + \n cast tempo login --no-browser\ \n\nSee https://docs.tempo.xyz for more information.", )) })?; let signer: mpp::PrivateKeySigner = config.key.parse().map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("invalid MPP key: {e}"))) + TransportErrorKind::custom(io::Error::other(format!("invalid MPP key: {e}"))) })?; let signing_mode = if let Some(wallet) = config.wallet_address { @@ -152,7 +346,7 @@ impl LazySessionProvider { .as_ref() .map(|hex_str| { crate::tempo::decode_key_authorization(hex_str).map(Box::new).map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "invalid MPP key_authorization: {e}" ))) }) @@ -223,6 +417,17 @@ where P::Provider: Send + Sync + 'static, { async fn do_request(self, req: RequestPacket) -> TransportResult { + // Per-request guard: launch `tempo wallet fund` at most once for one + // logical request, regardless of how many recovery paths attempt it. + let auto_fund_used = AtomicBool::new(false); + self.do_request_inner(req, &auto_fund_used).await + } + + async fn do_request_inner( + self, + req: RequestPacket, + auto_fund_used: &AtomicBool, + ) -> TransportResult { let body = serde_json::to_vec(&req).map_err(TransportErrorKind::custom)?; let headers = req.headers(); @@ -246,15 +451,53 @@ where // held until the retry response is fully handled. let _pay_guard = self.provider.lock_pay().await; - let (resolved, challenge) = Self::select_challenge(&resp, &self.provider)?; + // No local key for any offered challenge → run device-code flow, + // invalidate the cached provider, and fetch a fresh 402 (the original + // may have expired during the browser/passkey flow). + let (resolved, challenge) = + if let Some(chain_id) = tempo_chain_needing_auth(&self.url, &resp) { + debug!(chain_id, "launching wallet.tempo authorization"); + let cfg = crate::tempo::EnsureAccessKeyConfig::from_env(chain_id); + crate::tempo::ensure_access_key(cfg).await.map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "tempo access key authorization failed: {e}" + ))) + })?; + self.provider.invalidate_cached_provider(); + self.fetch_fresh_challenge(&headers, &body).await? + } else { + Self::select_challenge(&resp, &self.provider)? + }; + let funding_ctx = self.provider.funding_context(&challenge); debug!(id = %challenge.id, method = %challenge.method, intent = %challenge.intent, "received MPP 402 challenge, paying"); - let credential = resolved.pay(&challenge).await.map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let credential = match resolved.pay(&challenge).await { + Ok(credential) => credential, + Err(e) => { + // Only the explicit `InsufficientBalance` variant is treated as + // a fundable error. Any other failure must surface unchanged so + // we don't mask payment/protocol issues behind a fund prompt. + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + resolved.pay(&challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { + self.provider.rollback_pending(); TransportErrorKind::custom(std::io::Error::other(format!( "failed to format MPP credential: {e}" ))) @@ -286,9 +529,20 @@ where self.provider.commit_topup_and_track_voucher(); let resolved = self.provider.resolve()?; - let voucher_resp = self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(voucher_resp).await; + let voucher_resp = + self.pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used).await?; + + // Route the voucher response through the funding-aware handler so + // a final 402 here also gets the fund retry / contextual help. + let result = self + .handle_response_or_retry_after_fund( + voucher_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -304,7 +558,7 @@ where self.provider.rollback_pending(); self.provider.clear_channels(); - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "MPP channel not found on server (410 Gone). \ The server may have restarted or the channel was closed externally.\n\ Local channel state has been cleared. Re-run to open a new channel.", @@ -333,10 +587,19 @@ where debug!("MPP voucher stale, retrying with fresh voucher"); let resolved = self.provider.resolve()?; if resolved.supports(challenge.method.as_str(), challenge.intent.as_str()) { - let final_resp = - self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.flush_pending(); } else { @@ -372,10 +635,19 @@ where let (resolved, fresh_challenge) = self.fetch_fresh_challenge(&headers, &body).await?; - let final_resp = - self.pay_and_retry(&fresh_challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -386,9 +658,40 @@ where } self.provider.rollback_pending(); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) + && maybe_auto_fund(auto_fund_used, &funding_ctx).await? + { + let (resolved, fresh_challenge) = + self.fetch_fresh_challenge(&headers, &body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; + if result.is_ok() { + self.provider.set_key_provisioned(true); + self.provider.flush_pending(); + } else { + self.provider.rollback_pending(); + } + return result; + } + + let mut error_text = format!("{retry_text}{diagnostics}"); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) { + error_text.push_str(&tempo_wallet_fund_help(&funding_ctx)); + } return Err(TransportErrorKind::http_error( StatusCode::PAYMENT_REQUIRED.as_u16(), - format!("{retry_text}{diagnostics}"), + error_text, )); } @@ -409,15 +712,32 @@ where provider: &P::Provider, headers: &reqwest::header::HeaderMap, body: &[u8], + auto_fund_used: &AtomicBool, ) -> TransportResult { - let credential = provider.pay(challenge).await.map_err(|e| { - self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let funding_ctx = self.provider.funding_context(challenge); + let credential = match provider.pay(challenge).await { + Ok(credential) => credential, + Err(e) => { + self.provider.rollback_pending(); + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + provider.pay(challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "failed to format MPP credential: {e}" ))) })?; @@ -437,6 +757,41 @@ where }) } + async fn handle_response_or_retry_after_fund( + &self, + resp: reqwest::Response, + headers: &reqwest::header::HeaderMap, + body: &[u8], + funding_ctx: &FundingContext, + auto_fund_used: &AtomicBool, + ) -> TransportResult { + if resp.status() != StatusCode::PAYMENT_REQUIRED { + return Self::handle_response_with_funding(resp, Some(funding_ctx)).await; + } + + let diagnostics = format_http_diagnostics(resp.headers()); + let status = resp.status(); + let resp_body = resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if should_suggest_tempo_fund(status, &resp_body) + && maybe_auto_fund(auto_fund_used, funding_ctx).await? + { + self.provider.rollback_pending(); + + let (resolved, fresh_challenge) = self.fetch_fresh_challenge(headers, body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, headers, body, auto_fund_used) + .await?; + return Self::handle_response_with_funding(final_resp, Some(funding_ctx)).await; + } + + let mut error_text = format!("{}{diagnostics}", String::from_utf8_lossy(&resp_body)); + if should_suggest_tempo_fund(status, &resp_body) { + error_text.push_str(&tempo_wallet_fund_help(funding_ctx)); + } + Err(TransportErrorKind::http_error(status.as_u16(), error_text)) + } + /// Fetch a fresh 402 challenge from the server (unauthenticated request). /// /// Returns `Ok(Some((provider, challenge)))` if the server returns a 402 @@ -462,7 +817,7 @@ where // Non-402 → return whatever the server sent (could be success or error). let result = Self::handle_response(fresh_resp).await; return Err(result.err().unwrap_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "unexpected success on unauthenticated fresh probe", )) })); @@ -477,25 +832,14 @@ where resp: &reqwest::Response, provider: &P, ) -> TransportResult<(P::Provider, mpp::protocol::core::PaymentChallenge)> { - let www_auth_values: Vec<&str> = resp - .headers() - .get_all(WWW_AUTHENTICATE_HEADER) - .iter() - .filter_map(|v| v.to_str().ok()) - .collect(); - - if www_auth_values.is_empty() { - return Err(TransportErrorKind::custom(std::io::Error::other(format!( + let challenges = parse_challenges(resp); + if challenges.is_empty() && resp.headers().get(WWW_AUTHENTICATE_HEADER).is_none() { + return Err(TransportErrorKind::custom(io::Error::other(format!( "402 response missing WWW-Authenticate header{}", format_http_diagnostics(resp.headers()) )))); } - let challenges: Vec<_> = parse_www_authenticate_all(www_auth_values) - .into_iter() - .filter_map(|r| r.ok()) - .collect(); - let mut last_resolve_err: Option = None; let resolved_pair = challenges.iter().find_map(|c| { let (chain_id, currency) = extract_challenge_chain_and_currency(c); @@ -515,7 +859,7 @@ where } let offered: Vec<_> = challenges.iter().map(|c| format!("{}.{}", c.method, c.intent)).collect(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "no supported MPP challenge; server offered [{}]", offered.join(", "), ))) @@ -523,6 +867,17 @@ where } async fn handle_response(resp: reqwest::Response) -> TransportResult { + Self::handle_response_with_funding(resp, None).await + } + + /// Like [`Self::handle_response`] but, when an unsuccessful 402 looks like a + /// fundable error, appends actionable `tempo wallet fund` help that uses + /// the per-request `FundingContext` (so the suggested command includes + /// `--address` and `--network` when known). + async fn handle_response_with_funding( + resp: reqwest::Response, + funding_ctx: Option<&FundingContext>, + ) -> TransportResult { let status = resp.status(); debug!(%status, "received response from MPP transport"); let diagnostics = format_http_diagnostics(resp.headers()); @@ -536,10 +891,19 @@ where } if !status.is_success() { - return Err(TransportErrorKind::http_error( - status.as_u16(), - format!("{}{diagnostics}", String::from_utf8_lossy(&body)), - )); + let mut body_text = format!("{}{diagnostics}", String::from_utf8_lossy(&body)); + if should_suggest_tempo_fund(status, &body) { + let default_ctx; + let ctx = match funding_ctx { + Some(c) => c, + None => { + default_ctx = FundingContext::default(); + &default_ctx + } + }; + body_text.push_str(&tempo_wallet_fund_help(ctx)); + } + return Err(TransportErrorKind::http_error(status.as_u16(), body_text)); } serde_json::from_slice(&body) @@ -547,6 +911,57 @@ where } } +/// Returns `Some(chain_id)` when a 402 response should trigger the +/// `wallet.tempo.xyz` device-code authorization flow. +/// +/// Conditions: known Tempo endpoint, interactive (TTY, not `CI`), and no +/// offered Tempo challenge resolves against a local key on `(chain, currency)`. +/// The picked chain matches the first unresolved challenge — same iteration +/// order [`MppHttpTransport::select_challenge`] uses. +fn tempo_chain_needing_auth(url: &Url, resp: &reqwest::Response) -> Option { + if !io::stderr().is_terminal() || env::var_os("CI").is_some() { + return None; + } + pick_chain_needing_auth(url, &parse_challenges(resp)) +} + +/// Extract all parseable MPP challenges from a 402 response's `WWW-Authenticate` headers. +fn parse_challenges(resp: &reqwest::Response) -> Vec { + let values: Vec<&str> = resp + .headers() + .get_all(WWW_AUTHENTICATE_HEADER) + .iter() + .filter_map(|v| v.to_str().ok()) + .collect(); + parse_www_authenticate_all(values).into_iter().filter_map(|r| r.ok()).collect() +} + +/// Inner logic of [`tempo_chain_needing_auth`], factored out for testing. +fn pick_chain_needing_auth( + url: &Url, + challenges: &[mpp::protocol::core::PaymentChallenge], +) -> Option { + if !crate::tempo::is_known_tempo_endpoint(url) { + return None; + } + + let tempo_challenges: Vec<_> = + challenges.iter().filter(|c| c.method.as_str() == "tempo").collect(); + + // If any challenge already resolves with a local key, no auth needed. + let any_resolvable = tempo_challenges.iter().any(|c| { + let (chain_id, currency) = extract_challenge_chain_and_currency(c); + let currency = currency.and_then(|s| s.parse().ok()); + super::keys::discover_mpp_config(super::keys::DiscoverOptions { chain_id, currency }) + .is_some() + }); + if any_resolvable { + return None; + } + + tempo_challenges.iter().find_map(|c| extract_challenge_chain_and_currency(c).0) +} + /// Extract `(chainId, currency)` from a parsed MPP challenge. pub(super) fn extract_challenge_chain_and_currency( c: &mpp::protocol::core::PaymentChallenge, @@ -576,10 +991,28 @@ pub(crate) trait ResolveProvider { fn flush_pending(&self) {} fn rollback_pending(&self) {} fn commit_topup_and_track_voucher(&self) {} + /// Drop any cached payment provider so the next `resolve_for` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry. + fn invalidate_cached_provider(&self) {} + fn funding_wallet_address(&self) -> Option { + None + } + fn funding_chain_id(&self) -> Option { + None + } + fn funding_context(&self, challenge: &mpp::protocol::core::PaymentChallenge) -> FundingContext { + let (challenge_chain_id, token) = extract_challenge_chain_and_currency(challenge); + FundingContext { + wallet_address: self.funding_wallet_address(), + token, + chain_id: challenge_chain_id.or_else(|| self.funding_chain_id()).map(Chain::from_id), + } + } /// Acquire the payment serialization lock. The returned guard must be held /// across the entire 402 → pay → retry → response cycle to prevent /// concurrent channel opens and colliding expiring-nonce transactions. - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn lock_pay(&self) -> impl Future>> + Send { async { None } } } @@ -599,7 +1032,7 @@ impl ResolveProvider for LazySessionProvider { // regardless of opts. Re-check that the provider's key is compatible // with this challenge's chain/currency. if !provider.matches_challenge(opts.chain_id, opts.currency) { - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "cached provider does not match challenge chain/currency", ))); } @@ -623,7 +1056,16 @@ impl ResolveProvider for LazySessionProvider { fn commit_topup_and_track_voucher(&self) { Self::commit_topup_and_track_voucher(self) } - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn invalidate_cached_provider(&self) { + Self::invalidate(self) + } + fn funding_wallet_address(&self) -> Option { + self.inner.lock().unwrap().as_ref().map(|p| p.funding_wallet_address()) + } + fn funding_chain_id(&self) -> Option { + self.inner.lock().unwrap().as_ref().and_then(|p| p.key_chain_id()) + } + fn lock_pay(&self) -> impl Future>> + Send { let lock = self.pay_lock.clone(); async move { Some(lock.lock_owned().await) } } @@ -685,7 +1127,7 @@ mod tests { fn pay( &self, challenge: &PaymentChallenge, - ) -> impl std::future::Future> + Send { + ) -> impl Future> + Send { let echo = challenge.to_echo(); async move { Ok(PaymentCredential::with_source( @@ -697,6 +1139,21 @@ mod tests { } } + #[derive(Clone, Debug)] + struct InsufficientBalanceProvider; + + impl PaymentProvider for InsufficientBalanceProvider { + fn supports(&self, method: &str, intent: &str) -> bool { + method == "tempo" && (intent == "session" || intent == "charge") + } + + async fn pay(&self, _challenge: &PaymentChallenge) -> Result { + Err(MppError::InsufficientBalance(Some( + "wallet has 0 pathUSD but needs 100000".to_string(), + ))) + } + } + fn test_challenge() -> (PaymentChallenge, String) { let request = Base64UrlJson::from_value(&serde_json::json!({ "amount": "1000", @@ -853,8 +1310,238 @@ mod tests { handle.abort(); } + #[tokio::test] + async fn test_mpp_transport_payment_failure_suggests_tempo_wallet_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move || { + let www_auth = www_auth.clone(); + async move { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required", + ) + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + InsufficientBalanceProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + #[tokio::test] + async fn test_mpp_transport_retry_402_insufficient_balance_suggests_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail( + "Insufficient pathUSD balance: have 0, need 100000", + ), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("InsufficientBalanceError"), "got: {msg}"); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + /// Generic `verification-failed` has many non-funding causes (bad signature, + /// replay, expired challenge, clock skew, ...). The transport must surface + /// the original error verbatim and must NOT add a "fund your wallet" hint. + #[tokio::test] + async fn test_mpp_transport_final_402_verification_failed_does_not_suggest_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Verification Failed"), "got: {msg}"); + assert!( + !msg.contains("Tempo wallet payment could not be funded"), + "verification-failed must not be classified as fundable; got: {msg}" + ); + + handle.abort(); + } + + // --- Classifier unit tests -------------------------------------------- + + #[test] + fn classifier_only_triggers_on_explicit_insufficient_balance_problem() { + // explicit insufficient-balance → true + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail("Insufficient pathUSD balance"), + ) + .unwrap(); + assert!(should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_verification_failed() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_unrelated_text_with_balance_words() { + // Free-text 402 body that just happens to mention the word "balance" + // must NOT trigger the fund suggestion (no structured problem details). + let body = + b"402 Payment Required: server could not balance ledger entries; insufficient inputs."; + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, body)); + } + + #[test] + fn classifier_does_not_trigger_outside_402() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_detail("Insufficient balance"), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::INTERNAL_SERVER_ERROR, &body)); + assert!(!should_suggest_tempo_fund(StatusCode::OK, &body)); + } + + #[test] + fn fund_help_includes_address_and_network_for_known_chain() { + let ctx = FundingContext { + wallet_address: Some("0x000000000000000000000000000000000000dEaD".parse().unwrap()), + token: Some("0x20c0".to_string()), + chain_id: Some(Chain::from_id(42431)), + }; + let help = tempo_wallet_fund_help(&ctx); + assert!(help.contains("--address 0x"), "missing --address: {help}"); + assert!(help.contains("--network tempo-moderato"), "missing --network: {help}"); + assert!(help.contains("--no-browser"), "missing --no-browser: {help}"); + assert!(help.contains("Requested payment token: 0x20c0"), "missing token: {help}"); + + let mainnet = FundingContext { chain_id: Some(Chain::from_id(4217)), ..ctx }; + let help2 = tempo_wallet_fund_help(&mainnet); + assert!(help2.contains("--network tempo"), "missing tempo network: {help2}"); + } + + #[test] + fn auto_fund_policy_blocks_in_ci_and_non_tty() { + assert!(!interactive_tempo_fund_allowed(Some("1"), true, true, true), "must not run in CI"); + assert!( + interactive_tempo_fund_allowed(Some("0"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=0 must not disable" + ); + assert!( + interactive_tempo_fund_allowed(Some("false"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=false must not disable" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, false, true), + "stdin must be a terminal" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, true, false), + "stderr must be a terminal" + ); + assert!(!interactive_tempo_fund_allowed(Some("1"), false, true, true)); + assert!(!interactive_tempo_fund_allowed(Some("true"), false, true, true)); + assert!(interactive_tempo_fund_allowed(None, false, true, true)); + } + #[tokio::test] async fn test_plain_http_402_shows_mpp_setup_instructions() { + let _g = crate::tempo::test_env_mutex().lock().await; let (_, www_auth) = test_challenge(); let app = axum::Router::new().route( @@ -920,6 +1607,32 @@ mod tests { ); } + /// `invalidate_cached_provider` clears the cache so the next + /// `get_or_init` re-runs discovery — the path `do_request` takes after + /// `ensure_access_key` writes a fresh `keys.toml` entry. + #[tokio::test] + async fn lazy_session_provider_invalidate_clears_cache() { + let _g = crate::tempo::test_env_mutex().lock().await; + // TEMPO_PRIVATE_KEY lets discovery succeed without a keys.toml. + let key_hex = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + unsafe { + std::env::set_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV, key_hex); + std::env::remove_var(crate::tempo::TEMPO_HOME_ENV); + } + + let lazy = LazySessionProvider::new("https://rpc.example.com".into()); + let _ = lazy.get_or_init(Default::default()).expect("discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected provider to be cached"); + + ResolveProvider::invalidate_cached_provider(&lazy); + assert!(lazy.inner.lock().unwrap().is_none(), "expected cache to be cleared"); + + let _ = lazy.get_or_init(Default::default()).expect("re-discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected re-init to repopulate cache"); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV) }; + } + #[test] fn challenge_chain_and_currency_extraction() { let extract = |headers: Vec<&str>| -> Vec<(Option, Option)> { @@ -955,4 +1668,73 @@ mod tests { ); assert_eq!(extract(vec![&no_details]), vec![(None, Some("0x20c0".into()))]); } + + /// Auth must trigger when a key matches the chain but not the currency. + #[test] + fn pick_chain_needing_auth_currency_aware() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let dir = tempfile::tempdir().unwrap(); + let wallet = dir.path().join("wallet"); + std::fs::create_dir_all(&wallet).unwrap(); + std::fs::write( + wallet.join("keys.toml"), + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +chain_id = 4217 + +[[keys.limits]] +currency = "0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +limit = "1000" +"#, + ) + .unwrap(); + unsafe { + std::env::set_var(crate::tempo::TEMPO_HOME_ENV, dir.path()); + std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV); + } + + let url = Url::parse("https://rpc.mpp.tempo.xyz").unwrap(); + let mk = |currency: &str| -> PaymentChallenge { + PaymentChallenge { + id: "x".into(), + realm: "api".into(), + method: MethodName::new("tempo"), + intent: IntentName::new("charge"), + request: Base64UrlJson::from_value(&serde_json::json!({ + "amount": "1", + "currency": currency, + "recipient": "0xabc", + "methodDetails": { "chainId": 4217 } + })) + .unwrap(), + expires: None, + description: None, + digest: None, + opaque: None, + } + }; + + // Currency mismatch → auth needed. + let mismatched = mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + assert_eq!(pick_chain_needing_auth(&url, &[mismatched]), Some(4217)); + + // Currency match → no auth. + let matched = mk("0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert_eq!(pick_chain_needing_auth(&url, &[matched]), None); + + // Non-Tempo host → never triggers, even without a key. + let stripe_url = Url::parse("https://api.stripe.com").unwrap(); + assert_eq!( + pick_chain_needing_auth( + &stripe_url, + &[mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")] + ), + None, + ); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_HOME_ENV) }; + } } diff --git a/crates/common/src/provider/mpp/ws.rs b/crates/common/src/provider/mpp/ws.rs index f631d0b08a2fc..69aef7d4f4cbc 100644 --- a/crates/common/src/provider/mpp/ws.rs +++ b/crates/common/src/provider/mpp/ws.rs @@ -378,6 +378,8 @@ mod tests { /// MPP server sends challenge → client pays → server sends receipt. #[tokio::test] async fn test_ws_mpp_challenge_credential_receipt() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); @@ -452,6 +454,8 @@ mod tests { /// MPP server sends challenge, client pays, server closes → rollback. #[tokio::test] async fn test_ws_mpp_rollback_on_post_pay_close() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); diff --git a/crates/common/src/provider/runtime_transport.rs b/crates/common/src/provider/runtime_transport.rs index 7db1ebd1b3f91..f59a2efa75b8e 100644 --- a/crates/common/src/provider/runtime_transport.rs +++ b/crates/common/src/provider/runtime_transport.rs @@ -36,7 +36,11 @@ fn is_known_mpp_endpoint(url: &Url) -> bool { /// Only meant to be used internally by [RuntimeTransport]. #[derive(Clone, Debug)] pub enum InnerTransport { - /// HTTP transport with lazy MPP 402 handling + /// HTTP transport with lazy MPP 402 handling. + /// + /// For known Tempo endpoints, the MPP layer additionally runs the + /// `wallet.tempo.xyz` device-code flow on a 402 when no local access key + /// is configured (see [`crate::tempo::ensure_access_key`]). Http(LazyMppHttpTransport), /// WebSocket transport Ws(PubSubFrontend), diff --git a/crates/common/src/tempo/auth.rs b/crates/common/src/tempo/auth.rs new file mode 100644 index 0000000000000..d79306cfb74f2 --- /dev/null +++ b/crates/common/src/tempo/auth.rs @@ -0,0 +1,494 @@ +//! Tempo wallet device-code authorization flow. +//! +//! Implements the CLI side of the tempoxyz/accounts `cli-auth` device-code +//! protocol: generates a local secp256k1 access key, creates a PKCE-protected +//! device code, opens `wallet.tempo.xyz/cli-auth?code=` in the browser, +//! polls until the user authorizes the key on their passkey wallet, and writes +//! the resulting `keyAuthorization` to `~/.tempo/wallet/keys.toml`. + +use crate::tempo::{ + KeyEntry, KeyType, StoredTokenLimit, WalletType, decode_key_authorization, upsert_key_entry, +}; +use alloy_primitives::{Address, B256, hex}; +use alloy_signer_local::PrivateKeySigner; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use eyre::Result; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +#[cfg(any(unix, windows))] +use std::process::Command; +use std::{ + env, + sync::LazyLock, + time::{Duration, Instant}, +}; +use tempo_primitives::transaction::{SignatureType, SignedKeyAuthorization}; +use tokio::sync::Mutex; + +/// Default device-code service URL (production wallet.tempo.xyz). +const DEFAULT_CLI_AUTH_URL: &str = "https://wallet.tempo.xyz/cli-auth"; + +/// Returns `true` if `url`'s host is `tempo.xyz` or a subdomain of it. +pub(crate) fn is_known_tempo_endpoint(url: &url::Url) -> bool { + url.host_str().is_some_and(|host| host == "tempo.xyz" || host.ends_with(".tempo.xyz")) +} + +/// Env var to override the device-code service URL (for tests / staging). +const TEMPO_CLI_AUTH_URL_ENV: &str = "TEMPO_CLI_AUTH_URL"; + +const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(2); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); + +/// Per-process serialization of concurrent `ensure_access_key` calls. +/// +/// Prevents two `cast` invocations in the same process from racing two browser +/// popups for the same chain. +static AUTH_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +/// Configuration for [`ensure_access_key`]. +#[derive(Clone, Debug)] +pub struct EnsureAccessKeyConfig { + /// Chain ID the access key is being authorized for. + pub chain_id: u64, + /// Device-code service base URL. Defaults to [`DEFAULT_CLI_AUTH_URL`]. + pub(crate) service_url: String, + /// Poll interval. + pub(crate) poll_interval: Duration, + /// Total timeout for the authorization flow. + pub(crate) timeout: Duration, + /// If `true`, print the authorization URL to stderr instead of opening a + /// browser. + pub no_browser: bool, +} + +impl EnsureAccessKeyConfig { + /// Build a config from the environment for the given chain. + /// + /// `no_browser` defaults to `true` under `CI`; callers (e.g. `cast tempo + /// login --no-browser`) may override it. + pub fn from_env(chain_id: u64) -> Self { + Self { + chain_id, + service_url: env::var(TEMPO_CLI_AUTH_URL_ENV) + .unwrap_or_else(|_| DEFAULT_CLI_AUTH_URL.to_string()), + poll_interval: DEFAULT_POLL_INTERVAL, + timeout: DEFAULT_TIMEOUT, + no_browser: env::var_os("CI").is_some(), + } + } +} + +/// Open `url` via the OS default browser handler. On platforms without a known +/// opener, this is a no-op (the URL is still printed by [`ensure_access_key`]). +fn open_browser(_url: &str) { + #[cfg(target_os = "macos")] + let _ = Command::new("open").arg(_url).spawn(); + #[cfg(target_os = "windows")] + let _ = Command::new("cmd").args(["/c", "start", "", _url]).spawn(); + #[cfg(all(unix, not(target_os = "macos")))] + let _ = Command::new("xdg-open").arg(_url).spawn(); +} + +/// Result of [`ensure_access_key`]. +#[derive(Debug, Clone)] +pub struct AccessKeyOutcome { + pub wallet_address: Address, + pub key_address: Address, + pub chain_id: u64, +} + +/// Run the device-code flow, persist the resulting key to `keys.toml`, and +/// return the new entry's identifying fields. +pub async fn ensure_access_key(cfg: EnsureAccessKeyConfig) -> Result { + let _guard = AUTH_LOCK.lock().await; + + let signer = PrivateKeySigner::random(); + let key_address = signer.address(); + // The server requires uncompressed SEC1 (65-byte `0x04 || X || Y`); the + // default `to_sec1_bytes()` would emit the compressed 33-byte form. + let pub_key_hex = format!( + "0x{}", + hex::encode(signer.credential().verifying_key().to_encoded_point(false).as_bytes()), + ); + + let code_verifier = random_code_verifier(); + let client = reqwest::Client::builder().timeout(Duration::from_secs(30)).build()?; + let service = cfg.service_url.trim_end_matches('/'); + + let create_req = CreateCodeRequest { + chain_id: cfg.chain_id, + code_challenge: sha256_b64url(&code_verifier), + key_type: "secp256k1", + pub_key: pub_key_hex, + }; + let code = create_code_with_retry(&client, service, &create_req, cfg.timeout).await?; + + let browser_url = format!("{service}?code={code}"); + if cfg.no_browser { + let _ = crate::sh_eprintln!("Open this URL to authorize: {browser_url}"); + } else { + let _ = crate::sh_eprintln!( + "Opening wallet.tempo to authorize an access key…\n {browser_url}" + ); + open_browser(&browser_url); + } + + let poll = PollRequest { code_verifier }; + let started = Instant::now(); + loop { + // Retry transient network/5xx/429 failures within `cfg.timeout`. + let send_res = client.post(format!("{service}/poll/{code}")).json(&poll).send().await; + + let resp = match send_res { + Ok(r) => r, + Err(e) if is_transient_error(&e) && started.elapsed() < cfg.timeout => { + tracing::debug!(error = %e, "transient error polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + Err(e) => return Err(e.into()), + }; + + let status = resp.status(); + if !status.is_success() { + if is_transient_status(status) && started.elapsed() < cfg.timeout { + tracing::debug!(%status, "transient HTTP status polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code poll failed ({status}): {body}"); + } + + let body: PollResponse = resp.json().await?; + match body { + PollResponse::Pending => { + if started.elapsed() > cfg.timeout { + eyre::bail!("timed out waiting for wallet authorization (code {code})"); + } + tokio::time::sleep(cfg.poll_interval).await; + } + PollResponse::Expired => { + eyre::bail!("device code {code} expired before authorization"); + } + PollResponse::Authorized { account_address, key_authorization } => { + let hex_str = key_authorization.ok_or_else(|| { + eyre::eyre!("wallet authorized response missing key_authorization") + })?; + let signed: SignedKeyAuthorization = decode_key_authorization(&hex_str)?; + // Reject mismatches before persisting — an unusable keys.toml + // entry would silently break the next 402 retry. + if signed.authorization.key_id != key_address { + eyre::bail!( + "wallet authorized key {} but the locally generated key is {}", + signed.authorization.key_id, + key_address, + ); + } + if signed.authorization.chain_id != cfg.chain_id { + eyre::bail!( + "wallet authorized chain {} but {} was requested", + signed.authorization.chain_id, + cfg.chain_id, + ); + } + if signed.authorization.key_type != SignatureType::Secp256k1 { + eyre::bail!( + "wallet returned keyType {:?} but secp256k1 was requested", + signed.authorization.key_type, + ); + } + let chain_id = signed.authorization.chain_id; + let key_authorization = + if hex_str.starts_with("0x") { hex_str } else { format!("0x{hex_str}") }; + let entry = KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: account_address, + chain_id, + key_type: match signed.authorization.key_type { + SignatureType::P256 => KeyType::P256, + SignatureType::WebAuthn => KeyType::WebAuthn, + _ => KeyType::Secp256k1, + }, + key_address: Some(key_address), + key: Some(format!("0x{}", hex::encode(signer.to_bytes()))), + key_authorization: Some(key_authorization), + expiry: signed.authorization.expiry.map(|n| n.get()), + limits: signed + .authorization + .limits + .unwrap_or_default() + .into_iter() + .map(|l| StoredTokenLimit { currency: l.token, limit: l.limit.to_string() }) + .collect(), + }; + upsert_key_entry(entry)?; + return Ok(AccessKeyOutcome { + wallet_address: account_address, + key_address, + chain_id, + }); + } + } + } +} + +fn is_transient_error(err: &reqwest::Error) -> bool { + err.is_timeout() || err.is_connect() || err.is_request() +} + +fn is_transient_status(status: reqwest::StatusCode) -> bool { + status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS +} + +/// POST `/code` with exponential backoff on transient errors, bounded by `timeout`. +async fn create_code_with_retry( + client: &reqwest::Client, + service: &str, + req: &CreateCodeRequest, + timeout: Duration, +) -> Result { + let started = Instant::now(); + let mut backoff = Duration::from_millis(500); + loop { + let send_res = client.post(format!("{service}/code")).json(req).send().await; + + match send_res { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + let CreateCodeResponse { code } = resp.json().await?; + return Ok(code); + } + if is_transient_status(status) && started.elapsed() < timeout { + tracing::debug!(%status, "transient HTTP status creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code create failed ({status}): {body}"); + } + Err(e) if is_transient_error(&e) && started.elapsed() < timeout => { + tracing::debug!(error = %e, "transient error creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + } + Err(e) => return Err(e.into()), + } + } +} + +fn random_code_verifier() -> String { + let bytes = B256::random(); + URL_SAFE_NO_PAD.encode(bytes.as_slice()) +} + +fn sha256_b64url(input: &str) -> String { + let digest = Sha256::digest(input.as_bytes()); + URL_SAFE_NO_PAD.encode(digest) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CreateCodeRequest { + /// `0x`-hex per the SDK schema (server accepts hex string or bigint, not a plain JSON number). + #[serde(serialize_with = "serialize_u64_hex")] + chain_id: u64, + code_challenge: String, + key_type: &'static str, + pub_key: String, +} + +fn serialize_u64_hex(v: &u64, s: S) -> std::result::Result { + s.serialize_str(&format!("0x{v:x}")) +} + +#[derive(Deserialize)] +struct CreateCodeResponse { + code: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PollRequest { + code_verifier: String, +} + +/// Matches `tempoxyz/wallet` poll response shape. +#[derive(Deserialize)] +#[serde(tag = "status", rename_all = "lowercase")] +enum PollResponse { + Pending, + Expired, + Authorized { + account_address: Address, + #[serde(default)] + key_authorization: Option, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tempo::{TEMPO_HOME_ENV, read_tempo_keys_file, test_env_mutex}; + use axum::{Json, Router, extract::State, routing::post}; + use std::sync::{Arc, Mutex}; + + #[test] + fn pkce_challenge_matches_sdk_format() { + // Vector from RFC 7636 §4.2. + let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + let challenge = sha256_b64url(verifier); + assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); + } + + /// Recover the EOA from a SEC1-encoded public key (compressed or + /// uncompressed). + fn address_from_sec1_hex(s: &str) -> Address { + let stripped = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(stripped).expect("valid hex"); + let vk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes).expect("valid SEC1 pubkey"); + Address::from_public_key(&vk) + } + + #[derive(Clone)] + struct MockState { + wallet: Arc>>, + /// Derived from the `pubKey` posted to `/code` so `/poll` can echo + /// back a matching `keyId`, like a real wallet would. + key_id: Arc>>, + /// Chain ID the mock `/poll` returns in `keyAuthorization`. + poll_chain_id: u64, + } + + async fn create_code_handler( + State(state): State, + Json(body): Json, + ) -> Json { + // Sanity: required fields present and chainId is a 0x-hex string, + // matching the SDK wire format the live server enforces. + let pub_key = body + .get("pubKey") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("pubKey missing: {body}")); + assert!(body.get("codeChallenge").is_some(), "codeChallenge missing: {body}"); + let chain_id = body.get("chainId").unwrap_or_else(|| panic!("chainId missing: {body}")); + let chain_str = chain_id + .as_str() + .unwrap_or_else(|| panic!("chainId must be string, got {chain_id}: {body}")); + assert!(chain_str.starts_with("0x"), "chainId must be 0x-hex, got {chain_str}"); + let wallet: Address = "0x0000000000000000000000000000000000000042".parse().unwrap(); + *state.wallet.lock().unwrap() = Some(wallet); + *state.key_id.lock().unwrap() = Some(address_from_sec1_hex(pub_key)); + Json(serde_json::json!({ "code": "ABCDEFGH" })) + } + + /// Build the RLP-hex `SignedKeyAuthorization` blob the live server returns + /// in the `key_authorization` field. + fn signed_key_auth_hex(chain_id: u64, key_id: Address, expiry: u64) -> String { + use alloy_rlp::Encodable; + use tempo_primitives::transaction::{KeyAuthorization, PrimitiveSignature}; + let auth = KeyAuthorization::unrestricted(chain_id, SignatureType::Secp256k1, key_id) + .with_expiry(expiry); + let sig: PrimitiveSignature = serde_json::from_value(serde_json::json!({ + "type": "secp256k1", "r": "0x0", "s": "0x0", "yParity": 0 + })) + .unwrap(); + let signed = auth.into_signed(sig); + let mut buf = Vec::new(); + signed.encode(&mut buf); + format!("0x{}", hex::encode(buf)) + } + + async fn poll_handler(State(state): State) -> Json { + let wallet = state.wallet.lock().unwrap().expect("create_code must be called first"); + let key_id = state.key_id.lock().unwrap().expect("create_code must be called first"); + Json(serde_json::json!({ + "status": "authorized", + "account_address": wallet, + "key_authorization": signed_key_auth_hex(state.poll_chain_id, key_id, 9_999_999_999), + })) + } + + /// Spawn a mock wallet.tempo server whose `/poll` echoes `poll_chain_id`. + async fn spawn_mock_wallet(poll_chain_id: u64) -> (String, tokio::task::JoinHandle<()>) { + let app = Router::new() + .route("/code", post(create_code_handler)) + .route("/poll/{code}", post(poll_handler)) + .with_state(MockState { + wallet: Arc::default(), + key_id: Arc::default(), + poll_chain_id, + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let handle = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + (format!("http://{addr}"), handle) + } + + fn test_cfg(service_url: String) -> EnsureAccessKeyConfig { + EnsureAccessKeyConfig { + chain_id: 4217, + service_url, + poll_interval: Duration::from_millis(10), + timeout: Duration::from_secs(2), + no_browser: true, + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_happy_path_writes_keys_toml() { + // SAFETY: serialized with other tests that mutate TEMPO_HOME. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(4217).await; + let outcome = ensure_access_key(test_cfg(service_url)).await.unwrap(); + + let expected_wallet: Address = + "0x0000000000000000000000000000000000000042".parse().unwrap(); + assert_eq!(outcome.chain_id, 4217); + assert_eq!(outcome.wallet_address, expected_wallet); + + let file = read_tempo_keys_file().expect("keys.toml written"); + assert_eq!(file.keys.len(), 1); + let entry = &file.keys[0]; + assert_eq!(entry.wallet_address, outcome.wallet_address); + assert_eq!(entry.key_address, Some(outcome.key_address)); + assert_eq!(entry.chain_id, 4217); + assert_eq!(entry.expiry, Some(9_999_999_999)); + let decoded: tempo_primitives::transaction::SignedKeyAuthorization = + crate::tempo::decode_key_authorization(entry.key_authorization.as_deref().unwrap()) + .expect("RLP roundtrip"); + assert_eq!(decoded.authorization.chain_id, 4217); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_rejects_wrong_chain_id() { + // Wallet returns chain 99999 but client requested 4217 → must reject + // and persist nothing, else discovery would later fail to find a key + // for the requested chain. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(99999).await; + let err = ensure_access_key(test_cfg(service_url)).await.unwrap_err(); + assert!( + err.to_string().contains("wallet authorized chain 99999 but 4217 was requested"), + "expected chain mismatch error, got: {err}" + ); + assert!(read_tempo_keys_file().is_none_or(|f| f.keys.is_empty())); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } +} diff --git a/crates/common/src/tempo/keystore.rs b/crates/common/src/tempo/keystore.rs index 18edf39be59bd..b4f9527d1b106 100644 --- a/crates/common/src/tempo/keystore.rs +++ b/crates/common/src/tempo/keystore.rs @@ -5,8 +5,8 @@ use alloy_primitives::{Address, hex}; use alloy_rlp::Decodable; -use serde::Deserialize; -use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use std::{env, fs, io::Write, path::PathBuf}; /// Environment variable for an ephemeral Tempo private key. pub const TEMPO_PRIVATE_KEY_ENV: &str = "TEMPO_PRIVATE_KEY"; @@ -21,7 +21,7 @@ pub const DEFAULT_TEMPO_HOME: &str = ".tempo"; pub const WALLET_KEYS_PATH: &str = "wallet/keys.toml"; /// Wallet type matching `tempo-common`'s `WalletType` enum. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum WalletType { #[default] @@ -30,7 +30,7 @@ pub enum WalletType { } /// Cryptographic key type. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum KeyType { #[default] @@ -40,7 +40,7 @@ pub enum KeyType { } /// Per-token spending limit stored in `keys.toml`. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct StoredTokenLimit { pub currency: Address, pub limit: String, @@ -50,7 +50,7 @@ pub struct StoredTokenLimit { /// /// Mirrors the fields from `tempo-common::keys::model::KeyEntry`. /// Unknown fields are ignored by serde. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct KeyEntry { /// Wallet type: "local" or "passkey". #[serde(default)] @@ -65,20 +65,20 @@ pub struct KeyEntry { #[serde(default)] pub key_type: KeyType, /// Key address (the EOA derived from the private key). - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key_address: Option
, /// Key private key, stored inline in keys.toml. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key: Option, /// RLP-encoded signed key authorization (hex string). /// Used in keychain mode to atomically provision the access key on-chain. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub key_authorization: Option, /// Expiry timestamp. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub expiry: Option, /// Per-token spending limits. - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub limits: Vec, } @@ -90,17 +90,27 @@ impl KeyEntry { } /// The top-level structure of `keys.toml`. -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct KeysFile { #[serde(default)] pub keys: Vec, } +/// Process-wide mutex used by tests that mutate `TEMPO_HOME`. +/// +/// Returns a [`tokio::sync::Mutex`] so async tests can hold it across `.await` +/// points without tripping `clippy::await_holding_lock`. +#[cfg(test)] +pub(crate) fn test_env_mutex() -> &'static tokio::sync::Mutex<()> { + static M: std::sync::OnceLock> = std::sync::OnceLock::new(); + M.get_or_init(|| tokio::sync::Mutex::new(())) +} + /// Resolve the Tempo home directory. /// /// Uses `TEMPO_HOME` env var if set, otherwise `~/.tempo`. pub fn tempo_home() -> Option { - if let Ok(home) = std::env::var(TEMPO_HOME_ENV) { + if let Ok(home) = env::var(TEMPO_HOME_ENV) { return Some(PathBuf::from(home)); } dirs::home_dir().map(|h| h.join(DEFAULT_TEMPO_HOME)) @@ -122,7 +132,7 @@ pub fn read_tempo_keys_file() -> Option { return None; } - let contents = match std::fs::read_to_string(&keys_path) { + let contents = match fs::read_to_string(&keys_path) { Ok(c) => c, Err(e) => { tracing::warn!(?keys_path, %e, "failed to read tempo keys file"); @@ -148,3 +158,112 @@ pub fn decode_key_authorization(hex_str: &str) -> eyre::Result let auth = T::decode(&mut bytes.as_slice())?; Ok(auth) } + +/// Atomically upsert a [`KeyEntry`] into `keys.toml`. +/// +/// Replaces any existing entry for the same `(wallet_address, chain_id)`. +/// Each Tempo wallet has at most one active access key per chain, so a fresh +/// login always supersedes the previous entry regardless of the new key +/// address. Creates the file (and parent directories) if missing. Writes via +/// temp file + rename so a crash mid-write cannot corrupt the file. +pub(crate) fn upsert_key_entry(entry: KeyEntry) -> eyre::Result<()> { + let path = tempo_keys_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?; + let dir = path.parent().ok_or_else(|| eyre::eyre!("invalid keys path: {}", path.display()))?; + fs::create_dir_all(dir)?; + + let mut file = read_tempo_keys_file().unwrap_or_default(); + file.keys + .retain(|k| !(k.wallet_address == entry.wallet_address && k.chain_id == entry.chain_id)); + file.keys.push(entry); + + let body = toml::to_string_pretty(&file)?; + let contents = format!( + "# Tempo wallet keys — managed by Foundry / Tempo CLI.\n# Do not edit manually.\n\n{body}" + ); + + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; + tmp.write_all(contents.as_bytes())?; + tmp.flush()?; + tmp.persist(&path).map_err(|e| eyre::eyre!("failed to persist keys.toml: {e}"))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + fn with_tempo_home(f: F) { + let tmp = tempfile::tempdir().unwrap(); + // SAFETY: process-global env access is serialized via the shared mutex. + let _g = test_env_mutex().blocking_lock(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + f(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[test] + fn upsert_replaces_matching_entry_atomically() { + with_tempo_home(|| { + let wallet = Address::from_str("0x0000000000000000000000000000000000000001").unwrap(); + let key = Address::from_str("0x0000000000000000000000000000000000000abc").unwrap(); + + let mk = |expiry: u64| KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: wallet, + chain_id: 4217, + key_type: KeyType::Secp256k1, + key_address: Some(key), + key: Some("0xdead".to_string()), + key_authorization: Some("0xbeef".to_string()), + expiry: Some(expiry), + limits: vec![], + }; + + upsert_key_entry(mk(100)).unwrap(); + upsert_key_entry(mk(200)).unwrap(); + + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 1); + assert_eq!(file.keys[0].expiry, Some(200)); + + // Different chain_id => separate entry. + let mut other = mk(300); + other.chain_id = 42431; + upsert_key_entry(other).unwrap(); + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 2); + }); + } + + #[test] + fn upsert_replaces_when_key_address_changes() { + // Re-login produces a fresh random key address; the new entry must + // supersede the old one for the same (wallet, chain), not coexist. + with_tempo_home(|| { + let wallet = Address::from_str("0x0000000000000000000000000000000000000001").unwrap(); + let old_key = Address::from_str("0x000000000000000000000000000000000000aaaa").unwrap(); + let new_key = Address::from_str("0x000000000000000000000000000000000000bbbb").unwrap(); + + let mk = |key_addr: Address| KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: wallet, + chain_id: 4217, + key_type: KeyType::Secp256k1, + key_address: Some(key_addr), + key: Some("0xdead".to_string()), + key_authorization: Some("0xbeef".to_string()), + expiry: Some(100), + limits: vec![], + }; + + upsert_key_entry(mk(old_key)).unwrap(); + upsert_key_entry(mk(new_key)).unwrap(); + + let file = read_tempo_keys_file().unwrap(); + assert_eq!(file.keys.len(), 1, "old entry must be replaced, not duplicated"); + assert_eq!(file.keys[0].key_address, Some(new_key)); + }); + } +} diff --git a/crates/common/src/tempo/mod.rs b/crates/common/src/tempo/mod.rs index ec51dc607b5ab..ef8d0212bd453 100644 --- a/crates/common/src/tempo/mod.rs +++ b/crates/common/src/tempo/mod.rs @@ -1,8 +1,24 @@ //! Tempo network utilities. +pub mod auth; + +use crate::FoundryTransactionBuilder; +use alloy_network::Network; +use alloy_primitives::{Address, B256, Signature}; +use alloy_signer::Signer; +use eyre::{Context, Result}; +use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; +use std::sync::Arc; + mod keystore; + +pub(crate) use auth::is_known_tempo_endpoint; +pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key}; pub use keystore::*; +#[cfg(test)] +pub(crate) use keystore::test_env_mutex; + #[cfg(test)] mod tests; @@ -16,3 +32,173 @@ mod tests; /// /// See pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000; + +/// Gas sponsor configuration for Tempo fee-payer signatures. +#[derive(Clone, Debug)] +pub struct TempoSponsor { + sponsor: Address, + signer: Option>, + signature: Option, +} + +impl TempoSponsor { + pub const fn new( + sponsor: Address, + signer: Option>, + signature: Option, + ) -> Self { + Self { sponsor, signer, signature } + } + + pub const fn sponsor(&self) -> Address { + self.sponsor + } + + pub async fn attach_and_print( + &self, + tx: &mut N::TransactionRequest, + sender: Address, + ) -> Result + where + N::TransactionRequest: FoundryTransactionBuilder, + { + if self.sponsor == sender { + eyre::bail!( + "invalid Tempo sponsorship: sponsor {} must not equal transaction sender", + self.sponsor + ); + } + + let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| { + eyre::eyre!( + "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction" + ) + })?; + + let preview = TempoSponsorPreview { + sponsor: self.sponsor, + fee_token: tx.fee_token(), + valid_before: tx.valid_before().map(|v| v.get()), + valid_after: tx.valid_after().map(|v| v.get()), + digest, + }; + preview.print()?; + + let signature = if let Some(signature) = self.signature { + signature + } else if let Some(signer) = &self.signer { + signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")? + } else { + eyre::bail!("missing Tempo sponsor signature or signer") + }; + + let recovered = signature + .recover_address_from_prehash(&digest) + .context("failed to recover Tempo sponsor signature")?; + if recovered != self.sponsor { + eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor); + } + if recovered == sender { + eyre::bail!( + "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender" + ); + } + + tx.set_fee_payer_signature(signature); + Ok(preview) + } +} + +/// User-visible sponsor digest metadata for a single outgoing Tempo transaction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TempoSponsorPreview { + pub sponsor: Address, + pub fee_token: Option
, + pub valid_before: Option, + pub valid_after: Option, + pub digest: B256, +} + +impl TempoSponsorPreview { + pub fn print(&self) -> Result<()> { + crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?; + crate::sh_eprintln!( + "Tempo fee token: {}", + self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string()) + )?; + crate::sh_eprintln!( + "Tempo validity: after {}, before {}", + self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()), + self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string()) + )?; + crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?; + Ok(()) + } +} + +/// Resolves a `--tempo.sponsor-signer` URI into a Foundry wallet signer. +pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result { + let spec = spec.trim(); + let (scheme, value) = spec + .split_once("://") + .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value)) + .unwrap_or_else(|| (spec.to_ascii_lowercase(), "")); + + match scheme.as_str() { + "env" => { + if value.is_empty() { + eyre::bail!("env:// sponsor signer requires an environment variable name"); + } + let private_key = std::env::var(value) + .wrap_err_with(|| format!("{value} environment variable is required"))?; + foundry_wallets::utils::create_private_key_signer(&private_key) + } + "private-key" => { + if value.is_empty() { + eyre::bail!("private-key:// sponsor signer requires a private key"); + } + foundry_wallets::utils::create_private_key_signer(value) + } + "keystore" => { + if value.is_empty() { + eyre::bail!("keystore:// sponsor signer requires a keystore path"); + } + WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "account" => { + if value.is_empty() { + eyre::bail!("account:// sponsor signer requires an account name"); + } + WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "ledger" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { ledger: true, raw, ..Default::default() }.signer().await + } + "trezor" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { trezor: true, raw, ..Default::default() }.signer().await + } + "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await, + "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await, + "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await, + "browser" => { + eyre::bail!( + "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer" + ) + } + _ => eyre::bail!( + "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY" + ), + } +} diff --git a/crates/common/src/transactions/builder.rs b/crates/common/src/transactions/builder.rs index de03cf3adc73e..aa4c971680d00 100644 --- a/crates/common/src/transactions/builder.rs +++ b/crates/common/src/transactions/builder.rs @@ -9,7 +9,9 @@ use alloy_primitives::{Address, B256, Signature, TxKind, U256}; use alloy_provider::Provider; use alloy_signer::Signer; use eyre::Result; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionRequest; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_primitives::{ @@ -244,6 +246,24 @@ pub trait FoundryTransactionBuilder: NetworkTransactionBuilder { /// on-chain as part of this transaction. fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {} + /// Embeds key authorization before gas estimation/signing if the access key is not yet + /// provisioned on-chain. + /// + /// This mirrors the mutation performed by [`Self::sign_with_access_key`], but makes the final + /// transaction body available before fee-payer sponsor digests are computed. + fn prepare_access_key_authorization<'a>( + &'a mut self, + _provider: &'a impl Provider, + _wallet_address: Address, + _key_address: Address, + _key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + async { Ok(()) } + } + /// Converts a CREATE transaction into an AA-compatible call entry. /// /// Tempo AA transactions use a `calls` list instead of `to`+`input`. Must be @@ -355,6 +375,7 @@ impl FoundryTransactionBuilder for ::Transact } } +#[cfg(feature = "optimism")] impl FoundryTransactionBuilder for OpTransactionRequest { fn reset_gas_limit(&mut self) { self.as_mut().gas = None; @@ -439,6 +460,35 @@ impl FoundryTransactionBuilder for ::Tran self.key_authorization = Some(key_authorization); } + fn prepare_access_key_authorization<'a>( + &'a mut self, + provider: &'a impl Provider, + wallet_address: Address, + key_address: Address, + key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + let auth = key_authorization.cloned(); + + async move { + if let Some(auth) = auth { + let is_provisioned = provider + .get_keychain_key(wallet_address, key_address) + .await + .map(|info| info.keyId != Address::ZERO) + .unwrap_or(false); + + if !is_provisioned { + self.set_key_authorization(auth); + } + } + + Ok(()) + } + } + fn convert_create_to_call(&mut self) { if self.calls.is_empty() && self.inner.to.is_some_and(|to| to.is_create()) { let input = self.inner.input.input().cloned().unwrap_or_default(); @@ -473,7 +523,12 @@ impl FoundryTransactionBuilder for ::Tran let is_provisioned = provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false); - if !is_provisioned { + if !is_provisioned && self.key_authorization.is_none() { + if self.fee_payer_signature.is_some() { + eyre::bail!( + "cannot add Tempo key authorization after fee payer signature was attached" + ); + } self.set_key_authorization(auth); } } diff --git a/crates/common/src/transactions/receipt.rs b/crates/common/src/transactions/receipt.rs index 9ca6cb02b10ee..c2e34419248c4 100644 --- a/crates/common/src/transactions/receipt.rs +++ b/crates/common/src/transactions/receipt.rs @@ -7,6 +7,7 @@ use alloy_provider::{ use alloy_rpc_types::{BlockId, TransactionReceipt}; use eyre::Result; use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr}; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionReceipt; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionReceipt; @@ -23,6 +24,7 @@ impl FoundryReceiptResponse for TransactionReceipt { } } +#[cfg(feature = "optimism")] impl FoundryReceiptResponse for OpTransactionReceipt { fn set_contract_address(&mut self, contract_address: Address) { self.inner.contract_address = Some(contract_address); diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index bab59137b0130..8f63718e086cd 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -10,6 +10,10 @@ use std::path::PathBuf; pub struct FuzzConfig { /// The number of test cases that must execute for each property test pub runs: u32, + /// Optional 1-based fuzz run to execute. + pub run: Option, + /// Optional fuzz worker ID to pair with `run`. + pub worker: Option, /// Fails the fuzzed test if a revert occurs. pub fail_on_revert: bool, /// The maximum number of test case rejections allowed, @@ -37,6 +41,8 @@ impl Default for FuzzConfig { fn default() -> Self { Self { runs: 256, + run: None, + worker: None, fail_on_revert: true, max_test_rejects: 65536, seed: None, diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 270df14a6c291..000cefc26737a 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use crate::Config; use alloy_primitives::map::HashMap; use figment::{ @@ -5,6 +7,7 @@ use figment::{ value::{Dict, Map, Value}, }; use foundry_compilers::ProjectCompileOutput; +use foundry_evm_networks::NetworkVariant; use itertools::Itertools; mod natspec; @@ -123,6 +126,42 @@ impl InlineConfig { self.get_function(contract, function).is_some_and(|map| !map.is_empty()) } + /// Returns the configured [`NetworkVariant`] for a given test, checking function-level first + /// then contract-level. Returns `None` if no network annotation is present. + pub fn network_for( + &self, + profile: &Profile, + contract: &str, + function: &str, + ) -> Option { + let data = self.provide(contract, function).data().ok()?; + let dict = data.get(profile).or_else(|| data.get(&Profile::Default))?; + if let Some(Value::Dict(_, networks)) = dict.get("networks") + && let Some(Value::String(_, s)) = networks.get("network") + { + return s.parse().ok(); + } + None + } + + /// Returns all distinct [`NetworkVariant`]s referenced in any inline config annotation. + /// + /// This is used to determine whether a multi-network test pass is needed. + pub fn referenced_override_networks(&self, profile: &Profile) -> Vec { + let mut seen = BTreeSet::new(); + for (contract, function) in self.fn_level.keys() { + if let Some(v) = self.network_for(profile, contract, function) { + seen.insert(v); + } + } + for contract in self.contract_level.keys() { + if let Some(v) = self.network_for(profile, contract, "") { + seen.insert(v); + } + } + seen.into_iter().collect() + } + fn get_contract(&self, contract: &str) -> Option<&DataMap> { self.contract_level.get(contract) } diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index 3c8cad85bae10..cc3dabd32d4bf 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -29,3 +29,11 @@ ratatui = { version = "0.30", default-features = false, features = [ revm.workspace = true tracing.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 809e15b077c37..814beab402729 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -32,3 +32,7 @@ thiserror.workspace = true toml.workspace = true tracing.workspace = true regex.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index b8fd3760cd850..888f6269623a5 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -72,8 +72,8 @@ impl AsDoc for CommentsRef<'_> { writer.writeln_raw(format!( "{}{}: {}", if customs.len() == 1 { "" } else { "- " }, - &c.tag, - &c.value + c.tag, + c.value ))?; writer.writeln()?; } diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 03d569c17f500..801e813026a39 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -36,7 +36,7 @@ alloy-primitives = { workspace = true, features = [ alloy-provider.workspace = true alloy-network.workspace = true alloy-consensus.workspace = true -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-rpc-types = { workspace = true, features = ["anvil"] } alloy-sol-types.workspace = true alloy-rlp.workspace = true @@ -54,9 +54,10 @@ revm = { workspace = true, features = [ "blst", ] } revm-inspectors.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-network.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } tempo-revm.workspace = true tempo-alloy.workspace = true tempo-contracts.workspace = true @@ -77,7 +78,18 @@ url.workspace = true [dev-dependencies] alloy-serde.workspace = true -op-alloy-consensus.workspace = true -op-alloy-rpc-types.workspace = true anvil.workspace = true foundry-test-utils.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "foundry-common/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", +] diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 0cfd56a44219c..b836023a968b7 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -223,8 +223,8 @@ fn trimmed_hex(s: &[u8]) -> String { } else { format!( "{}…{} ({} bytes)", - &hex::encode(&s[..n / 2]), - &hex::encode(&s[s.len() - n / 2..]), + hex::encode(&s[..n / 2]), + hex::encode(&s[s.len() - n / 2..]), s.len(), ) } diff --git a/crates/evm/core/src/env.rs b/crates/evm/core/src/env.rs index 132b986f55e7f..bfc45b6b5d773 100644 --- a/crates/evm/core/src/env.rs +++ b/crates/evm/core/src/env.rs @@ -4,13 +4,9 @@ use alloy_consensus::Typed2718; pub use alloy_evm::EvmEnv; use alloy_evm::FromRecoveredTx; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, U256}; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; -use op_revm::{ - OpTransaction, - transaction::{OpTxTr, deposit::DEPOSIT_TRANSACTION_TYPE}, -}; +#[cfg(feature = "optimism")] +use op_revm::transaction::deposit::DEPOSIT_TRANSACTION_TYPE; use revm::{ Context, Database, Journal, context::{Block, BlockEnv, Cfg, CfgEnv, Transaction, TxEnv}, @@ -236,9 +232,16 @@ pub trait FoundryTransaction: Transaction { /// Sets whether the transaction is a system transaction fn set_system_transaction(&mut self, _is_system_transaction: bool) {} - /// Returns `true` if transaction is of type [`DEPOSIT_TRANSACTION_TYPE`]. + /// Returns `true` if transaction is an Optimism deposit transaction. fn is_deposit(&self) -> bool { - self.tx_type() == DEPOSIT_TRANSACTION_TYPE + #[cfg(feature = "optimism")] + { + self.tx_type() == DEPOSIT_TRANSACTION_TYPE + } + #[cfg(not(feature = "optimism"))] + { + false + } } // Tempo methods @@ -320,188 +323,6 @@ impl FoundryTransaction for TxEnv { } } -impl FoundryTransaction for OpTransaction { - fn set_tx_type(&mut self, tx_type: u8) { - self.base.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.base.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.base.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.base.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.base.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.base.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.base.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.base.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.base.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.base.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.base.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.base.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - OpTxTr::enveloped_tx(self) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.enveloped_tx = Some(bytes); - } - - fn source_hash(&self) -> Option { - OpTxTr::source_hash(self) - } - - fn set_source_hash(&mut self, source_hash: B256) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.source_hash = source_hash; - } - } - - fn mint(&self) -> Option { - OpTxTr::mint(self) - } - - fn set_mint(&mut self, mint: u128) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.mint = Some(mint); - } - } - - fn is_system_transaction(&self) -> bool { - OpTxTr::is_system_transaction(self) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.is_system_transaction = is_system_transaction; - } - } -} - -impl FoundryTransaction for OpTx { - fn set_tx_type(&mut self, tx_type: u8) { - self.0.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.0.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.0.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.0.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.0.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.0.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.0.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.0.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.0.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.0.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.0.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.0.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - FoundryTransaction::enveloped_tx(&self.0) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.0.set_enveloped_tx(bytes); - } - - fn source_hash(&self) -> Option { - FoundryTransaction::source_hash(&self.0) - } - - fn set_source_hash(&mut self, source_hash: B256) { - self.0.set_source_hash(source_hash); - } - - fn mint(&self) -> Option { - FoundryTransaction::mint(&self.0) - } - - fn set_mint(&mut self, mint: u128) { - self.0.set_mint(mint); - } - - fn is_system_transaction(&self) -> bool { - FoundryTransaction::is_system_transaction(&self.0) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - self.0.set_system_transaction(is_system_transaction); - } -} - impl FoundryTransaction for TempoTxEnv { fn set_tx_type(&mut self, tx_type: u8) { self.inner.set_tx_type(tx_type); @@ -687,32 +508,6 @@ impl FromAnyRpcTransaction for TxEnv { } } -impl FromAnyRpcTransaction for OpTx { - fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { - if let Some(envelope) = tx.as_envelope() { - return Ok(Self(OpTransaction:: { - base: TxEnv::from_recovered_tx(envelope, tx.from()), - enveloped_tx: None, - deposit: Default::default(), - })); - } - - // Handle OP deposit transactions from `Unknown` envelope variant. - if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner - && unknown.ty() == DEPOSIT_TX_TYPE_ID - { - let mut fields = unknown.inner.fields.clone(); - fields.insert("from".to_string(), serde_json::to_value(tx.from())?); - let deposit_tx: TxDeposit = fields - .deserialize_into() - .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; - return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); - } - - eyre::bail!("cannot convert unknown transaction type to OpTransaction") - } -} - impl FromAnyRpcTransaction for TempoTxEnv { fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { use alloy_consensus::Transaction as _; @@ -747,6 +542,222 @@ impl FromAnyRpcTransaction for TempoTxEnv { } } +#[cfg(feature = "optimism")] +mod optimism { + use super::*; + use alloy_op_evm::OpTx; + use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; + use op_revm::{OpTransaction, transaction::OpTxTr}; + + impl FoundryTransaction for OpTransaction { + fn set_tx_type(&mut self, tx_type: u8) { + self.base.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.base.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.base.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.base.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.base.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.base.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.base.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.base.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.base.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.base.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.base.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.base.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + OpTxTr::enveloped_tx(self) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.enveloped_tx = Some(bytes); + } + + fn source_hash(&self) -> Option { + OpTxTr::source_hash(self) + } + + fn set_source_hash(&mut self, source_hash: B256) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.source_hash = source_hash; + } + } + + fn mint(&self) -> Option { + OpTxTr::mint(self) + } + + fn set_mint(&mut self, mint: u128) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.mint = Some(mint); + } + } + + fn is_system_transaction(&self) -> bool { + OpTxTr::is_system_transaction(self) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.is_system_transaction = is_system_transaction; + } + } + } + + impl FoundryTransaction for OpTx { + fn set_tx_type(&mut self, tx_type: u8) { + self.0.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.0.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.0.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.0.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.0.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.0.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.0.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.0.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.0.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.0.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.0.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.0.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + FoundryTransaction::enveloped_tx(&self.0) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.0.set_enveloped_tx(bytes); + } + + fn source_hash(&self) -> Option { + FoundryTransaction::source_hash(&self.0) + } + + fn set_source_hash(&mut self, source_hash: B256) { + self.0.set_source_hash(source_hash); + } + + fn mint(&self) -> Option { + FoundryTransaction::mint(&self.0) + } + + fn set_mint(&mut self, mint: u128) { + self.0.set_mint(mint); + } + + fn is_system_transaction(&self) -> bool { + FoundryTransaction::is_system_transaction(&self.0) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + self.0.set_system_transaction(is_system_transaction); + } + } + + impl FromAnyRpcTransaction for OpTx { + fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { + if let Some(envelope) = tx.as_envelope() { + return Ok(Self(OpTransaction:: { + base: TxEnv::from_recovered_tx(envelope, tx.from()), + enveloped_tx: None, + deposit: Default::default(), + })); + } + + // Handle OP deposit transactions from `Unknown` envelope variant. + if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner + && unknown.ty() == DEPOSIT_TX_TYPE_ID + { + let mut fields = unknown.inner.fields.clone(); + fields.insert("from".to_string(), serde_json::to_value(tx.from())?); + let deposit_tx: TxDeposit = fields + .deserialize_into() + .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; + return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); + } + + eyre::bail!("cannot convert unknown transaction type to OpTransaction") + } + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU64; @@ -755,14 +766,10 @@ mod tests { use alloy_consensus::{Sealed, Signed, TxEip1559, transaction::Recovered}; use alloy_evm::{EthEvmFactory, EvmFactory}; use alloy_network::{AnyTxType, UnknownTxEnvelope, UnknownTypedTransaction}; - use alloy_op_evm::OpEvmFactory; use alloy_primitives::Signature; use alloy_rpc_types::{Transaction as RpcTransaction, TransactionInfo}; use alloy_serde::WithOtherFields; use foundry_evm_hardforks::TempoHardfork; - use op_alloy_consensus::{OpTxEnvelope, transaction::OpTransactionInfo}; - use op_alloy_rpc_types::Transaction as OpRpcTransaction; - use op_revm::OpSpecId; use revm::database::EmptyDB; use tempo_alloy::primitives::{ AASigned, TempoSignature, TempoTransaction, TempoTxEnvelope, @@ -793,30 +800,6 @@ mod tests { evm.ctx_mut().set_evm(evm_env); } - #[test] - fn op_evm_foundry_context_ext_implementation() { - let mut evm = - OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); - - // Test EVM Context Block mutation - evm.ctx_mut().block_mut().set_number(U256::from(123)); - assert_eq!(evm.ctx().block().number(), U256::from(123)); - - // Test EVM Context Tx mutation - evm.ctx_mut().tx_mut().set_nonce(99); - assert_eq!(evm.ctx().tx().nonce(), 99); - - // Test EVM Context Cfg mutation - evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; - assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); - - // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env - let tx_env = evm.ctx().tx_clone(); - evm.ctx_mut().set_tx(tx_env); - let evm_env = evm.ctx().evm_clone(); - evm.ctx_mut().set_evm(evm_env); - } - #[test] fn tempo_evm_foundry_context_ext_implementation() { let mut evm = TempoEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default()); @@ -874,23 +857,6 @@ mod tests { assert_eq!(tx_env.kind, TxKind::Call(Address::with_last_byte(0xBB))); } - #[test] - fn from_any_rpc_transaction_for_op() { - let from = Address::random(); - let signed_tx = make_signed_eip1559(); - - // Build the eth TxEnv to compare against op base - let rpc_tx = RpcTransaction::from_transaction( - Recovered::new_unchecked(signed_tx.into(), from), - TransactionInfo::default(), - ); - let any_tx = >::from(rpc_tx); - let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base, expected_base); - } - #[test] fn from_any_rpc_transaction_unknown_envelope_errors() { let unknown = AnyTxEnvelope::Unknown(UnknownTxEnvelope { @@ -915,39 +881,6 @@ mod tests { assert!(result.to_string().contains("unknown transaction type")); } - #[test] - fn from_any_rpc_transaction_for_op_deposit() { - let from = Address::random(); - let source_hash = B256::random(); - let deposit = TxDeposit { - source_hash, - from, - to: TxKind::Call(Address::with_last_byte(0xCC)), - mint: 1111, - value: U256::from(200), - gas_limit: 21000, - is_system_transaction: true, - input: Default::default(), - }; - - // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as AnyRpcTransaction. - let op_rpc_tx = OpRpcTransaction::from_transaction( - Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), - OpTransactionInfo::default(), - ); - let json = serde_json::to_value(&op_rpc_tx).unwrap(); - let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base.caller, from); - assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); - assert_eq!(op_tx_env.base.value, U256::from(200)); - assert_eq!(op_tx_env.base.gas_limit, 21000); - assert_eq!(op_tx_env.deposit.source_hash, source_hash); - assert_eq!(op_tx_env.deposit.mint, Some(1111)); - assert!(op_tx_env.deposit.is_system_transaction); - } - #[test] fn from_any_rpc_transaction_for_tempo_eth_envelope() { let from = Address::random(); @@ -1004,4 +937,88 @@ mod tests { assert_eq!(tx_env.inner.chain_id, Some(42431)); assert_eq!(tx_env.fee_token, fee_token); } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + use alloy_op_evm::{OpEvmFactory, OpTx}; + use op_alloy_consensus::{OpTxEnvelope, TxDeposit, transaction::OpTransactionInfo}; + use op_alloy_rpc_types::Transaction as OpRpcTransaction; + use op_revm::OpSpecId; + + #[test] + fn op_evm_foundry_context_ext_implementation() { + let mut evm = + OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); + + // Test EVM Context Block mutation + evm.ctx_mut().block_mut().set_number(U256::from(123)); + assert_eq!(evm.ctx().block().number(), U256::from(123)); + + // Test EVM Context Tx mutation + evm.ctx_mut().tx_mut().set_nonce(99); + assert_eq!(evm.ctx().tx().nonce(), 99); + + // Test EVM Context Cfg mutation + evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; + assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); + + // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env + let tx_env = evm.ctx().tx_clone(); + evm.ctx_mut().set_tx(tx_env); + let evm_env = evm.ctx().evm_clone(); + evm.ctx_mut().set_evm(evm_env); + } + + #[test] + fn from_any_rpc_transaction_for_op() { + let from = Address::random(); + let signed_tx = make_signed_eip1559(); + + // Build the eth TxEnv to compare against op base + let rpc_tx = RpcTransaction::from_transaction( + Recovered::new_unchecked(signed_tx.into(), from), + TransactionInfo::default(), + ); + let any_tx = >::from(rpc_tx); + let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base, expected_base); + } + + #[test] + fn from_any_rpc_transaction_for_op_deposit() { + let from = Address::random(); + let source_hash = B256::random(); + let deposit = TxDeposit { + source_hash, + from, + to: TxKind::Call(Address::with_last_byte(0xCC)), + mint: 1111, + value: U256::from(200), + gas_limit: 21000, + is_system_transaction: true, + input: Default::default(), + }; + + // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as + // AnyRpcTransaction. + let op_rpc_tx = OpRpcTransaction::from_transaction( + Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), + OpTransactionInfo::default(), + ); + let json = serde_json::to_value(&op_rpc_tx).unwrap(); + let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base.caller, from); + assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); + assert_eq!(op_tx_env.base.value, U256::from(200)); + assert_eq!(op_tx_env.base.gas_limit, 21000); + assert_eq!(op_tx_env.deposit.source_hash, source_hash); + assert_eq!(op_tx_env.deposit.mint, Some(1111)); + assert!(op_tx_env.deposit.is_system_transaction); + } + } } diff --git a/crates/evm/core/src/evm/mod.rs b/crates/evm/core/src/evm/mod.rs index 708226be003a2..fc9e9e7d2810f 100644 --- a/crates/evm/core/src/evm/mod.rs +++ b/crates/evm/core/src/evm/mod.rs @@ -10,14 +10,11 @@ use alloy_evm::{ EthEvmFactory, Evm, EvmEnv, EvmFactory, FromRecoveredTx, precompiles::PrecompilesMap, }; use alloy_network::{Ethereum, Network}; -use alloy_op_evm::OpEvmFactory; use alloy_primitives::{Address, Signature, U256}; use alloy_rlp::Decodable; use foundry_common::{FoundryReceiptResponse, FoundryTransactionBuilder, fmt::UIfmt}; use foundry_config::FromEvmVersion; use foundry_fork_db::{DatabaseError, ForkBlockEnv}; -use op_alloy_network::Optimism; -use op_revm::OpHaltReason; use revm::{ Database, context::{ @@ -36,10 +33,12 @@ use tempo_evm::evm::TempoEvmFactory; use tempo_revm::TempoHaltReason; pub mod eth; +#[cfg(feature = "optimism")] pub mod op; pub mod tempo; pub use eth::*; +#[cfg(feature = "optimism")] pub use op::*; pub use tempo::*; @@ -75,13 +74,6 @@ impl FoundryEvmNetwork for TempoEvmNetwork { type EvmFactory = TempoEvmFactory; } -#[derive(Clone, Copy, Debug, Default)] -pub struct OpEvmNetwork; -impl FoundryEvmNetwork for OpEvmNetwork { - type Network = Optimism; - type EvmFactory = OpEvmFactory; -} - /// Convenience type aliases for accessing associated types through [`FoundryEvmNetwork`]. pub type EvmFactoryFor = ::EvmFactory; pub type FoundryContextFor<'db, FEN> = @@ -249,15 +241,6 @@ impl IntoInstructionResult for HaltReason { } } -impl IntoInstructionResult for OpHaltReason { - fn into_instruction_result(self) -> InstructionResult { - match self { - Self::Base(eth) => eth.into(), - Self::FailedDeposit => InstructionResult::Stop, - } - } -} - impl IntoInstructionResult for TempoHaltReason { fn into_instruction_result(self) -> InstructionResult { match self { diff --git a/crates/evm/core/src/evm/op.rs b/crates/evm/core/src/evm/op.rs index cb8bf272d9a05..efb74ad3abf50 100644 --- a/crates/evm/core/src/evm/op.rs +++ b/crates/evm/core/src/evm/op.rs @@ -1,6 +1,7 @@ use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; use alloy_op_evm::{OpEvm, OpEvmContext, OpEvmFactory, OpTx}; use foundry_fork_db::DatabaseError; +use op_alloy_network::Optimism; use op_revm::{OpEvm as RevmEvm, OpHaltReason, OpSpecId, OpTransactionError, handler::OpHandler}; use revm::{ context::{ @@ -10,16 +11,33 @@ use revm::{ handler::{EthFrame, EvmTr, FrameResult, Handler, instructions::EthInstructions}, inspector::InspectorHandler, interpreter::{ - FrameInput, SharedMemory, interpreter::EthInterpreter, interpreter_action::FrameInit, + FrameInput, InstructionResult, SharedMemory, interpreter::EthInterpreter, + interpreter_action::FrameInit, }, }; use crate::{ FoundryContextExt, FoundryInspectorExt, backend::{DatabaseExt, JournaledState}, - evm::{FoundryEvmFactory, NestedEvm}, + evm::{FoundryEvmFactory, FoundryEvmNetwork, IntoInstructionResult, NestedEvm}, }; +#[derive(Clone, Copy, Debug, Default)] +pub struct OpEvmNetwork; +impl FoundryEvmNetwork for OpEvmNetwork { + type Network = Optimism; + type EvmFactory = OpEvmFactory; +} + +impl IntoInstructionResult for OpHaltReason { + fn into_instruction_result(self) -> InstructionResult { + match self { + Self::Base(eth) => eth.into(), + Self::FailedDeposit => InstructionResult::Stop, + } + } +} + type OpEvmHandler<'db, I> = OpHandler, EVMError, EthFrame>; diff --git a/crates/evm/core/src/fork/database.rs b/crates/evm/core/src/fork/database.rs index aefa0e2ee9741..2284823047ca6 100644 --- a/crates/evm/core/src/fork/database.rs +++ b/crates/evm/core/src/fork/database.rs @@ -212,13 +212,18 @@ pub struct ForkDbStateSnapshot { } impl ForkDbStateSnapshot { - fn get_storage(&self, address: Address, index: U256) -> Option { - self.local - .cache - .accounts - .get(&address) - .and_then(|account| account.storage.get(&index)) - .copied() + /// Lookup storage in `state_snapshot`, then fall back to the backend (remote RPC). + fn storage_from_snapshot_or_backend( + &self, + address: Address, + index: U256, + ) -> Result { + // Check state_snapshot.storage first (data fetched by SharedBackend / disk cache). + if let Some(val) = self.state_snapshot.storage.get(&address).and_then(|s| s.get(&index)) { + return Ok(*val); + } + // Fall back to the underlying backend (SharedBackend → remote RPC). + DatabaseRef::storage_ref(&self.local, address, index) } } @@ -250,15 +255,9 @@ impl DatabaseRef for ForkDbStateSnapshot { match self.local.cache.accounts.get(&address) { Some(account) => match account.storage.get(&index) { Some(entry) => Ok(*entry), - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), - }, - }, - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), + None => self.storage_from_snapshot_or_backend(address, index), }, + None => self.storage_from_snapshot_or_backend(address, index), } } @@ -303,4 +302,28 @@ mod tests { assert!(loaded.is_some()); assert_eq!(loaded.unwrap(), info); } + + /// Verifies that `ForkDbStateSnapshot::storage_ref` reads from `state_snapshot.storage` + /// when the slot is missing from `local.cache.accounts`. Without this lookup the call + /// would fall through to the backend and return the unrelated remote value. + #[tokio::test(flavor = "multi_thread")] + async fn fork_db_state_snapshot_reads_storage_from_snapshot() { + let rpc = foundry_test_utils::rpc::next_http_rpc_endpoint(); + let provider = get_http_provider(rpc.clone()); + let meta = BlockchainDbMeta::new(BlockEnv::default(), rpc); + let db = BlockchainDb::new(meta, None); + let backend = SharedBackend::spawn_backend(Arc::new(provider), db, None).await; + + let address = Address::random(); + let slot = U256::from(42u64); + let expected = U256::from(0xdeadbeefu64); + + let mut state_snapshot = StateSnapshot::default(); + state_snapshot.storage.entry(address).or_default().insert(slot, expected); + + let snapshot = ForkDbStateSnapshot { local: CacheDB::new(backend), state_snapshot }; + + let got = DatabaseRef::storage_ref(&snapshot, address, slot).unwrap(); + assert_eq!(got, expected); + } } diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index 1b2201a9b8b84..c2edbb9dfd33b 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -5,6 +5,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::constants::DEFAULT_CREATE2_DEPLOYER; use alloy_primitives::{Address, map::HashMap}; use auto_impl::auto_impl; diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 82efd4a1aaaaa..ab68eb08821e8 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -137,8 +137,12 @@ impl EvmOpts { /// [`NetworkConfigs::with_chain_id`] to auto-enable the correct network /// (e.g. Tempo, OP Stack) based on the chain ID. pub async fn infer_network_from_fork(&mut self) { + #[cfg(feature = "optimism")] + let already_op = self.networks.is_optimism(); + #[cfg(not(feature = "optimism"))] + let already_op = false; if !self.networks.is_tempo() - && !self.networks.is_optimism() + && !already_op && let Some(ref fork_url) = self.fork_url && let Ok(provider) = self.fork_provider_with_url::(fork_url) && let Ok(chain_id) = provider.get_chain_id().await @@ -474,6 +478,7 @@ mod tests { // Plain anvil (chain id 31337) without tempo flag -> Ethereum (no network flags set). assert!(!evm_opts.networks.is_tempo()); + #[cfg(feature = "optimism")] assert!(!evm_opts.networks.is_optimism()); assert!(!evm_opts.networks.is_celo()); assert_eq!(evm_opts.networks, NetworkConfigs::default()); diff --git a/crates/evm/coverage/Cargo.toml b/crates/evm/coverage/Cargo.toml index d2d7b077ee9f0..758604564726e 100644 --- a/crates/evm/coverage/Cargo.toml +++ b/crates/evm/coverage/Cargo.toml @@ -25,3 +25,7 @@ semver.workspace = true tracing.workspace = true rayon.workspace = true solar.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 70bce50a89882..5dbf07c7a356c 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -61,3 +61,16 @@ serde.workspace = true uuid.workspace = true rayon.workspace = true tokio.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm-core/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 33152b73dda3c..1932a834ab397 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -17,7 +17,7 @@ use foundry_evm_core::{ use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError, - FuzzFixtures, FuzzTestResult, + FuzzFixtures, FuzzRunMetadata, FuzzTestResult, strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state}, }; use foundry_evm_traces::SparsedTraceArena; @@ -71,6 +71,8 @@ struct WorkerState { runs: u32, /// Failure reason if this worker failed failure: Option, + /// Fuzz run metadata that produced the failure. + failure_run: Option, /// Last run timestamp in milliseconds /// /// Used to identify which worker ran last and collect its traces and call breakpoints @@ -93,6 +95,7 @@ impl WorkerState { deprecated_cheatcodes: HashMap::default(), runs: 0, failure: None, + failure_run: None, last_run_timestamp: 0, failed_corpus_replays: 0, } @@ -196,8 +199,14 @@ impl FuzzedExecutor { config: FuzzConfig, persisted_failure: Option, ) -> Self { - let max_workers = - if config.runs == 0 { 0 } else { Ord::max(1, config.runs / MIN_RUNS_PER_WORKER) }; + let run_limit = if config.run.is_some() { 1 } else { config.runs }; + let max_workers = if run_limit == 0 { + 0 + } else if config.run.is_some() { + 1 + } else { + Ord::max(1, run_limit / MIN_RUNS_PER_WORKER) + }; let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize); Self { executor_f: executor, runner, sender, config, persisted_failure, num_workers } } @@ -221,8 +230,9 @@ impl FuzzedExecutor { ) -> Result { let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone()); - debug!(n = self.num_workers, "spawning workers"); - let workers = (0..self.num_workers) + let worker_ids = self.worker_ids(); + debug!(n = worker_ids.len(), "spawning workers"); + let workers = worker_ids .into_par_iter() .map(|worker_id| { let _guard = tokio_handle.enter(); @@ -364,8 +374,14 @@ impl FuzzedExecutor { } else { vec![] }; + let fuzz = failed_worker.failure_run.unwrap_or_default(); result.counterexample = Some(CounterExample::Single( - BaseCounterExample::from_fuzz_call(calldata, args, call.traces), + BaseCounterExample::from_fuzz_call(calldata, args, call.traces) + .with_fuzz_metadata(FuzzRunMetadata::new( + fuzz.seed.or(self.config.seed), + fuzz.run, + fuzz.worker, + )), )); } Some(TestCaseError::Reject(reason)) => { @@ -453,16 +469,7 @@ impl FuzzedExecutor { runner_config.cases = worker_runs; let mut runner = if let Some(seed) = self.config.seed { - // For deterministic parallel fuzzing, derive a unique seed for each worker - let worker_seed = if worker_id == 0 { - // Master worker uses the provided seed as is. - seed - } else { - // Derive a worker-specific seed using keccak256(seed || worker_id) - let seed_data = - [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); - U256::from_be_bytes(keccak256(seed_data).0) - }; + let worker_seed = Self::fuzz_worker_seed(seed, worker_id); trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}"); let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>()); TestRunner::new_with_rng(runner_config, rng) @@ -470,11 +477,25 @@ impl FuzzedExecutor { TestRunner::new(runner_config) }; - let mut persisted_failure = self.persisted_failure.as_ref().filter(|_| worker_id == 0); + if let Some(target_run) = self.config.run { + for _ in 1..target_run { + if let Err(err) = corpus.new_input(&mut runner, &shared_state.state, func) { + worker.failure = Some(TestCaseError::fail(format!( + "failed to generate fuzzed input in worker {}: {err}", + worker.id + ))); + shared_state.try_claim_failure(worker_id); + return Ok(worker); + } + } + } + + let mut persisted_failure = + self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none()); // Offset to stagger corpus syncs across workers; so that workers don't sync at the same // time. - let sync_offset = worker_id as u32 * 100; + let sync_offset = (worker_id as u32).saturating_mul(100); let sync_threshold = SYNC_INTERVAL + sync_offset; let mut runs_since_sync = sync_threshold; // Always sync at the start. let mut last_metrics_report = Instant::now(); @@ -483,11 +504,27 @@ impl FuzzedExecutor { // 2. Worker hasn't reached its specific run limit 'stop: while shared_state.should_continue() && worker.runs < worker_runs { // If counterexample recorded, replay it first, without incrementing runs. - let input = if worker_id == 0 + let (input, fuzz_run) = if worker_id == 0 && let Some(failure) = persisted_failure.take() && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector) { - failure.calldata.clone() + let seed = failure.fuzz.seed.or(self.config.seed); + if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() + && let Some(seed) = seed + { + let run = failure.fuzz.run.unwrap_or(1); + let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize; + cheats.set_seed(Self::fuzz_run_seed(seed, worker, run)); + } + + ( + failure.calldata.clone(), + Some(FuzzRunMetadata::new( + seed, + failure.fuzz.run, + Some(failure.fuzz.worker.unwrap_or(worker_id as u32)), + )), + ) } else { runs_since_sync += 1; if runs_since_sync >= sync_threshold { @@ -503,13 +540,14 @@ impl FuzzedExecutor { runs_since_sync = 0; } + let fuzz_run = self.config.run.unwrap_or(worker.runs + 1); if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() && let Some(seed) = self.config.seed { - cheats.set_seed(seed.wrapping_add(U256::from(worker.runs))); + cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run)); } - match corpus.new_input(&mut runner, &shared_state.state, func) { + let input = match corpus.new_input(&mut runner, &shared_state.state, func) { Ok(input) => input, Err(err) => { worker.failure = Some(TestCaseError::fail(format!( @@ -519,13 +557,24 @@ impl FuzzedExecutor { shared_state.try_claim_failure(worker_id); break 'stop; } - } + }; + + ( + input, + Some(FuzzRunMetadata::new( + self.config.seed, + Some(fuzz_run), + Some(worker_id as u32), + )), + ) }; let mut inc_runs = || { let total_runs = shared_state.increment_runs(); debug_assert!( - shared_state.timer.is_enabled() || total_runs <= self.config.runs, + shared_state.timer.is_enabled() + || total_runs + <= if self.config.run.is_some() { 1 } else { self.config.runs }, "worker runs were not distributed correctly" ); worker.runs += 1; @@ -595,6 +644,7 @@ impl FuzzedExecutor { .. }) => { inc_runs(); + worker.failure_run = fuzz_run; // Only classify magic skip payloads when the revert originates from the // cheatcode address. @@ -656,7 +706,7 @@ impl FuzzedExecutor { /// Determines the number of runs per worker. const fn runs_per_worker(&self, worker_id: usize) -> u32 { let worker_id = worker_id as u32; - let total_runs = self.config.runs; + let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs }; let n = self.num_workers as u32; let runs = total_runs / n; let remainder = total_runs % n; @@ -664,4 +714,29 @@ impl FuzzedExecutor { // assuming `worker_id` is in `0..n`. if worker_id < remainder { runs + 1 } else { runs } } + + /// Returns the worker IDs to execute. + fn worker_ids(&self) -> Vec { + if self.config.run.is_some() { + vec![self.config.worker.unwrap_or(0) as usize] + } else { + (0..self.num_workers).collect() + } + } + + /// Derives the deterministic RNG seed for a fuzz worker. + fn fuzz_worker_seed(seed: U256, worker_id: usize) -> U256 { + if worker_id == 0 { + seed + } else { + let worker_id = worker_id as u32; + let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); + U256::from_be_bytes(keccak256(seed_data).0) + } + } + + /// Derives the deterministic RNG seed for cheatcode randomness in a worker-local run. + fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 { + Self::fuzz_worker_seed(seed, worker_id).wrapping_add(U256::from(run.saturating_sub(1))) + } } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 27d8e6a0ed588..e02cdbc393ee6 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -736,7 +736,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { if !msg.is_empty() { msg.push_str(", "); } - msg.push_str(&format!("{}", &corpus_manager.metrics)); + msg.push_str(&format!("{}", corpus_manager.metrics)); } progress.set_message(msg); } diff --git a/crates/evm/fuzz/Cargo.toml b/crates/evm/fuzz/Cargo.toml index 62e4e80a73674..5629b17d936da 100644 --- a/crates/evm/fuzz/Cargo.toml +++ b/crates/evm/fuzz/Cargo.toml @@ -50,3 +50,12 @@ rand.workspace = true serde.workspace = true thiserror.workspace = true tracing.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 44d71fb6deee3..9c3e7d179c7f6 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -33,6 +33,27 @@ pub use strategies::LiteralMaps; mod inspector; pub use inspector::Fuzzer; +/// Metadata needed to reproduce a fuzz run. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct FuzzRunMetadata { + /// Seed used for the worker's input stream. + #[serde(default, rename = "fuzz_seed", skip_serializing_if = "Option::is_none")] + pub seed: Option, + /// 1-based run inside the worker's input stream. + #[serde(default, rename = "fuzz_run", skip_serializing_if = "Option::is_none")] + pub run: Option, + /// Worker that generated the input stream. + #[serde(default, rename = "fuzz_worker", skip_serializing_if = "Option::is_none")] + pub worker: Option, +} + +impl FuzzRunMetadata { + /// Creates metadata for reproducing a fuzz run. + pub const fn new(seed: Option, run: Option, worker: Option) -> Self { + Self { seed, run, worker } + } +} + /// Details of a transaction generated by fuzz strategy for fuzzing a target. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BasicTxDetails { @@ -102,6 +123,9 @@ pub struct BaseCounterExample { /// Whether to display sequence as solidity. #[serde(skip)] pub show_solidity: bool, + /// Fuzz metadata needed to reproduce this counterexample. + #[serde(flatten)] + pub fuzz: FuzzRunMetadata, } impl BaseCounterExample { @@ -137,6 +161,7 @@ impl BaseCounterExample { ), traces, show_solidity, + fuzz: FuzzRunMetadata::default(), }; } } @@ -154,6 +179,7 @@ impl BaseCounterExample { raw_args: None, traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } @@ -176,8 +202,15 @@ impl BaseCounterExample { raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()), traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } + + /// Sets fuzz metadata for reproducing this counterexample. + pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self { + self.fuzz = fuzz; + self + } } impl fmt::Display for BaseCounterExample { @@ -229,7 +262,7 @@ impl fmt::Display for BaseCounterExample { if let Some(sig) = &self.signature { write!(f, "calldata={sig}")? } else { - write!(f, "calldata={}", &self.calldata)? + write!(f, "calldata={}", self.calldata)? } if let Some(args) = &self.args { diff --git a/crates/evm/hardforks/Cargo.toml b/crates/evm/hardforks/Cargo.toml index 9bf318028487a..68f6fb23bab07 100644 --- a/crates/evm/hardforks/Cargo.toml +++ b/crates/evm/hardforks/Cargo.toml @@ -16,10 +16,14 @@ workspace = true [dependencies] alloy-chains.workspace = true alloy-hardforks = { workspace = true, features = ["serde"] } -alloy-op-hardforks = { workspace = true, features = ["serde"] } +alloy-op-hardforks = { workspace = true, features = ["serde"], optional = true } alloy-rpc-types.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } revm.workspace = true serde = { workspace = true, features = ["derive"] } tempo-chainspec.workspace = true foundry-compilers.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "dep:op-revm"] diff --git a/crates/evm/hardforks/src/lib.rs b/crates/evm/hardforks/src/lib.rs index 8a29ebb7af4ec..a8e0d51738263 100644 --- a/crates/evm/hardforks/src/lib.rs +++ b/crates/evm/hardforks/src/lib.rs @@ -8,11 +8,13 @@ use std::str::FromStr; use alloy_chains::Chain; use alloy_rpc_types::BlockNumberOrTag; use foundry_compilers::artifacts::EvmVersion; +#[cfg(feature = "optimism")] use op_revm::OpSpecId; use revm::primitives::hardfork::SpecId; use serde::{Deserialize, Serialize}; pub use alloy_hardforks::EthereumHardfork; +#[cfg(feature = "optimism")] pub use alloy_op_hardforks::OpHardfork; pub use tempo_chainspec::hardfork::TempoHardfork; @@ -20,6 +22,7 @@ pub use tempo_chainspec::hardfork::TempoHardfork; #[serde(into = "String")] pub enum FoundryHardfork { Ethereum(EthereumHardfork), + #[cfg(feature = "optimism")] Optimism(OpHardfork), Tempo(TempoHardfork), } @@ -28,6 +31,7 @@ impl From for String { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(h) => format!("optimism:{h}"), FoundryHardfork::Tempo(h) => format!("tempo:{h}"), } @@ -64,6 +68,7 @@ impl FromStr for FoundryHardfork { .map(Self::Ethereum) .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")), + #[cfg(feature = "optimism")] "op" | "optimism" => OpHardfork::from_str(&fork) .map(Self::Optimism) .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")), @@ -83,6 +88,7 @@ impl FoundryHardfork { Self::Ethereum(h) } + #[cfg(feature = "optimism")] pub const fn optimism(h: OpHardfork) -> Self { Self::Optimism(h) } @@ -95,6 +101,7 @@ impl FoundryHardfork { pub fn name(&self) -> String { match self { Self::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] Self::Optimism(h) => format!("{h}"), Self::Tempo(h) => format!("{h}"), } @@ -106,6 +113,7 @@ impl FoundryHardfork { pub const fn namespace(&self) -> Option<&'static str> { match self { Self::Ethereum(_) => None, + #[cfg(feature = "optimism")] Self::Optimism(_) => Some("optimism"), Self::Tempo(_) => Some("tempo"), } @@ -119,6 +127,7 @@ impl FoundryHardfork { if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Ethereum(fork)); } + #[cfg(feature = "optimism")] if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Optimism(fork)); } @@ -143,12 +152,14 @@ impl From for EthereumHardfork { } } +#[cfg(feature = "optimism")] impl From for FoundryHardfork { fn from(value: OpHardfork) -> Self { Self::Optimism(value) } } +#[cfg(feature = "optimism")] impl From for OpHardfork { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -177,12 +188,14 @@ impl From for SpecId { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork).into(), FoundryHardfork::Tempo(hardfork) => hardfork.into(), } } } +#[cfg(feature = "optimism")] impl From for OpSpecId { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -223,6 +236,7 @@ pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId { } /// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`. +#[cfg(feature = "optimism")] pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId { match hardfork { OpHardfork::Bedrock => OpSpecId::BEDROCK, @@ -265,6 +279,7 @@ impl FromEvmVersion for SpecId { } } +#[cfg(feature = "optimism")] impl FromEvmVersion for OpSpecId { fn from_evm_version(version: EvmVersion) -> Self { match version { @@ -324,16 +339,6 @@ mod tests { assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA); } - #[test] - fn test_optimism_spec_id_mapping() { - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); - - // Test latest hardforks - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); - } - #[test] fn test_tempo_spec_id_mapping() { assert_eq!(SpecId::from(TempoHardfork::Genesis), SpecId::OSAKA); @@ -371,25 +376,40 @@ mod tests { } #[test] - fn test_from_chain_and_timestamp_op_mainnet() { - let op_chain_id = 10; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); + fn test_from_chain_and_timestamp_unknown_chain() { + assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); } - #[test] - fn test_from_chain_and_timestamp_base() { - let base_chain_id = 8453; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); - } + #[cfg(feature = "optimism")] + mod optimism { + use super::*; - #[test] - fn test_from_chain_and_timestamp_unknown_chain() { - assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); + #[test] + fn test_optimism_spec_id_mapping() { + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); + + // Test latest hardforks + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); + } + + #[test] + fn test_from_chain_and_timestamp_op_mainnet() { + let op_chain_id = 10; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } + + #[test] + fn test_from_chain_and_timestamp_base() { + let base_chain_id = 8453; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } } } diff --git a/crates/evm/networks/Cargo.toml b/crates/evm/networks/Cargo.toml index a63ed34ba61cf..00c9abf0f90f7 100644 --- a/crates/evm/networks/Cargo.toml +++ b/crates/evm/networks/Cargo.toml @@ -19,7 +19,7 @@ foundry-evm-hardforks.workspace = true alloy-chains.workspace = true alloy-eips.workspace = true alloy-evm.workspace = true -alloy-op-hardforks.workspace = true +alloy-op-hardforks = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = [ "serde", "getrandom", @@ -43,4 +43,8 @@ clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } serde.workspace = true [dev-dependencies] -serde_json.workspace = true \ No newline at end of file +serde_json.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "foundry-evm-hardforks/optimism"] diff --git a/crates/evm/networks/src/lib.rs b/crates/evm/networks/src/lib.rs index 303b9ca8b7a13..384cee5a7bed3 100644 --- a/crates/evm/networks/src/lib.rs +++ b/crates/evm/networks/src/lib.rs @@ -11,7 +11,6 @@ use alloy_chains::{ }; use alloy_eips::eip1559::BaseFeeParams; use alloy_evm::precompiles::PrecompilesMap; -use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; use alloy_primitives::{Address, ChainId, map::AddressHashMap}; use clap::Parser; use foundry_evm_hardforks::FoundryHardfork; @@ -20,20 +19,52 @@ use std::collections::BTreeMap; pub mod celo; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] +#[cfg(feature = "optimism")] +mod optimism; + +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + clap::ValueEnum, +)] #[serde(rename_all = "lowercase")] #[clap(rename_all = "lowercase")] pub enum NetworkVariant { #[default] Ethereum, + #[cfg(feature = "optimism")] Optimism, Tempo, } +impl std::str::FromStr for NetworkVariant { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "ethereum" => Ok(Self::Ethereum), + #[cfg(feature = "optimism")] + "optimism" => Ok(Self::Optimism), + "tempo" => Ok(Self::Tempo), + _ => Err(format!("unknown network variant: {s}")), + } + } +} + impl NetworkVariant { pub const fn name(&self) -> &'static str { match self { Self::Ethereum => "ethereum", + #[cfg(feature = "optimism")] Self::Optimism => "optimism", Self::Tempo => "tempo", } @@ -50,32 +81,37 @@ impl From for NetworkVariant { fn from(chain_id: ChainId) -> Self { let chain = Chain::from_id(chain_id); if chain.is_tempo() { - Self::Tempo - } else if chain.is_optimism() { - Self::Optimism - } else { - Self::Ethereum + return Self::Tempo; + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::Optimism; } + Self::Ethereum } } #[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)] pub struct NetworkConfigs { /// Enable a specific network family. - #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "optimism", "tempo"])] + #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] #[serde(default)] - network: Option, + pub(crate) network: Option, /// Enable Celo network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "optimism", "tempo"])] + #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] celo: bool, /// Enable Optimism network features (deprecated: use --network optimism). + #[cfg(feature = "optimism")] #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])] // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the // canonical form is `network = "optimism"`. #[serde(default)] - optimism: bool, + pub(crate) optimism: bool, /// Enable Tempo network features (deprecated: use --network tempo). - #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "optimism"])] + #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the // canonical form is `network = "tempo"`. #[serde(default)] @@ -102,10 +138,6 @@ impl Serialize for NetworkConfigs { } impl NetworkConfigs { - pub fn with_optimism() -> Self { - Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } - } - pub fn with_celo() -> Self { Self { celo: true, ..Default::default() } } @@ -114,11 +146,7 @@ impl NetworkConfigs { Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() } } - pub fn is_optimism(&self) -> bool { - matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) - } - - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { matches!(self.resolved_network(), Some(NetworkVariant::Tempo)) } @@ -127,14 +155,18 @@ impl NetworkConfigs { } /// Returns the resolved network variant, folding legacy flags. - fn resolved_network(&self) -> Option { - self.network.or(if self.optimism { - Some(NetworkVariant::Optimism) - } else if self.tempo { - Some(NetworkVariant::Tempo) - } else { - None - }) + const fn resolved_network(&self) -> Option { + if let Some(n) = self.network { + return Some(n); + } + #[cfg(feature = "optimism")] + if self.optimism { + return Some(NetworkVariant::Optimism); + } + if self.tempo { + return Some(NetworkVariant::Tempo); + } + None } /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum. @@ -150,16 +182,12 @@ impl NetworkConfigs { /// For Optimism networks, returns Canyon parameters if the Canyon hardfork is active /// at the given timestamp, otherwise returns pre-Canyon parameters. pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_hardforks = OpChainHardforks::op_mainnet(); - if op_hardforks.is_canyon_active_at_timestamp(timestamp) { - BaseFeeParams::optimism_canyon() - } else { - BaseFeeParams::optimism() - } - } else { - BaseFeeParams::ethereum() + return self.op_base_fee_params(timestamp); } + let _ = timestamp; + BaseFeeParams::ethereum() } pub fn bypass_prevrandao(&self, chain_id: u64) -> bool { @@ -174,21 +202,23 @@ impl NetworkConfigs { pub fn with_chain_id(self, chain_id: u64) -> Self { let chain = Chain::from_id(chain_id); - if self.resolved_network().is_none() { - if chain.is_tempo() { - Self::with_tempo() - } else if chain.is_optimism() { - Self::with_optimism() + if self.resolved_network().is_some() { + return if !self.celo + && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) + { + Self::with_celo() } else { self - } - } else if !self.celo - && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) - { - Self::with_celo() - } else { - self + }; + } + if chain.is_tempo() { + return Self::with_tempo(); + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::with_optimism(); } + self } /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an @@ -208,6 +238,7 @@ impl NetworkConfigs { let network = match hardfork { FoundryHardfork::Ethereum(_) => self, FoundryHardfork::Tempo(_) => Self::with_tempo(), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(_) => Self::with_optimism(), }; @@ -243,6 +274,21 @@ impl NetworkConfigs { } } +impl From for NetworkConfigs { + fn from(network: NetworkVariant) -> Self { + match network { + NetworkVariant::Ethereum => Self::default(), + NetworkVariant::Tempo => { + Self { network: Some(network), tempo: true, ..Default::default() } + } + #[cfg(feature = "optimism")] + NetworkVariant::Optimism => { + Self { network: Some(network), optimism: true, ..Default::default() } + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -254,17 +300,6 @@ mod tests { let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() }; let via_old = NetworkConfigs { tempo: true, ..Default::default() }; assert_eq!(via_new.is_tempo(), via_old.is_tempo()); - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.active_network_name(), via_old.active_network_name()); - } - - #[test] - fn new_optimism_flag_equivalent_to_legacy() { - let via_new = - NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; - let via_old = NetworkConfigs { optimism: true, ..Default::default() }; - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.is_tempo(), via_old.is_tempo()); assert_eq!(via_new.active_network_name(), via_old.active_network_name()); } @@ -276,31 +311,11 @@ mod tests { assert_eq!(cfg.active_network_name(), Some("tempo")); } - #[test] - fn active_network_name_optimism() { - let cfg = NetworkConfigs::with_optimism(); - assert_eq!(cfg.active_network_name(), Some("optimism")); - } - #[test] fn active_network_name_default_is_none() { assert_eq!(NetworkConfigs::default().active_network_name(), None); } - // --- new flag takes precedence over legacy flag --- - - #[test] - fn new_flag_wins_over_legacy_when_both_set() { - // --network optimism --tempo: network field wins - let cfg = NetworkConfigs { - network: Some(NetworkVariant::Optimism), - tempo: true, - ..Default::default() - }; - assert!(cfg.is_optimism()); - assert!(!cfg.is_tempo()); - } - // --- Serde round-trip --- #[test] @@ -309,16 +324,6 @@ mod tests { let json = serde_json::to_string(&original).unwrap(); let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); assert!(restored.is_tempo()); - assert!(!restored.is_optimism()); - } - - #[test] - fn serde_roundtrip_optimism() { - let original = NetworkConfigs::with_optimism(); - let json = serde_json::to_string(&original).unwrap(); - let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); - assert!(restored.is_optimism()); - assert!(!restored.is_tempo()); } #[test] @@ -345,8 +350,55 @@ mod tests { let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#; let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap(); assert!(cfg_tempo.is_tempo()); - let json_optimism = r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; - let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); - assert!(cfg_optimism.is_optimism()); + } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + + #[test] + fn new_optimism_flag_equivalent_to_legacy() { + let via_new = + NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; + let via_old = NetworkConfigs { optimism: true, ..Default::default() }; + assert_eq!(via_new.is_optimism(), via_old.is_optimism()); + assert_eq!(via_new.is_tempo(), via_old.is_tempo()); + assert_eq!(via_new.active_network_name(), via_old.active_network_name()); + } + + #[test] + fn active_network_name_optimism() { + let cfg = NetworkConfigs::with_optimism(); + assert_eq!(cfg.active_network_name(), Some("optimism")); + } + + #[test] + fn new_flag_wins_over_legacy_when_both_set() { + // --network optimism --tempo: network field wins + let cfg = NetworkConfigs { + network: Some(NetworkVariant::Optimism), + tempo: true, + ..Default::default() + }; + assert!(cfg.is_optimism()); + assert!(!cfg.is_tempo()); + } + + #[test] + fn serde_roundtrip_optimism() { + let original = NetworkConfigs::with_optimism(); + let json = serde_json::to_string(&original).unwrap(); + let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); + assert!(restored.is_optimism()); + assert!(!restored.is_tempo()); + } + + #[test] + fn serde_optimism_field_deserialized() { + let json_optimism = + r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; + let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); + assert!(cfg_optimism.is_optimism()); + } } } diff --git a/crates/evm/networks/src/optimism.rs b/crates/evm/networks/src/optimism.rs new file mode 100644 index 0000000000000..5fffa38a333c7 --- /dev/null +++ b/crates/evm/networks/src/optimism.rs @@ -0,0 +1,25 @@ +//! Optimism-specific extensions for [`NetworkConfigs`] and related helpers. + +use crate::{NetworkConfigs, NetworkVariant}; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; + +impl NetworkConfigs { + pub fn with_optimism() -> Self { + Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } + } + + pub const fn is_optimism(&self) -> bool { + matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) + } + + /// Optimism-specific base fee parameters, picking Canyon vs pre-Canyon based on `timestamp`. + pub(crate) fn op_base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + let op_hardforks = OpChainHardforks::op_mainnet(); + if op_hardforks.is_canyon_active_at_timestamp(timestamp) { + BaseFeeParams::optimism_canyon() + } else { + BaseFeeParams::optimism() + } + } +} diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index 90d2db724cebc..73d64d3ab5d07 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -50,3 +50,7 @@ tempfile.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true yansi.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml index bad5c577bc69e..b6f11772620f9 100644 --- a/crates/fmt/Cargo.toml +++ b/crates/fmt/Cargo.toml @@ -26,3 +26,7 @@ foundry-test-utils.workspace = true toml.workspace = true snapbox.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index 89a9bf152c8c2..4b986017b71dd 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -711,7 +711,7 @@ impl<'sess> State<'sess, '_> { // Merge the lines and let the wrapper handle breaking if needed let merged_line = format!( "{current_line} {next_content}", - next_content = &next_line[prefix.len()..].trim_start() + next_content = next_line[prefix.len()..].trim_start() ); result.push(merged_line); diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 064834d248d5f..667da6b442ca1 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -117,7 +117,7 @@ tempfile.workspace = true alloy-signer-local.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -126,3 +126,15 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cli/optimism", + "forge-script/optimism", + "forge-verify/optimism", + "forge-doc/optimism", + "forge-fmt/optimism", + "forge-lint/optimism", + "forge-sol-macro-gen/optimism", +] diff --git a/crates/forge/assets/tempo/MailTemplate.s.sol b/crates/forge/assets/tempo/MailTemplate.s.sol index 27512efe4d5ec..45006f7cd0e06 100644 --- a/crates/forge/assets/tempo/MailTemplate.s.sol +++ b/crates/forge/assets/tempo/MailTemplate.s.sol @@ -14,7 +14,7 @@ contract MailScript is Script { function run(string memory salt) public { vm.startBroadcast(); - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); ITIP20 token = ITIP20( diff --git a/crates/forge/assets/tempo/MailTemplate.t.sol b/crates/forge/assets/tempo/MailTemplate.t.sol index b1749db5df0bf..19760303860a1 100644 --- a/crates/forge/assets/tempo/MailTemplate.t.sol +++ b/crates/forge/assets/tempo/MailTemplate.t.sol @@ -17,7 +17,7 @@ contract MailTest is Test { address public constant BOB = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); function setUp() public virtual { - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); token = ITIP20( diff --git a/crates/forge/src/cmd/coverage.rs b/crates/forge/src/cmd/coverage.rs index ea034bce87185..b8ce2a9b945b1 100644 --- a/crates/forge/src/cmd/coverage.rs +++ b/crates/forge/src/cmd/coverage.rs @@ -87,8 +87,11 @@ impl CoverageArgs { config = self.load_config()?; } - // Set fuzz seed so coverage reports are deterministic - config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so coverage reports are deterministic, + // but allow the user to override it via `--fuzz-seed` or `[fuzz] seed` in config. + if config.fuzz.seed.is_none() { + config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let (paths, mut output) = { let (project, output) = self.build(&config)?; diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 765bb64f95fdd..4f638b6edcdf8 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -13,7 +13,10 @@ use eyre::{Context, ContextCompat, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{LoadConfig, find_contract_artifacts, read_constructor_args_file}, + utils::{ + LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane, + read_constructor_args_file, resolve_lane, + }, }; use foundry_common::{ FoundryTransactionBuilder, @@ -203,6 +206,11 @@ impl CreateArgs { self.tx.tempo.key_id = Some(ak.key_address); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane. + // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key. + let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?; + // Whether to broadcast the transaction or not let dry_run = !self.broadcast; @@ -223,6 +231,7 @@ impl CreateArgs { dry_run, None, Some(browser), + resolved_lane, ) .await } else if self.unlocked { @@ -239,6 +248,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } else if let Some(ak) = access_key { @@ -259,6 +269,7 @@ impl CreateArgs { dry_run, Some((signer, ak)), None, + resolved_lane, ) .await } else { @@ -282,6 +293,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } @@ -362,6 +374,7 @@ impl CreateArgs { dry_run: bool, tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>, browser_signer: Option>, + resolved_lane: Option, ) -> Result<()> where N::TransactionRequest: FoundryTransactionBuilder + serde::Serialize, @@ -398,7 +411,7 @@ impl CreateArgs { // If Tempo chain fee token must be set if chain.is_tempo() { - if let Some(fee_token) = self.tx.tempo.fee_token { + if let Some(fee_token) = self.tx.tempo.common.fee_token { deployer.tx.set_fee_token(fee_token); } else { deployer.tx.set_fee_token(DEFAULT_FEE_TOKEN); @@ -408,15 +421,18 @@ impl CreateArgs { // Apply user-provided gas, fee, nonce, and Tempo options. self.tx.apply::(&mut deployer.tx, is_legacy); - // For keychain mode, set key_id and nonce_key before gas estimation. // Convert the CREATE into an AA-compatible call entry since Tempo AA // transactions use a `calls` list instead of `to`+`input`. + if chain.is_tempo() { + deployer.tx.convert_create_to_call(); + } + + // For keychain mode, set key_id and nonce_key before gas estimation. if let Some((_, ref ak)) = tempo_keychain { deployer.tx.set_key_id(ak.key_address); if deployer.tx.nonce_key().is_none() { deployer.tx.set_nonce_key(U256::ZERO); } - deployer.tx.convert_create_to_call(); } // Fetch defaults from provider for values not specified by user. @@ -424,6 +440,20 @@ impl CreateArgs { deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?); } + maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?; + + if let Some((_, ref ak)) = tempo_keychain { + deployer + .tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } + // set access list if specified if let Some(access_list) = match self.tx.access_list { None => None, @@ -500,6 +530,11 @@ impl CreateArgs { return Ok(()); } + let tempo_sponsor = self.tx.tempo.sponsor_config().await?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut deployer.tx, deployer_address).await?; + } + // Deploy the actual contract let (deployed_contract, receipt) = if let Some(browser) = browser_signer { // Browser wallet signs and sends the transaction diff --git a/crates/forge/src/cmd/snapshot.rs b/crates/forge/src/cmd/snapshot.rs index c8dc2ba72aae1..7c6fb51ce3266 100644 --- a/crates/forge/src/cmd/snapshot.rs +++ b/crates/forge/src/cmd/snapshot.rs @@ -99,8 +99,11 @@ impl GasSnapshotArgs { } pub async fn run(mut self) -> Result<()> { - // Set fuzz seed so gas snapshots are deterministic - self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so gas snapshots are deterministic, + // but allow the user to override it via `--fuzz-seed`. + if self.test.fuzz_seed.is_none() { + self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let outcome = self.test.compile_and_run().await?; outcome.ensure_ok(false)?; diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 94376dc5238cd..dd8f3afd56197 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -3,7 +3,7 @@ use crate::{ MultiContractRunner, MultiContractRunnerBuilder, decode::decode_console_logs, gas_report::GasReport, - multi_runner::matches_artifact, + multi_runner::{MultiNetworkConfig, matches_artifact}, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ CallTraceDecoderBuilder, InternalTraceMode, TraceKind, @@ -31,7 +31,7 @@ use foundry_compilers::{ utils::source_files_iter, }; use foundry_config::{ - Config, figment, + Config, InlineConfig, figment, figment::{ Metadata, Profile, Provider, value::{Dict, Map}, @@ -39,10 +39,11 @@ use foundry_config::{ filter::GlobMatcher, }; use foundry_debugger::Debugger; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::evm::{ - BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, SpecFor, TempoEvmNetwork, - TxEnvFor, + BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor, }, opts::EvmOpts, traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth}, @@ -169,6 +170,14 @@ pub struct TestArgs { #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")] pub fuzz_runs: Option, + /// Run only the fuzz case at the given 1-based run index. + #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")] + pub fuzz_run: Option, + + /// Run the fuzz case from the given worker. Requires `--fuzz-run`. + #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")] + pub fuzz_worker: Option, + /// Timeout for each fuzz run in seconds. #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")] pub fuzz_timeout: Option, @@ -301,6 +310,10 @@ impl TestArgs { filter: &ProjectPathsAwareFilter, coverage: bool, ) -> Result { + if config.fuzz.run == Some(0) { + bail!("`fuzz.run` must be greater than 0"); + } + // Explicitly enable isolation for gas reports for more correct gas accounting. if self.gas_report { evm_opts.isolate = true; @@ -342,40 +355,80 @@ impl TestArgs { // Auto-detect network from fork chain ID when not explicitly configured. evm_opts.infer_network_from_fork().await; - // Dispatch based on network type. - let (libraries, mut outcome) = if evm_opts.networks.is_tempo() { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? - } else if evm_opts.networks.is_optimism() { - self.build_and_run_tests::( + // Parse inline config early to detect per-test network annotations. + let inline_config = InlineConfig::new_parsed(output, &config)?; + let override_networks = inline_config.referenced_override_networks(&config.profile); + + let (libraries, mut outcome) = if override_networks.is_empty() { + // Single-pass: no per-test network overrides, use global network setting. + self.dispatch_network( + &evm_opts, config, - evm_opts, + evm_opts.clone(), output, filter, coverage, should_debug, decode_internal, + MultiNetworkConfig::default(), ) .await? } else { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? + // Multi-pass: run each distinct network separately and merge results. + let all_override_networks = override_networks.clone(); + let multi_pass_timer = Instant::now(); + + // Default pass: global network, runs tests without an explicit network annotation. + let (libraries, mut outcome) = self + .dispatch_network( + &evm_opts, + config.clone(), + evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: None, + }, + ) + .await?; + + // Override passes: one per annotated network. + for &network in &override_networks { + let mut pass_evm_opts = evm_opts.clone(); + pass_evm_opts.networks = network.into(); + let (_, pass_outcome) = self + .dispatch_network( + &pass_evm_opts, + config.clone(), + pass_evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: Some(network), + }, + ) + .await?; + merge_outcomes(&mut outcome, pass_outcome); + } + + // Print the merged summary (per-pass summaries are suppressed in `run_tests_inner`). + if !self.summary && !shell::is_json() { + sh_println!("{}", outcome.summary(multi_pass_timer.elapsed()))?; + } + if self.summary && !outcome.results.is_empty() { + let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); + sh_println!("{}", &summary_report)?; + } + + (libraries, outcome) }; if should_draw { @@ -461,6 +514,7 @@ impl TestArgs { coverage: bool, should_debug: bool, decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, ) -> eyre::Result<(Libraries, TestOutcome)> { let verbosity = evm_opts.verbosity; let (evm_env, tx_env, fork_block) = @@ -476,6 +530,7 @@ impl TestArgs { .enable_isolation(evm_opts.isolate) .fail_fast(self.fail_fast) .set_coverage(coverage) + .with_multi_network(multi_network) .build::(output, evm_env, tx_env, evm_opts)?; let libraries = runner.libraries.clone(); @@ -483,6 +538,62 @@ impl TestArgs { Ok((libraries, outcome)) } + /// Dispatches `build_and_run_tests` to the correct network type based on `evm_opts.networks`. + #[allow(clippy::too_many_arguments)] + async fn dispatch_network( + &self, + dispatch_opts: &EvmOpts, + config: Config, + evm_opts: EvmOpts, + output: &ProjectCompileOutput, + filter: &ProjectPathsAwareFilter, + coverage: bool, + should_debug: bool, + decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, + ) -> eyre::Result<(Libraries, TestOutcome)> { + if dispatch_opts.networks.is_tempo() { + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } else { + #[cfg(feature = "optimism")] + if dispatch_opts.networks.is_optimism() { + return self + .build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await; + } + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } + } + /// Run all tests that matches the filter predicate from a test runner async fn run_tests_inner( &self, @@ -586,6 +697,11 @@ impl TestArgs { let libraries = runner.libraries.clone(); + // Capture multi-pass state before moving `runner` into the spawn task. + // In multi-pass mode the per-pass summary is suppressed; the merged summary is + // printed once by the caller after all passes complete. + let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty(); + // Run tests in a streaming fashion. let (tx, rx) = channel::<(String, SuiteResult)>(); let timer = Instant::now(); @@ -643,6 +759,13 @@ impl TestArgs { let tests = &mut suite_result.test_results; let has_tests = !tests.is_empty(); + // In multi-pass (per-test network override) mode, skip suites that contributed no + // tests to this pass so we don't emit a stray blank line in the suite header or + // pollute the outcome with empty entries. + if is_multi_pass && !has_tests && suite_result.warnings.is_empty() { + continue; + } + // Clear the addresses and labels from previous test. decoder.clear_addresses(); @@ -903,17 +1026,17 @@ impl TestArgs { if let Some(gas_report) = gas_report { let finalized = gas_report.finalize(); - sh_println!("{}", &finalized)?; + sh_println!("{finalized}")?; outcome.gas_report = Some(finalized); } - if !self.summary && !shell::is_json() { + if !is_multi_pass && !self.summary && !shell::is_json() { sh_println!("{}", outcome.summary(duration))?; } - if self.summary && !outcome.results.is_empty() { + if !is_multi_pass && self.summary && !outcome.results.is_empty() { let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); - sh_println!("{}", &summary_report)?; + sh_println!("{summary_report}")?; } // Reattach the task. @@ -980,6 +1103,12 @@ impl Provider for TestArgs { if let Some(fuzz_runs) = self.fuzz_runs { fuzz_dict.insert("runs".to_string(), fuzz_runs.into()); } + if let Some(fuzz_run) = self.fuzz_run { + fuzz_dict.insert("run".to_string(), fuzz_run.into()); + } + if let Some(fuzz_worker) = self.fuzz_worker { + fuzz_dict.insert("worker".to_string(), fuzz_worker.into()); + } if let Some(fuzz_timeout) = self.fuzz_timeout { fuzz_dict.insert("timeout".to_string(), fuzz_timeout.into()); } @@ -1023,6 +1152,29 @@ fn list( Ok(TestOutcome::empty(Some(runner.known_contracts), false)) } +/// Merges `other` into `base` by extending suite results. +/// +/// For suites that appear in both, test results are combined (function-level pass routing ensures +/// each function appears in exactly one pass, so there are no key conflicts in practice). +fn merge_outcomes(base: &mut TestOutcome, other: TestOutcome) { + for (suite_id, other_suite) in other.results { + match base.results.entry(suite_id) { + std::collections::btree_map::Entry::Vacant(e) => { + e.insert(other_suite); + } + std::collections::btree_map::Entry::Occupied(mut e) => { + let base_suite = e.get_mut(); + base_suite.test_results.extend(other_suite.test_results); + base_suite.warnings.extend(other_suite.warnings); + base_suite.duration = base_suite.duration.max(other_suite.duration); + } + } + } + if let Some(decoder) = other.last_run_decoder { + base.last_run_decoder = Some(decoder); + } +} + /// Load persisted filter (with last test run failures) from file. fn last_run_failures(config: &Config) -> Option { match fs::read_to_string(&config.test_failures_file) { @@ -1131,6 +1283,14 @@ mod tests { assert!(args.fuzz_seed.is_some()); } + #[test] + fn fuzz_run() { + let args: TestArgs = + TestArgs::parse_from(["foundry-cli", "--fuzz-run", "10", "--fuzz-worker", "2"]); + assert_eq!(args.fuzz_run, Some(10)); + assert_eq!(args.fuzz_worker, Some(2)); + } + #[test] fn extract_chain() { let test = |arg: &str, expected: Chain| { diff --git a/crates/forge/src/cmd/test/summary.rs b/crates/forge/src/cmd/test/summary.rs index f8a72272af53c..a0123e896d0bf 100644 --- a/crates/forge/src/cmd/test/summary.rs +++ b/crates/forge/src/cmd/test/summary.rs @@ -25,9 +25,9 @@ impl TestSummaryReport { impl Display for TestSummaryReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "{}", self.format_json_output(&self.is_detailed, &self.outcome))?; } else { - writeln!(f, "\n{}", &self.format_table_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "\n{}", self.format_table_output(&self.is_detailed, &self.outcome))?; } Ok(()) } diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index 6c93dc03b28b5..58b11d98874ed 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -146,7 +146,7 @@ impl GasReport { impl Display for GasReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output())?; + writeln!(f, "{}", self.format_json_output())?; } else { for (name, contract) in &self.contracts { if contract.functions.is_empty() { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 88bbc6156c812..675f0c3e6c99c 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -27,6 +27,7 @@ use foundry_evm::{ opts::EvmOpts, traces::{InternalTraceMode, TraceMode}, }; +use foundry_evm_networks::NetworkVariant; use foundry_linking::{LinkOutput, Linker}; use rayon::prelude::*; @@ -280,6 +281,25 @@ impl MultiContractRunner { } } +/// Tracks network assignment across a multi-network test run. +/// +/// When inline config specifies different networks for different tests, the runner performs one +/// pass per distinct network. This struct encodes which pass we're in so each `ContractRunner` +/// can skip tests that belong to a different pass. +/// +/// Default (empty `all_override_networks`, `None` pass) = single-pass mode, every test runs. +#[derive(Clone, Debug, Default)] +pub struct MultiNetworkConfig { + /// All networks explicitly referenced in inline config annotations across the whole suite. + /// Empty means single-pass mode (no per-test network overrides present). + pub all_override_networks: Vec, + /// The network this pass is responsible for. + /// `None` = default pass: runs tests *without* an explicit network annotation (or annotated + /// with a network not in `all_override_networks`). + /// `Some(v)` = override pass: runs only tests annotated with exactly `v`. + pub pass_network: Option, +} + /// Configuration for the test runner. /// /// This is modified after instantiation through inline config. @@ -311,6 +331,9 @@ pub struct TestRunnerConfig { pub isolation: bool, /// Whether to exit early on test failure or if test run interrupted. pub early_exit: EarlyExit, + + /// Multi-network pass configuration. Default = single-pass mode. + pub multi_network: MultiNetworkConfig, } impl TestRunnerConfig { @@ -423,6 +446,8 @@ pub struct MultiContractRunnerBuilder { pub isolation: bool, /// Whether to exit early on test failure. pub fail_fast: bool, + /// Multi-network pass configuration. + pub multi_network: MultiNetworkConfig, } impl MultiContractRunnerBuilder { @@ -437,6 +462,7 @@ impl MultiContractRunnerBuilder { isolation: Default::default(), decode_internal: Default::default(), fail_fast: false, + multi_network: Default::default(), } } @@ -470,6 +496,11 @@ impl MultiContractRunnerBuilder { self } + pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self { + self.multi_network = multi_network; + self + } + pub const fn fail_fast(mut self, fail_fast: bool) -> Self { self.fail_fast = fail_fast; self @@ -594,6 +625,7 @@ impl MultiContractRunnerBuilder { inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?), isolation: self.isolation, early_exit: EarlyExit::new(self.fail_fast), + multi_network: self.multi_network, config: self.config, }, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index d924c416759a2..7feaf35254636 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -109,6 +109,25 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { } } + /// Returns `true` if `func` should run in the current multi-network pass. + /// + /// In single-pass mode (no inline network overrides) every function passes. + /// In multi-pass mode: + /// - Default pass (`pass_network = None`): includes functions *without* an override annotation. + /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`. + fn function_matches_network_pass(&self, func: &Function) -> bool { + let multi = &self.mcr.tcfg.multi_network; + if multi.all_override_networks.is_empty() { + return true; + } + let profile = &self.tcfg.config.profile; + let func_network = self.mcr.inline_config.network_for(profile, self.name, &func.name); + match &multi.pass_network { + None => func_network.is_none_or(|n| !multi.all_override_networks.contains(&n)), + Some(target) => func_network.as_ref() == Some(target), + } + } + /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -380,6 +399,7 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { .abi .functions() .filter(|func| filter.matches_test_function(func)) + .filter(|func| self.function_matches_network_pass(func)) .collect::>(); debug!( "Found {} test functions out of {} in {:?}", @@ -826,7 +846,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { ); if let Some(ref progress) = progress { - progress.set_prefix(format!("{}\n{warn}\n", &func.name)); + progress.set_prefix(format!("{}\n{warn}\n", func.name)); } else { let _ = sh_warn!("{warn}"); } @@ -1052,7 +1072,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { self.cr.name, &func.name, fuzz_config.timeout, - fuzz_config.runs, + if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs }, ); let state = self.build_fuzz_state(false); diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 6e0acebc67225..5d8378c50c9ac 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -3000,7 +3000,7 @@ Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] +=====================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 132459 | 396 | | | | | +| 132471 | 396 | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -3023,7 +3023,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/FallbackWithCalldataTest.sol:CounterWithFallback", "deployment": { - "gas": 132459, + "gas": 132471, "size": 396 }, "functions": { diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 0eeb3757982e9..3eebca475a781 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -577,6 +577,32 @@ forgetest_init!(can_get_evm_opts, |prj, _cmd| { } }); +// Regression test for : +// the bare `ETH_RPC_URL` env var must NOT cause `forge` commands to set +// `eth_rpc_url` (which would silently fork all `forge test` runs). +// Only `--rpc-url`, `foundry.toml`, the `FOUNDRY_ETH_RPC_URL` env var, or +// cheatcodes should configure forking. +forgetest_init!(eth_rpc_url_env_does_not_set_fork_url, |prj, _cmd| { + prj.initialize_default_contracts(); + let url = "http://127.0.0.1:8545"; + + let mut cmd = prj.forge_bin(); + cmd.arg("config") + .arg("--root") + .arg(prj.root()) + .arg("--json") + .env("ETH_RPC_URL", url) + // Make sure the figment-style env var is not set in the test environment. + .env_remove("FOUNDRY_ETH_RPC_URL"); + let output = cmd.output().unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let config: Config = serde_json::from_str(stdout.as_ref()).unwrap(); + assert_eq!( + config.eth_rpc_url, None, + "bare ETH_RPC_URL must not propagate to forge config (regression #14538)" + ); +}); + // checks that we can set various config values forgetest_init!(can_set_config_values, |prj, _cmd| { prj.initialize_default_contracts(); @@ -1269,6 +1295,8 @@ forgetest_init!(test_default_config, |prj, cmd| { "show_progress": false, "fuzz": { "runs": 256, + "run": null, + "worker": null, "fail_on_revert": true, "max_test_rejects": 65536, "seed": null, diff --git a/crates/forge/tests/cli/failure_assertions.rs b/crates/forge/tests/cli/failure_assertions.rs index 48a17c723b261..77d5a5e84cfbb 100644 --- a/crates/forge/tests/cli/failure_assertions.rs +++ b/crates/forge/tests/cli/failure_assertions.rs @@ -70,8 +70,13 @@ Suite result: FAILED. 0 passed; 7 failed; 0 skipped; [ELAPSED] .stdout_eq( r#"No files changed, compilation skipped ... +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != [..]] testShouldFailExpectRevertNestedCreateInnerAddress() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterNestedCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterTopLevelCreate() ([GAS]) [FAIL: next call did not revert as expected] testShouldFailExpectRevertsNotOnImmediateNextCall() ([GAS]) -Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +Suite result: FAILED. 0 passed; 6 failed; 0 skipped; [ELAPSED] ... "#, ); diff --git a/crates/forge/tests/cli/inline_config.rs b/crates/forge/tests/cli/inline_config.rs index 04fb2369d83b0..ba01767d58b26 100644 --- a/crates/forge/tests/cli/inline_config.rs +++ b/crates/forge/tests/cli/inline_config.rs @@ -425,3 +425,107 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); + +// Checks that tests annotated with `forge-config: default.networks.network` run on the correct +// EVM network, and that unannotated tests run on the globally configured network. +// +// Each test makes a real call to the Tempo `TipFeeManager` precompile at +// `0xfeec000000000000000000000000000000000000` (a Tempo-only contract that exists on the +// Moderato testnet and is auto-injected by the in-memory Tempo EVM): +// +// * The default-network test asserts the precompile has no code (it does not exist on Ethereum). +// * The Tempo-network test asserts the precompile has code and `userTokens(address)` returns the +// unset zero-address sentinel, proving the Tempo network was actually selected for that test and +// the Tempo genesis state was loaded. +forgetest!(per_test_network_routing, |prj, cmd| { + prj.add_test( + "inline.sol", + r#" + address constant TIP_FEE_MANAGER = 0xfeEC000000000000000000000000000000000000; + + contract DefaultNetwork { + // No annotation -> runs on the globally selected network (Ethereum by default). + // The Tempo FeeManager precompile must NOT exist here. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + } + + contract TempoNetwork { + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + // Sentinel bytecode (0xef) is injected at every Tempo precompile address. + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + // Call a Tempo-only method: `userTokens(address)` returns the user's preferred + // fee token, or the zero address when none is set. + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + + // Mixed contract: one function annotated with Tempo, one unannotated (runs on Ethereum). + contract MixedNetwork { + // No annotation -> runs on Ethereum; precompile must be absent. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + "#, + ); + + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 3 test suites [ELAPSED]: 4 tests passed, 0 failed, 0 skipped (4 total tests) + +"#]]); +}); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index fd69907be2f09..8420e24eb3df7 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -1,4 +1,7 @@ -use forge_lint::{linter::Lint, sol::med::REGISTERED_LINTS}; +use forge_lint::{ + linter::Lint, + sol::{self, SolLint}, +}; use foundry_config::{ DenyLevel, LintSeverity, LinterConfig, SolidityErrorCode, lint::LintSpecificConfig, }; @@ -203,7 +206,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -230,7 +233,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -610,7 +613,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -637,7 +640,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -665,7 +668,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect 13 │ uint256 result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift "# @@ -694,7 +697,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]).stdout_eq(str![[r#" @@ -855,7 +858,7 @@ note[unused-import]: unused imports should be removed 8 │ import { _PascalCaseInfo } from "./ContractWithLints.sol"; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import "#]]); @@ -887,7 +890,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase 6 │ uint256 public CounterB_Fail_Lint; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `counterBFailLint` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable "#]]); @@ -992,7 +995,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { ], "children": [ { - "message": "https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic", + "message": "https://getfoundry.sh/forge/linting/unwrapped-modifier-logic", "code": null, "level": "help", "spans": [], @@ -1048,7 +1051,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { "rendered": null } ], - "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" + "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" } "#]], ); @@ -1129,46 +1132,46 @@ Warning: Key `deny_warnings` is being deprecated in favor of `deny = warnings`. #[tokio::test] async fn ensure_lint_rule_docs() { - const FOUNDRY_BOOK_LINT_PAGE_URL: &str = "https://book.getfoundry.sh/forge/linting"; - - // Fetch the content of the lint reference - let content = match reqwest::get(FOUNDRY_BOOK_LINT_PAGE_URL).await { - Ok(resp) => { - assert!( - resp.status().is_success(), - "Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}). Status: {status}", - status = resp.status() - ); - match resp.text().await { - Ok(text) => text, - Err(e) => { - panic!("Failed to read response text: {e}"); - } + let client = reqwest::Client::new(); + let mut failures = Vec::new(); + + for lint in registered_lints() { + let url = lint.help(); + let response = match client.get(url).send().await { + Ok(response) => response, + Err(err) => { + failures.push(format!("{} ({url}) could not be fetched: {err}", lint.id())); + continue; } + }; + + if !response.status().is_success() { + failures.push(format!("{} ({url}) returned HTTP {}", lint.id(), response.status())); + continue; } - Err(e) => { - panic!("Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}): {e}",); - } - }; - // Ensure no missing lints - let mut missing_lints = Vec::new(); - for lint in REGISTERED_LINTS { + let content = match response.text().await { + Ok(content) => content.to_lowercase(), + Err(err) => { + failures + .push(format!("{} ({url}) response body could not be read: {err}", lint.id())); + continue; + } + }; + let selector = lint.id().to_lowercase(); let selector_with_space = selector.replace('-', " "); - if !content.to_lowercase().contains(&selector) - && !content.to_lowercase().contains(&selector_with_space) - { - missing_lints.push(lint.id()); + if !content.contains(&selector) && !content.contains(&selector_with_space) { + failures.push(format!("{} ({url}) did not mention the lint id", lint.id())); } } - if !missing_lints.is_empty() { + if !failures.is_empty() { let mut msg = String::from( - "Foundry Book lint validation failed. The following lints must be added to the docs:\n", + "Foundry Book lint validation failed. The following lint pages are missing or invalid:\n", ); - for lint in missing_lints { - msg.push_str(&format!(" - {lint}\n")); + for failure in failures { + msg.push_str(&format!(" - {failure}\n")); } msg.push_str("Please open a PR: https://github.com/foundry-rs/book"); panic!("{msg}"); @@ -1177,11 +1180,21 @@ async fn ensure_lint_rule_docs() { #[test] fn ensure_no_privileged_lint_id() { - for lint in REGISTERED_LINTS { + for lint in registered_lints() { assert_ne!(lint.id(), "all", "lint-id 'all' is reserved. Please use a different id"); } } +fn registered_lints() -> impl Iterator { + sol::high::REGISTERED_LINTS + .iter() + .chain(sol::med::REGISTERED_LINTS) + .chain(sol::low::REGISTERED_LINTS) + .chain(sol::info::REGISTERED_LINTS) + .chain(sol::gas::REGISTERED_LINTS) + .chain(sol::codesize::REGISTERED_LINTS) +} + // forgetest!(dependency_warnings_do_not_affect_lint_exit_code, |prj, cmd| { // Library with code that triggers a solc warning (unused local variable) @@ -1265,3 +1278,195 @@ contract OldContract { "# ]]); }); + +const PRAGMA_INCONSISTENT_ALPHA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Alpha {} +"#; + +const PRAGMA_INCONSISTENT_BETA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract Beta {} +"#; + +forgetest!(pragma_inconsistent_cross_file, |prj, cmd| { + prj.add_source("Alpha", PRAGMA_INCONSISTENT_ALPHA); + prj.add_source("Beta", PRAGMA_INCONSISTENT_BETA); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +const PRAGMA_EXACT_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract A {} +"#; + +const PRAGMA_EXACT_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract B {} +"#; + +const PRAGMA_EXACT_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract C {} +"#; + +const PRAGMA_CARET_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract A {} +"#; + +const PRAGMA_CARET_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract B {} +"#; + +const PRAGMA_CARET_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract C {} +"#; + +const NO_PRAGMA_C: &str = r#" +// SPDX-License-Identifier: MIT + +contract C {} +"#; + +// Multiple files all using the exact same pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_exact_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_EXACT_C); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Multiple files all using the exact same caret pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_caret_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + prj.add_source("B", PRAGMA_CARET_B); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// A single file in the project cannot conflict with itself. +forgetest!(pragma_inconsistent_single_file_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Even files that share a requirement still emit when ANY other variant exists. +// Two files with `0.8.20` plus one file with `^0.8.20` => 3 emits total. +forgetest!(pragma_inconsistent_duplicates_among_conflict, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_CARET_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +// Files without a `pragma solidity` directive must not affect the conflict computation. +// Note: `add_raw_source` is used here to bypass the helper that would otherwise inject a default +// `pragma solidity =;` for files that omit one. +forgetest!(pragma_inconsistent_files_without_pragma, |prj, cmd| { + prj.add_raw_source("A", PRAGMA_EXACT_A); + prj.add_raw_source("B", PRAGMA_CARET_B); + // C has no pragma at all; should be ignored by the cross-file check. + prj.add_raw_source("C", NO_PRAGMA_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); diff --git a/crates/forge/tests/cli/lint/geiger.rs b/crates/forge/tests/cli/lint/geiger.rs index faecfb212fb90..202866e83e35f 100644 --- a/crates/forge/tests/cli/lint/geiger.rs +++ b/crates/forge/tests/cli/lint/geiger.rs @@ -21,7 +21,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ bytes memory stuff = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:10:20 @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 10 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:11:20 @@ -100,7 +100,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 11 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 3 linter note(s) ... diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 242a0ebb4267f..031d80f0cf071 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3241,7 +3241,7 @@ contract CounterScript is Script { error: the following required arguments were not provided: --broadcast -Usage: [..] script --broadcast --verify --rpc-url [ARGS]... +Usage: [..] script --broadcast --verify --rpc-url [ARGS]... For more information, try '--help'. diff --git a/crates/forge/tests/cli/test_cmd/fuzz.rs b/crates/forge/tests/cli/test_cmd/fuzz.rs index fefefb30d9b15..454b014a6e1bc 100644 --- a/crates/forge/tests/cli/test_cmd/fuzz.rs +++ b/crates/forge/tests/cli/test_cmd/fuzz.rs @@ -1,4 +1,5 @@ use alloy_primitives::U256; +use foundry_evm::fuzz::BaseCounterExample; use foundry_test_utils::{TestCommand, forgetest_init, str}; use regex::Regex; @@ -845,6 +846,8 @@ forgetest_init!(test_fuzz_random_uint_varies_across_runs, |prj, cmd| { prj.add_test( "RandomFuzzTest.t.sol", r#" +pragma solidity >=0.8.0; + import {Test} from "forge-std/Test.sol"; contract RandomFuzzTest is Test { @@ -868,3 +871,145 @@ Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) ... "#]]); }); + +forgetest_init!(test_fuzz_run_replays_random_uint_failure, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + function testFuzz_randomUint_shouldFail(uint256) public { + uint256 rand = vm.randomUint(0, 4); + assertTrue(rand != 0, "hit value 0"); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: hit value 0; counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +"#]]; + + cmd.args(["test", "--fuzz-seed", "1", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + assert_eq!(persisted_failure.fuzz.seed, Some(U256::from(1))); + assert_eq!(persisted_failure.fuzz.worker, Some(0)); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + cmd.forge_fuse() + .args([ + "test", + "--fuzz-seed", + "1", + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure().stdout_eq(expected_output); +}); + +forgetest_init!(test_fuzz_rerun_replays_random_uint_failure_without_seed, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + error Random(uint256 value); + + function testFuzz_randomUint_shouldFail(uint256) public { + revert Random(vm.randomUint()); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: Random([..]); counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +Tip: Run `forge test --rerun` to retry only the 1 failed test + +[SEED] (use `--fuzz-seed` to reproduce) + +"#]]; + + let assert = cmd + .args(["test", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + let reason = random_failure_reason(&stdout); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + let fuzz_seed = format!("{:#x}", persisted_failure.fuzz.seed.unwrap()); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + let assert = cmd + .forge_fuse() + .args([ + "test", + "--fuzz-seed", + &fuzz_seed, + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd + .forge_fuse() + .args(["test", "--rerun", "-j1"]) + .assert_failure() + .stdout_eq(expected_output); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure(); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); +}); + +fn random_failure_reason(stdout: &str) -> String { + Regex::new(r"\[FAIL: (Random\([^)]+\))") + .unwrap() + .captures(stdout) + .unwrap_or_else(|| panic!("{stdout}"))[1] + .to_string() +} diff --git a/crates/forge/tests/cli/test_cmd/repros.rs b/crates/forge/tests/cli/test_cmd/repros.rs index 32bfe6a98a9fd..3803385b496ab 100644 --- a/crates/forge/tests/cli/test_cmd/repros.rs +++ b/crates/forge/tests/cli/test_cmd/repros.rs @@ -783,6 +783,66 @@ ParserError: Source "Missing.sol" not found: File not found. Searched the follow "#]]); }); +// https://github.com/foundry-rs/foundry/issues/10463 +forgetest_init!(issue_10463, |prj, cmd| { + prj.add_test( + "Issue10463.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract Issue10463Test is Test { + event Foo(); + + error CustomError(uint256 code); + + function revertingBefore(bool shouldRevert) external { + if (shouldRevert) revert(); + emit Foo(); + } + + function revertingWithReason() external pure { + revert("revert reason"); + } + + function revertingWithCustomError() external pure { + revert CustomError(42); + } + + function testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() public { + vm.expectEmit(); + emit Foo(); + + this.revertingBefore(true); + } + + function testExpectEmitPreservesRevertReason() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithReason(); + } + + function testExpectEmitPreservesCustomError() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithCustomError(); + } +} +"#, + ); + + cmd.arg("test").assert_failure().stdout_eq(str![[r#" +... +Ran 3 tests for test/Issue10463.t.sol:Issue10463Test +[FAIL: CustomError(42)] testExpectEmitPreservesCustomError() ([GAS]) +[FAIL: revert reason] testExpectEmitPreservesRevertReason() ([GAS]) +[FAIL: EvmError: Revert] testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() ([GAS]) +Suite result: FAILED. 0 passed; 3 failed; 0 skipped; [ELAPSED] +... +"#]]); +}); + // https://github.com/foundry-rs/foundry/issues/12803 // Test gas underflow prevention on Cancun (no EIP-7702 gas floor) forgetest_init!(issue_12803_cancun, |prj, cmd| { diff --git a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol index 7e482c6673155..838183a1b0b5e 100644 --- a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol +++ b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol @@ -233,6 +233,63 @@ contract ExpectRevertWithReverterFailureTest is DSTest { aContract.doNotRevert(); aContract.callAndRevert(); } + + // + // Regression: must fail because 0xdead is not the actual reverter when a + // top-level CREATE constructor reverts directly. + function testShouldFailExpectRevertWrongReverterTopLevelCreate() public { + vm.expectRevert(address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // even when an exact-bytes pattern is also supplied for a top-level CREATE. + function testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() public { + vm.expectRevert(abi.encodePacked("Reverted by DContract"), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // for `expectPartialRevert(bytes4, address)` against a top-level CREATE. + function testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() public { + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail when the innermost reverting frame is a nested + // CREATE and the reverter address argument does not match the would-be + // deployed address of the failed deployment. + function testShouldFailExpectRevertWrongReverterNestedCreate() public { + vm.expectRevert(address(0xdead)); + new NestedDContractCreator(); + } + + // + // Regression: documents the intended semantics for nested CREATEs — the + // matched reverter is the *outer* would-be-deployed address (the contract + // whose deployment failed), NOT the innermost reverting CREATE's address. + // Supplying the inner address must fail. + function testShouldFailExpectRevertNestedCreateInnerAddress() public { + // Outer = NestedDContractCreator at this contract's next nonce. + // Inner = DContract created from inside the outer constructor (deployer + // is the outer, nonce 1). + address outer = + vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + address inner = vm.computeCreateAddress(outer, 1); + vm.expectRevert(inner); + new NestedDContractCreator(); + } +} + +// Used by `testShouldFailExpectRevertWrongReverterNestedCreate`: a contract whose +// constructor directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } } contract ExpectRevertCountFailureTest is DSTest { diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 87864721432d9..589a0a5069e37 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -24,3 +24,7 @@ eyre.workspace = true heck.workspace = true rayon.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/lint/README.md b/crates/lint/README.md index e7c6471555da3..9d5dad0c0272e 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -17,11 +17,14 @@ It helps enforce best practices and improve code quality within Foundry projects - `divide-before-multiply`: Warns against performing division before multiplication in the same expression, which can cause precision loss. - `incorrect-erc20-interface`: Flags ERC20 interfaces and implementations with non-compliant function signatures. - `incorrect-erc721-interface`: Flags ERC721 interfaces and implementations with non-compliant function signatures. + - `tx-origin`: Flags use of `tx.origin` in authorization-like predicates. - `unsafe-typecast`: Typecasts that can truncate values should be checked. - **Low Severity:** - `block-timestamp`: Warns when `block.timestamp` is used in a comparison, as it may be manipulated by validators. + - `missing-zero-check`: Address parameter is used in a state write or value transfer without a zero-address check. - **Informational / Style Guide:** - `boolean-equal`: Boolean comparisons to constants should be simplified. + - `too-many-digits`: Numeric literals with 5+ consecutive zeros are error-prone. - `pascal-case-struct`: Flags for struct names not adhering to `PascalCase`. - `mixed-case-function`: Flags for function names not adhering to `mixedCase`. - `mixed-case-variable`: Flags for mutable variable names not adhering to `mixedCase`. @@ -31,10 +34,15 @@ It helps enforce best practices and improve code quality within Foundry projects - `unaliased-plain-import`: Use named imports `{A, B}` or alias `import ".." as X`. - `named-struct-fields`: Prefer initializing structs with named fields. - `unsafe-cheatcode`: Usage of unsafe cheatcodes that can perform dangerous operations. + - `multi-contract-file`: Prefer having only one contract, interface, or library per file. + - `interface-file-naming`: Interface file names should be prefixed with `I`. + - `interface-naming`: Interface names should be prefixed with `I`. + - `pragma-inconsistent`: Flags projects whose source files declare different Solidity pragma version requirements. - **Gas Optimizations:** - `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings. - `could-be-immutable`: Recommends declaring constructor-only state variables as `immutable`. - `custom-errors`: Recommends using custom errors instead of strings and plain reverts for potential gas savings. + - `unused-state-variables`: State variables that are never used should be removed. - **Code Size:** - `unwrapped-modifier-logic`: Recommends wrapping modifier logic to reduce contract code size. diff --git a/crates/lint/docs/README.md b/crates/lint/docs/README.md new file mode 100644 index 0000000000000..5eb4110a92e57 --- /dev/null +++ b/crates/lint/docs/README.md @@ -0,0 +1,52 @@ +# Forge lint documentation + +This directory contains one markdown file per registered `forge-lint` rule. Each file is referenced +by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the +[Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. + +## Adding a new lint + +When you add a new lint with `declare_forge_lint!`, you **must** also add a documentation file at +`crates/lint/docs/.md`. The presence of the file is enforced by the +`registered_lints_have_docs` unit test in [`crates/lint/src/sol/mod.rs`](../src/sol/mod.rs). + +Use [`_template.md`](./_template.md) as a starting point. + +## File structure + +Each lint doc file should follow this structure: + +```markdown +# + +**Severity**: `` +**ID**: `` + +A one-paragraph description of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` + +## Configuration + +Document any inline-config or `foundry.toml` options that affect this lint, if any. +``` diff --git a/crates/lint/docs/_template.md b/crates/lint/docs/_template.md new file mode 100644 index 0000000000000..41c735a0ba579 --- /dev/null +++ b/crates/lint/docs/_template.md @@ -0,0 +1,28 @@ +# + +**Severity**: `` +**ID**: `` + +One-paragraph summary of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` diff --git a/crates/lint/docs/asm-keccak256.md b/crates/lint/docs/asm-keccak256.md new file mode 100644 index 0000000000000..4678cfe9f8d12 --- /dev/null +++ b/crates/lint/docs/asm-keccak256.md @@ -0,0 +1,42 @@ +# Inefficient keccak256 call + +**Severity**: `Gas` +**ID**: `asm-keccak256` + +Flags calls to the high-level `keccak256(...)` builtin that can be cheaply rewritten with inline +assembly. + +## What it does + +Reports `keccak256(arg)` calls and (when possible) emits a fix suggestion that uses inline +assembly to compute the hash directly, avoiding the overhead of the high-level call. + +## Why is this bad? + +The high-level `keccak256` call performs additional memory management and ABI encoding compared +to a direct `keccak256(ptr, len)` opcode invocation. In hot paths the difference is visible in +gas reports. + +## Example + +### Bad + +```solidity +bytes32 h = keccak256(abi.encodePacked(a, b)); +``` + +### Good + +```solidity +bytes32 h; +assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, a) + mstore(add(m, 0x20), b) + h := keccak256(m, 0x40) +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/block-timestamp.md b/crates/lint/docs/block-timestamp.md new file mode 100644 index 0000000000000..a51b55ff5d8cc --- /dev/null +++ b/crates/lint/docs/block-timestamp.md @@ -0,0 +1,44 @@ +# Use of block.timestamp in comparisons + +**Severity**: `Low` +**ID**: `block-timestamp` + +Flags use of `block.timestamp` as an operand of a comparison, where its value can be slightly +manipulated by the block proposer. + +## What it does + +Reports any comparison expression (`<`, `<=`, `>`, `>=`, `==`, `!=`) that directly or +transitively reads `block.timestamp`. + +## Why is this bad? + +Block proposers can adjust `block.timestamp` within a small window (a few seconds). This is +usually harmless, but for short-window logic — auctions ending, randomness, time-locked +withdrawals — a few seconds of manipulation can be enough for an attacker to capture value. + +Using `block.timestamp` for general scheduling (hours/days) is fine; what's risky is fine-grained +timing and treating timestamps as a source of randomness. + +## Example + +### Bad + +```solidity +function settle() external { + require(block.timestamp >= auctionEnd, "auction ongoing"); + // ... +} +``` + +### Good + +```solidity +// Prefer block numbers for tight windows, or accept a clearly large grace period. +require(block.number >= endBlock, "auction ongoing"); +``` + +## Notes + +This lint is intentionally conservative: not every flagged comparison is exploitable. Review +each occurrence in context. diff --git a/crates/lint/docs/boolean-cst.md b/crates/lint/docs/boolean-cst.md new file mode 100644 index 0000000000000..f5c65dfec2789 --- /dev/null +++ b/crates/lint/docs/boolean-cst.md @@ -0,0 +1,37 @@ +# Misuse of a boolean constant + +**Severity**: `Med` +**ID**: `boolean-cst` + +Flags expressions where a boolean constant (`true`/`false`) is used as a control-flow condition +or operand of a boolean operator, which usually indicates dead code or a leftover debug toggle. + +## What it does + +Reports `if (true)`, `if (false)`, `while (true)` outside of intentional infinite loops, and +boolean operators (`&&`, `||`) where one side is a literal `true`/`false`. + +## Why is this bad? + +A literal boolean as a condition makes the surrounding branch dead, hides logic errors, or +preserves a forgotten debug shortcut that bypasses real checks. + +## Example + +### Bad + +```solidity +if (true) { // always taken + doSomething(); +} +require(condition && true, "unreachable"); // 'true' is redundant +``` + +### Good + +```solidity +if (condition) { + doSomething(); +} +require(condition, "..."); +``` diff --git a/crates/lint/docs/boolean-equal.md b/crates/lint/docs/boolean-equal.md new file mode 100644 index 0000000000000..9397003b039b4 --- /dev/null +++ b/crates/lint/docs/boolean-equal.md @@ -0,0 +1,34 @@ +# Boolean comparison to a constant + +**Severity**: `Info` +**ID**: `boolean-equal` + +Flags expressions of the form `x == true`, `x == false`, `x != true`, `x != false`, which can be +simplified. + +## What it does + +Reports any equality comparison between a boolean expression and a literal `true` or `false`. + +## Why is this bad? + +Comparing a boolean to a boolean literal is redundant and harms readability. Use the boolean +expression directly (or its negation). + +## Example + +### Bad + +```solidity +if (paused == true) revert(); +if (paused == false) doSomething(); +require(ok != false, "fail"); +``` + +### Good + +```solidity +if (paused) revert(); +if (!paused) doSomething(); +require(ok, "fail"); +``` diff --git a/crates/lint/docs/could-be-immutable.md b/crates/lint/docs/could-be-immutable.md new file mode 100644 index 0000000000000..bda1de6379955 --- /dev/null +++ b/crates/lint/docs/could-be-immutable.md @@ -0,0 +1,42 @@ +# State variable could be immutable + +**Severity**: `Gas` +**ID**: `could-be-immutable` + +Flags state variables that are assigned only in the constructor and never written to afterward — +making them eligible to be declared `immutable`. + +## What it does + +Reports each non-`constant`, non-`immutable` state variable whose only writes occur in the +constructor (or in initialization at declaration time). + +## Why is this bad? + +`immutable` state variables are stored in the deployed bytecode rather than in storage, eliminating +an `SLOAD` per access and saving substantial gas across the contract's lifetime. Declaring such +variables `immutable` also expresses intent and prevents future writes. + +## Example + +### Bad + +```solidity +contract C { + address owner; + constructor() { owner = msg.sender; } +} +``` + +### Good + +```solidity +contract C { + address immutable OWNER; + constructor() { OWNER = msg.sender; } +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/custom-errors.md b/crates/lint/docs/custom-errors.md new file mode 100644 index 0000000000000..9e01e01d593e9 --- /dev/null +++ b/crates/lint/docs/custom-errors.md @@ -0,0 +1,45 @@ +# Prefer custom errors over revert strings + +**Severity**: `Gas` +**ID**: `custom-errors` + +Flags `require(cond, "message")`, `revert("message")`, and `revert()` calls; suggests replacing +them with a `revert CustomError(...)`. + +## What it does + +Reports `require` calls whose second argument is a string literal, and `revert(...)` calls that +are either bare or have a string-literal argument. + +## Why is this bad? + +Custom errors: +- cost less gas than encoding/decoding a string, +- can carry typed parameters for richer diagnostics, +- shrink contract bytecode (string constants live in code). + +Solidity 0.8.4+ supports custom errors natively. + +## Example + +### Bad + +```solidity +require(amount > 0, "amount must be > 0"); +revert("not authorized"); +revert(); +``` + +### Good + +```solidity +error AmountZero(); +error NotAuthorized(); + +if (amount == 0) revert AmountZero(); +if (!authorized) revert NotAuthorized(); +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/divide-before-multiply.md b/crates/lint/docs/divide-before-multiply.md new file mode 100644 index 0000000000000..f082bef19a1bd --- /dev/null +++ b/crates/lint/docs/divide-before-multiply.md @@ -0,0 +1,32 @@ +# Divide before multiply + +**Severity**: `Med` +**ID**: `divide-before-multiply` + +Flags arithmetic expressions where division is performed before multiplication, which can cause +unintended precision loss in integer arithmetic. + +## What it does + +Warns on expressions of the form `(a / b) * c` (or equivalent shapes), where the integer division +truncates before the result is multiplied. + +## Why is this bad? + +Solidity's integer division truncates toward zero. Performing `(a / b) * c` discards the remainder +of `a / b` before scaling, while `(a * c) / b` preserves precision. This pattern frequently +manifests as fee/share/yield miscalculations. + +## Example + +### Bad + +```solidity +uint256 share = (amount / total) * weight; // truncates first, then scales +``` + +### Good + +```solidity +uint256 share = (amount * weight) / total; // preserves precision +``` diff --git a/crates/lint/docs/erc20-unchecked-transfer.md b/crates/lint/docs/erc20-unchecked-transfer.md new file mode 100644 index 0000000000000..d7d053e020cca --- /dev/null +++ b/crates/lint/docs/erc20-unchecked-transfer.md @@ -0,0 +1,43 @@ +# Unchecked ERC20 transfer return value + +**Severity**: `High` +**ID**: `erc20-unchecked-transfer` + +Flags calls to ERC20 `transfer` and `transferFrom` where the boolean return value is ignored. + +## What it does + +Warns when a function with the same signature as +`transfer(address,uint256)` or `transferFrom(address,address,uint256)` and a `bool` return type is +invoked but the result is not checked. + +## Why is this bad? + +The ERC20 spec allows tokens to signal failure by returning `false` instead of reverting. Ignoring +the return value lets a "failed" transfer go unnoticed, allowing accounting to drift and creating +common DeFi exploits. Use a wrapper such as OpenZeppelin's `SafeERC20` or check the boolean +explicitly. + +## Example + +### Bad + +```solidity +token.transfer(to, amount); +token.transferFrom(from, to, amount); +``` + +### Good + +```solidity +require(token.transfer(to, amount), "transfer failed"); +require(token.transferFrom(from, to, amount), "transferFrom failed"); + +// or use SafeERC20 +SafeERC20.safeTransfer(token, to, amount); +``` + +## Notes + +This lint can produce false positives when the callee does not strictly conform to the ERC20 +interface (e.g. tokens that revert on failure rather than returning `false`). diff --git a/crates/lint/docs/incorrect-erc20-interface.md b/crates/lint/docs/incorrect-erc20-interface.md new file mode 100644 index 0000000000000..65fb8313c205f --- /dev/null +++ b/crates/lint/docs/incorrect-erc20-interface.md @@ -0,0 +1,42 @@ +# Incorrect ERC20 interface + +**Severity**: `Med` +**ID**: `incorrect-erc20-interface` + +Flags interfaces or contracts whose function signatures match an ERC20 method by name and +parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC20 method +(`totalSupply`, `balanceOf`, `transfer`, `transferFrom`, `approve`, `allowance`), the lint checks +that the return type matches the spec. A mismatch is reported. + +## Why is this bad? + +Tokens that diverge from the ERC20 spec break composability with the wider ecosystem (DEXes, +lending protocols, multisigs) and are a common source of integration bugs and exploits. + +## Example + +### Bad + +```solidity +interface IBadERC20 { + function balanceOf(address) external view returns (bool); // should be uint256 + function transfer(address, uint256) external; // should return bool +} +``` + +### Good + +```solidity +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-erc721-interface.md b/crates/lint/docs/incorrect-erc721-interface.md new file mode 100644 index 0000000000000..4803afdde7cc1 --- /dev/null +++ b/crates/lint/docs/incorrect-erc721-interface.md @@ -0,0 +1,48 @@ +# Incorrect ERC721 interface + +**Severity**: `Med` +**ID**: `incorrect-erc721-interface` + +Flags interfaces or contracts whose function signatures match an ERC721 (or ERC165) method by +name and parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC721/ERC165 method +(`balanceOf`, `ownerOf`, `safeTransferFrom`, `transferFrom`, `approve`, `setApprovalForAll`, +`getApproved`, `isApprovedForAll`, `supportsInterface`), the lint checks that the return type +matches the spec. A mismatch is reported. + +## Why is this bad? + +Non-conforming NFT contracts break marketplaces, indexers, and any protocol that relies on the +ERC721 spec. A wrong return type often compiles and deploys silently but causes integration +failures at runtime. + +## Example + +### Bad + +```solidity +interface IBadERC721 { + function balanceOf(address) external view returns (bool); // should be uint256 + function ownerOf(uint256) external view returns (bool); // should be address + function supportsInterface(bytes4) external view returns (uint256); // should be bool +} +``` + +### Good + +```solidity +interface IERC721 { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-shift.md b/crates/lint/docs/incorrect-shift.md new file mode 100644 index 0000000000000..a9a70c7f93128 --- /dev/null +++ b/crates/lint/docs/incorrect-shift.md @@ -0,0 +1,37 @@ +# Incorrect shift order + +**Severity**: `High` +**ID**: `incorrect-shift` + +Flags shift operations where a literal appears on the left and a non-literal on the right, which +is almost always the wrong operand order. + +## What it does + +Warns when the left-hand operand of `<<` or `>>` is a numeric literal and the right-hand operand +is a non-literal expression (e.g. a variable, function call, or composite expression). + +## Why is this bad? + +Shift expressions like `2 << x` are usually a typo for `x << 2`. In the former, the *value being +shifted* is a tiny constant and the *shift amount* is dynamic — almost never the intended +behavior, and a known source of bugs in production contracts. + +## Example + +### Bad + +```solidity +result = 2 << stateValue; // shift amount comes from state +result = 8 >> localValue; // shift amount comes from a local +result = 16 << (stateValue + 1); // shift amount is a dynamic expression +``` + +### Good + +```solidity +result = stateValue << 2; +result = localValue >> 3; +result = stateValue << localShiftAmount; +result = 1 << 8; // both literals — fine +``` diff --git a/crates/lint/docs/inline-assembly.md b/crates/lint/docs/inline-assembly.md new file mode 100644 index 0000000000000..bba61148b84c5 --- /dev/null +++ b/crates/lint/docs/inline-assembly.md @@ -0,0 +1,69 @@ +# Inline assembly + +**Severity**: `Info` +**ID**: `inline-assembly` + +Flags every `assembly { ... }` block. Inline assembly bypasses many of Solidity's safety +features (type checks, overflow checks, memory layout invariants) and is a common source of +high-impact bugs, so each occurrence should be reviewed deliberately. + +## What it does + +Reports every inline assembly statement, including blocks declared with the `"evmasm"` dialect +and/or the `("memory-safe")` flag. Blocks declared as memory-safe — either via the modern +`("memory-safe")` flag or the legacy `/// @solidity memory-safe-assembly` NatSpec marker — are +still reported, but with a softer message acknowledging the developer attestation: review +focuses on business logic and side effects rather than memory layout. + +## Why is this bad? + +Assembly skips Solidity's compile-time checks and many of its runtime guarantees. Mistakes +inside an `assembly` block can corrupt memory, break the free memory pointer, leak storage, +escalate privileges via `delegatecall`, or destroy the contract via `selfdestruct`. Even when +required for gas or features unavailable in high-level Solidity, assembly should be small, +documented, and reviewed. + +## When inline assembly is reasonable + +Some idioms are widely used and generally safe: + +- Reading transaction/chain context: `chainid()`, `gas()`, `returndatasize()`. +- Probing code: `codesize()`, `extcodesize(addr)`, `extcodehash(addr)`. +- Reading the free memory pointer: `mload(0x40)`. +- Cheap hashing of a known memory layout, when paired with `("memory-safe")`. + +If you must use assembly: + +1. Keep the block minimal and well-commented. +2. Add the `("memory-safe")` flag when the block does not violate Solidity's memory model, so + the optimizer (and reviewers) can rely on it. The legacy + `/// @solidity memory-safe-assembly` NatSpec marker on the line directly above the block is + also recognized for compatibility with older codebases. +3. Suppress the lint locally to mark the block as audited: + ```solidity + // forge-lint: disable-next-line(inline-assembly) + assembly ("memory-safe") { /* reviewed: ... */ } + ``` + +## Example + +### Bad + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory) { + assembly { + let ok := call(gas(), target, 0, add(data.offset, 0), data.length, 0, 0) + // ... + } +} +``` + +### Good + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory result) { + bool ok; + (ok, result) = target.call(data); + require(ok, "call failed"); +} +``` diff --git a/crates/lint/docs/interface-file-naming.md b/crates/lint/docs/interface-file-naming.md new file mode 100644 index 0000000000000..ff72a0c175e8e --- /dev/null +++ b/crates/lint/docs/interface-file-naming.md @@ -0,0 +1,31 @@ +# Interface file naming + +**Severity**: `Info` +**ID**: `interface-file-naming` + +Flags Solidity files whose only top-level declaration is an interface but whose filename is not +prefixed with `I`. + +## What it does + +Reports interface-only files whose path basename does not start with `I` (e.g. `IERC20.sol`). + +## Why is this bad? + +Prefixing interface filenames with `I` is the prevailing convention in the Solidity ecosystem. +Following it makes import paths predictable and lets reviewers tell at a glance whether they are +looking at an interface or an implementation. + +## Example + +### Bad + +```text +contracts/Token.sol // file contains only `interface Token { ... }` +``` + +### Good + +```text +contracts/IToken.sol // file contains only `interface IToken { ... }` +``` diff --git a/crates/lint/docs/interface-naming.md b/crates/lint/docs/interface-naming.md new file mode 100644 index 0000000000000..5c6b12b946091 --- /dev/null +++ b/crates/lint/docs/interface-naming.md @@ -0,0 +1,31 @@ +# Interface name should be prefixed with 'I' + +**Severity**: `Info` +**ID**: `interface-naming` + +Flags `interface` declarations whose names are not prefixed with `I`. + +## What it does + +Reports `interface Foo` where `Foo` does not start with `I` (e.g. `IFoo`). + +## Why is this bad? + +Prefixing interfaces with `I` is the prevailing convention in Solidity codebases (`IERC20`, +`IERC721`, `IUniswapV2Pair`, ...). Following it makes the role of each type unambiguous at use +sites and aligns with the matching +[`interface-file-naming`](https://getfoundry.sh/forge/linting/interface-file-naming) lint. + +## Example + +### Bad + +```solidity +interface ERC20 { /* ... */ } +``` + +### Good + +```solidity +interface IERC20 { /* ... */ } +``` diff --git a/crates/lint/docs/missing-zero-check.md b/crates/lint/docs/missing-zero-check.md new file mode 100644 index 0000000000000..7eab1f3a00117 --- /dev/null +++ b/crates/lint/docs/missing-zero-check.md @@ -0,0 +1,39 @@ +# Missing zero-address check + +**Severity**: `Low` +**ID**: `missing-zero-check` + +Flags entry-point functions and constructors where an `address` parameter flows into a state write +or value transfer without a zero-address guard. + +## What it does + +Performs a taint analysis from each `address` parameter of an externally callable, state-mutating +function (or constructor) and reports a parameter that reaches a sink (state write, `transfer`, +`call{value: ...}`, etc.) without first being compared against `address(0)` in an `if`/`require`/ +`assert` predicate. + +## Why is this bad? + +Forgetting a zero-address check is a common source of value loss: tokens become permanently +unrecoverable, ownership is renounced unintentionally, or upgrades are bricked. Adding an explicit +guard is cheap and removes an entire class of operational mistakes. + +## Example + +### Bad + +```solidity +function setOwner(address newOwner) external onlyOwner { + owner = newOwner; // no zero-address check +} +``` + +### Good + +```solidity +function setOwner(address newOwner) external onlyOwner { + require(newOwner != address(0), "zero address"); + owner = newOwner; +} +``` diff --git a/crates/lint/docs/mixed-case-function.md b/crates/lint/docs/mixed-case-function.md new file mode 100644 index 0000000000000..9997dcb5691c7 --- /dev/null +++ b/crates/lint/docs/mixed-case-function.md @@ -0,0 +1,32 @@ +# Function names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-function` + +Flags function names that do not follow `mixedCase`. + +## What it does + +Reports functions whose names contain underscores, start with an uppercase letter, or otherwise +deviate from `mixedCase`. Test functions starting with `test`, `invariant_`, or `statefulFuzz` +and user-defined patterns (e.g. `ERC20`) are exempted. + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for function names. Consistent style makes call +sites uniform, helps editor tooling, and reduces friction in code review. + +## Example + +### Bad + +```solidity +function get_balance() external view returns (uint256); +function GetBalance() external view returns (uint256); +``` + +### Good + +```solidity +function getBalance() external view returns (uint256); +``` diff --git a/crates/lint/docs/mixed-case-variable.md b/crates/lint/docs/mixed-case-variable.md new file mode 100644 index 0000000000000..3341e1a0c48ad --- /dev/null +++ b/crates/lint/docs/mixed-case-variable.md @@ -0,0 +1,36 @@ +# Mutable variable names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-variable` + +Flags mutable variable names (locals, parameters, mutable state) that do not follow `mixedCase`. + +## What it does + +Reports mutable variable identifiers that contain underscores, start with an uppercase letter, +or otherwise deviate from `mixedCase`. + +`constant` and `immutable` state variables are not flagged by this lint — see +[`screaming-snake-case-const`](https://getfoundry.sh/forge/linting/screaming-snake-case-const) and +[`screaming-snake-case-immutable`](https://getfoundry.sh/forge/linting/screaming-snake-case-immutable). + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for mutable variables. Consistent style makes +code easier to scan and review. + +## Example + +### Bad + +```solidity +uint256 public total_supply; +address Owner; +``` + +### Good + +```solidity +uint256 public totalSupply; +address owner; +``` diff --git a/crates/lint/docs/multi-contract-file.md b/crates/lint/docs/multi-contract-file.md new file mode 100644 index 0000000000000..beabc827e4ea6 --- /dev/null +++ b/crates/lint/docs/multi-contract-file.md @@ -0,0 +1,37 @@ +# Multiple contracts in one file + +**Severity**: `Info` +**ID**: `multi-contract-file` + +Flags source files that declare more than one top-level contract, interface, or library. + +## What it does + +Reports each top-level `contract`, `interface`, or `library` definition (after the first) in a +file that contains more than one such declaration. + +## Why is this bad? + +Keeping one contract per file improves discoverability (`grep`, IDE jump-to-file), simplifies +import paths, and avoids unintentional bytecode bloat from artifacts that bundle unrelated +contracts. + +## Example + +### Bad + +```solidity +// File: Token.sol +contract TokenA { /* ... */ } +contract TokenB { /* ... */ } +``` + +### Good + +```solidity +// File: TokenA.sol +contract TokenA { /* ... */ } + +// File: TokenB.sol +contract TokenB { /* ... */ } +``` diff --git a/crates/lint/docs/named-struct-fields.md b/crates/lint/docs/named-struct-fields.md new file mode 100644 index 0000000000000..45713e2555ddc --- /dev/null +++ b/crates/lint/docs/named-struct-fields.md @@ -0,0 +1,31 @@ +# Prefer named struct fields + +**Severity**: `Info` +**ID**: `named-struct-fields` + +Flags struct construction expressions that pass fields positionally instead of by name. + +## What it does + +Reports `Struct(a, b, c)` style struct construction; suggests `Struct({ field1: a, field2: b, +field3: c })` instead. + +## Why is this bad? + +Positional struct construction is fragile: adding or reordering fields silently changes the +meaning of every existing call site. Named-field construction is self-documenting and resilient +to struct changes. + +## Example + +### Bad + +```solidity +User memory u = User(addr, 100, true); +``` + +### Good + +```solidity +User memory u = User({ wallet: addr, balance: 100, active: true }); +``` diff --git a/crates/lint/docs/pascal-case-struct.md b/crates/lint/docs/pascal-case-struct.md new file mode 100644 index 0000000000000..02a243bd56bf4 --- /dev/null +++ b/crates/lint/docs/pascal-case-struct.md @@ -0,0 +1,31 @@ +# Struct names should use PascalCase + +**Severity**: `Info` +**ID**: `pascal-case-struct` + +Flags struct definitions whose names do not follow `PascalCase`. + +## What it does + +Reports any `struct` whose identifier does not match the `PascalCase` convention. + +## Why is this bad? + +The Solidity style guide recommends `PascalCase` for type-like names (contracts, structs, +enums, libraries). Consistent casing makes code easier to scan and integrates with editor +features and external tooling. + +## Example + +### Bad + +```solidity +struct user_info { uint256 balance; } +struct USERINFO { uint256 balance; } +``` + +### Good + +```solidity +struct UserInfo { uint256 balance; } +``` diff --git a/crates/lint/docs/pragma-inconsistent.md b/crates/lint/docs/pragma-inconsistent.md new file mode 100644 index 0000000000000..095f45783773d --- /dev/null +++ b/crates/lint/docs/pragma-inconsistent.md @@ -0,0 +1,41 @@ +# Inconsistent pragma directives + +**Severity**: `Info` +**ID**: `pragma-inconsistent` + +Flags projects whose source files declare incompatible or differently-shaped Solidity version +pragmas. + +## What it does + +Inspects every `pragma solidity ...;` directive across all input source files and reports when +their version requirements are inconsistent (different exact versions, mixed caret/tilde/range +shapes, etc.). + +## Why is this bad? + +A project compiled under multiple Solidity versions can subtly change behavior between files +(e.g. checked arithmetic, default visibility, ABI encoding). Aligning pragmas across the project +removes a hidden source of integration bugs and makes upgrades coordinated. + +## Example + +### Bad + +```solidity +// A.sol +pragma solidity 0.8.18; + +// B.sol +pragma solidity ^0.8.20; + +// C.sol +pragma solidity >=0.7.0 <0.9.0; +``` + +### Good + +```solidity +// All files +pragma solidity 0.8.20; +``` diff --git a/crates/lint/docs/rtlo.md b/crates/lint/docs/rtlo.md new file mode 100644 index 0000000000000..58ce648752c6f --- /dev/null +++ b/crates/lint/docs/rtlo.md @@ -0,0 +1,32 @@ +# Right-to-left override character + +**Severity**: `High` +**ID**: `rtlo` + +Flags the presence of Unicode bidirectional override characters in source code, which can be used +to hide malicious behavior ("Trojan Source", [CVE-2021-42574](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574)). + +## What it does + +Detects the right-to-left override codepoint (`U+202E`) and other bidirectional control characters +embedded in identifiers, strings, and comments. + +## Why is this bad? + +These characters render source code in a different visual order than how the compiler reads it, +allowing an attacker to make malicious code look benign on review. Solidity contracts are public +and frequently audited visually; this attack vector must not be ignored. + +## Example + +### Bad + +```solidity +// transfer(victim‮, attacker)/* // U+202E hidden between args +``` + +### Good + +```solidity +// Avoid bidirectional override characters in code and comments. +``` diff --git a/crates/lint/docs/screaming-snake-case-const.md b/crates/lint/docs/screaming-snake-case-const.md new file mode 100644 index 0000000000000..72a16c5875fae --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-const.md @@ -0,0 +1,30 @@ +# Constants should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-const` + +Flags `constant` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `constant` whose identifier deviates from `SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for constants so they stand out from +mutable state and immutables at call sites. + +## Example + +### Bad + +```solidity +uint256 constant maxSupply = 1_000_000; +uint256 constant Max_Supply = 1_000_000; +``` + +### Good + +```solidity +uint256 constant MAX_SUPPLY = 1_000_000; +``` diff --git a/crates/lint/docs/screaming-snake-case-immutable.md b/crates/lint/docs/screaming-snake-case-immutable.md new file mode 100644 index 0000000000000..cee5590e16d27 --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-immutable.md @@ -0,0 +1,31 @@ +# Immutables should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-immutable` + +Flags `immutable` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `immutable` whose identifier deviates from +`SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for `immutable` variables so they +visually align with `constant` ones and stand out from mutable state at call sites. + +## Example + +### Bad + +```solidity +address immutable owner; +address immutable Owner; +``` + +### Good + +```solidity +address immutable OWNER; +``` diff --git a/crates/lint/docs/too-many-digits.md b/crates/lint/docs/too-many-digits.md new file mode 100644 index 0000000000000..5decb67bec9c3 --- /dev/null +++ b/crates/lint/docs/too-many-digits.md @@ -0,0 +1,32 @@ +# Numeric literal with too many digits + +**Severity**: `Info` +**ID**: `too-many-digits` + +Flags numeric literals containing five or more consecutive zeros, which are easy to misread. + +## What it does + +Reports decimal numeric literals that contain a run of 5 or more `0` characters. + +## Why is this bad? + +Long sequences of zeros are difficult to count visually, and an off-by-one zero is a common bug +(e.g. funding `1_000_000` instead of `10_000_000`). Use scientific notation, sub-denominations, or +underscore separators to make the magnitude obvious. + +## Example + +### Bad + +```solidity +uint256 amount = 1000000000000000000; +``` + +### Good + +```solidity +uint256 amount = 1e18; +uint256 amount2 = 1 ether; +uint256 amount3 = 1_000_000_000_000_000_000; +``` diff --git a/crates/lint/docs/tx-origin.md b/crates/lint/docs/tx-origin.md new file mode 100644 index 0000000000000..26877cf9c0116 --- /dev/null +++ b/crates/lint/docs/tx-origin.md @@ -0,0 +1,34 @@ +# Use of tx.origin for authorization + +**Severity**: `Med` +**ID**: `tx-origin` + +Flags use of `tx.origin` inside authorization-like predicates such as `require`, `assert`, `if`, +`while`, and `for` conditions. + +## What it does + +Reports `tx.origin` reads when they are used as part of a guard condition. Plain reads outside of +guard predicates are not reported. + +## Why is this bad? + +`tx.origin` is the original externally owned account that started the whole transaction, not the +immediate caller. If authorization checks rely on `tx.origin`, a malicious contract can call the +protected contract while the legitimate owner is the transaction origin. + +Use `msg.sender` for authorization checks instead. + +## Example + +### Bad + +```solidity +require(tx.origin == owner, "not owner"); +``` + +### Good + +```solidity +require(msg.sender == owner, "not owner"); +``` diff --git a/crates/lint/docs/unaliased-plain-import.md b/crates/lint/docs/unaliased-plain-import.md new file mode 100644 index 0000000000000..be8c5120028d6 --- /dev/null +++ b/crates/lint/docs/unaliased-plain-import.md @@ -0,0 +1,34 @@ +# Unaliased plain import + +**Severity**: `Info` +**ID**: `unaliased-plain-import` + +Flags `import "path";` statements that pull in every top-level symbol from another file without +an alias. + +## What it does + +Reports plain imports of the form `import "path";`. Suggests using either named imports +(`import { A, B } from "path"`) or an aliased import (`import "path" as X`). + +## Why is this bad? + +Plain imports pollute the importing file's namespace and make the source of each symbol +non-obvious. Named or aliased imports make the dependency surface explicit and reduce the chance +of accidental name collisions. + +## Example + +### Bad + +```solidity +import "./Lib.sol"; +``` + +### Good + +```solidity +import { Foo, Bar } from "./Lib.sol"; +// or +import "./Lib.sol" as Lib; +``` diff --git a/crates/lint/docs/unchecked-call.md b/crates/lint/docs/unchecked-call.md new file mode 100644 index 0000000000000..9a0a4143a0e0e --- /dev/null +++ b/crates/lint/docs/unchecked-call.md @@ -0,0 +1,34 @@ +# Unchecked low-level call + +**Severity**: `High` +**ID**: `unchecked-call` + +Flags low-level calls (`call`, `delegatecall`, `staticcall`, `callcode`) whose `success` return +value is ignored. + +## What it does + +Warns when the boolean returned by a low-level call is discarded — either because the return value +is not assigned or because only the `bytes memory` payload is used. + +## Why is this bad? + +Low-level calls do **not** revert when the callee fails; they silently return `false`. Ignoring +the success flag means a failed call is indistinguishable from a successful one, leading to bugs +where state is updated on the assumption that an external interaction succeeded. + +## Example + +### Bad + +```solidity +target.call(data); // success ignored +(, bytes memory ret) = target.call(data); // only payload kept +``` + +### Good + +```solidity +(bool ok, ) = target.call(data); +require(ok, "call failed"); +``` diff --git a/crates/lint/docs/unsafe-cheatcode.md b/crates/lint/docs/unsafe-cheatcode.md new file mode 100644 index 0000000000000..0aef657b0b7be --- /dev/null +++ b/crates/lint/docs/unsafe-cheatcode.md @@ -0,0 +1,35 @@ +# Usage of unsafe cheatcodes + +**Severity**: `Info` +**ID**: `unsafe-cheatcode` + +Flags use of Foundry cheatcodes that perform dangerous side effects (filesystem access, network +activity, environment variable reads, etc.) so they cannot slip into production code unnoticed. + +## What it does + +Reports calls to cheatcodes whose effects extend beyond the EVM sandbox or that bypass typical +test invariants. The flagged set follows the cheatcode's +[`Safety::Unsafe`](https://book.getfoundry.sh/cheatcodes) classification. + +## Why is this bad? + +Unsafe cheatcodes can read/write files, hit the network, or fork external state. They are +appropriate in tests with explicit intent but should not be added without review, and must +never end up in shipped contract code. + +## Example + +### Bad + +```solidity +vm.writeFile("./out.txt", data); // unsafe — writes to host filesystem +vm.envString("PRIVATE_KEY"); // unsafe — reads host environment +``` + +### Good + +```solidity +// Use safe cheatcodes (vm.expectRevert, vm.prank, vm.warp, ...) and explicit +// inputs/fixtures instead of pulling state from the host environment. +``` diff --git a/crates/lint/docs/unsafe-typecast.md b/crates/lint/docs/unsafe-typecast.md new file mode 100644 index 0000000000000..89d493eec3c3f --- /dev/null +++ b/crates/lint/docs/unsafe-typecast.md @@ -0,0 +1,40 @@ +# Unsafe typecast + +**Severity**: `Med` +**ID**: `unsafe-typecast` + +Flags explicit numeric typecasts that can silently truncate or alter the value. + +## What it does + +Reports casts where the source value's type is wider than the target type +(e.g. `uint256 → uint128`, `int256 → uint128`), unless the cast is preceded by a check that +guarantees the value fits in the target. + +## Why is this bad? + +Solidity does **not** revert on narrowing casts; it silently keeps the lowest bits, which can +cause severe accounting bugs (e.g. amount overflows, wrong fees, broken invariants). Use a checked +cast helper such as OpenZeppelin's `SafeCast` whenever the source value is not provably bounded. + +## Example + +### Bad + +```solidity +function setAmount(uint256 amount) external { + smallAmount = uint128(amount); // silent truncation if amount >= 2**128 +} +``` + +### Good + +```solidity +function setAmount(uint256 amount) external { + require(amount <= type(uint128).max, "overflow"); + smallAmount = uint128(amount); +} + +// or +smallAmount = SafeCast.toUint128(amount); +``` diff --git a/crates/lint/docs/unused-import.md b/crates/lint/docs/unused-import.md new file mode 100644 index 0000000000000..08f2545a36587 --- /dev/null +++ b/crates/lint/docs/unused-import.md @@ -0,0 +1,40 @@ +# Unused import + +**Severity**: `Info` +**ID**: `unused-import` + +Flags imported symbols (or whole import statements) whose imported names are not referenced +anywhere in the source unit. + +## What it does + +Reports `import "..."`, `import "..." as X`, and `import { A, B } from "..."` statements where one +or more imported names are never used. Symbols brought in via `import * as X` are tracked through +`X.member` accesses. + +## Why is this bad? + +Unused imports add noise, slow down compilation, can cause name collisions, and frequently +indicate dead code or stale refactors. + +## Example + +### Bad + +```solidity +import { A, B } from "./Lib.sol"; // B is never used + +contract C { + A internal a; +} +``` + +### Good + +```solidity +import { A } from "./Lib.sol"; + +contract C { + A internal a; +} +``` diff --git a/crates/lint/docs/unused-state-variables.md b/crates/lint/docs/unused-state-variables.md new file mode 100644 index 0000000000000..758c6e58b911b --- /dev/null +++ b/crates/lint/docs/unused-state-variables.md @@ -0,0 +1,39 @@ +# Unused state variable + +**Severity**: `Gas` +**ID**: `unused-state-variables` + +Flags state variables that are declared but never read or written anywhere in the contract or its +descendants. + +## What it does + +Reports each state variable that has no read or write site across the project. + +## Why is this bad? + +Unused state variables waste storage slots, inflate deployment cost, and are a strong signal of +dead or stale code that should be removed. + +## Example + +### Bad + +```solidity +contract C { + uint256 unused; // never read or written + uint256 public total; // used elsewhere +} +``` + +### Good + +```solidity +contract C { + uint256 public total; +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/unwrapped-modifier-logic.md b/crates/lint/docs/unwrapped-modifier-logic.md new file mode 100644 index 0000000000000..985c79962af07 --- /dev/null +++ b/crates/lint/docs/unwrapped-modifier-logic.md @@ -0,0 +1,51 @@ +# Unwrapped modifier logic + +**Severity**: `CodeSize` +**ID**: `unwrapped-modifier-logic` + +Flags modifiers whose body contains non-trivial logic that should be moved into a helper function +to reduce contract code size. + +## What it does + +Reports modifiers whose body contains statements other than a single placeholder, simple builtin +calls (`require`/`assert`), or a single library function call. Modifiers that use inline assembly +are exempted. + +## Why is this bad? + +Solidity inlines a modifier's body at every call site, so any non-trivial logic is duplicated +across all functions that use the modifier. Wrapping the logic in an internal function and calling +it from the modifier keeps the bytecode small while preserving behavior. + +## Example + +### Bad + +```solidity +modifier onlyAuth() { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; + _; +} +``` + +### Good + +```solidity +modifier onlyAuth() { + _checkAuth(); + _; +} + +function _checkAuth() internal { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; +} +``` + +## Notes + +This is a `CodeSize`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/src/linter/mod.rs b/crates/lint/src/linter/mod.rs index 0a5b4a40a5118..3e38a02726605 100644 --- a/crates/lint/src/linter/mod.rs +++ b/crates/lint/src/linter/mod.rs @@ -1,8 +1,10 @@ mod early; mod late; +mod project; pub use early::{EarlyLintPass, EarlyLintVisitor}; pub use late::{LateLintPass, LateLintVisitor}; +pub use project::{ProjectLintEmitter, ProjectLintPass, ProjectSource}; use foundry_common::comments::inline_config::InlineConfig; use foundry_compilers::Language; diff --git a/crates/lint/src/linter/project.rs b/crates/lint/src/linter/project.rs new file mode 100644 index 0000000000000..38fc1ad1ba59f --- /dev/null +++ b/crates/lint/src/linter/project.rs @@ -0,0 +1,92 @@ +use super::{Lint, LintContext, LinterConfig}; +use foundry_common::comments::inline_config::InlineConfig; +use foundry_config::lint::LintSpecificConfig; +use solar::{ + ast, + interface::{Session, Span, diagnostics::DiagMsg, source_map::SourceFile}, +}; +use std::{path::PathBuf, sync::Arc}; + +/// A single source unit visible to a project-wide lint pass, pre-loaded with its inline config so +/// emits respect `// forge-lint: disable-*` markers without rebuilding it per emit. +pub struct ProjectSource<'ast> { + pub path: PathBuf, + pub file: Arc, + pub ast: &'ast ast::SourceUnit<'ast>, + pub inline_config: InlineConfig>, +} + +/// Trait for lints that need to inspect every input source at once (e.g. cross-file checks). +/// +/// `check_project` runs once after all per-file [`super::EarlyLintPass`] / +/// [`super::LateLintPass`] passes have completed. +pub trait ProjectLintPass<'ast>: Send + Sync { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]); +} + +/// Helper passed to [`ProjectLintPass::check_project`] for emitting diagnostics against a specific +/// source. +pub struct ProjectLintEmitter<'s, 'c> { + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, +} + +impl<'s, 'c> ProjectLintEmitter<'s, 'c> { + pub const fn new( + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, + ) -> Self { + Self { sess, with_description, with_json_emitter, lint_specific, active_lints } + } + + /// Returns `true` if the given lint id is enabled for this run. Project passes that perform + /// expensive analysis should guard their work behind this check. + pub fn is_lint_enabled(&self, id: &'static str) -> bool { + self.active_lints.contains(&id) + } + + /// Emits a diagnostic with the lint's default description as the message. + pub fn emit<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + ) where + 'c: 'a, + { + self.build_ctx(source).emit(lint, span); + } + + /// Emits a diagnostic with a caller-provided message. + pub fn emit_with_msg<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + msg: impl Into, + ) where + 'c: 'a, + { + self.build_ctx(source).emit_with_msg(lint, span, msg); + } + + fn build_ctx<'a, 'ast>(&'a self, source: &'a ProjectSource<'ast>) -> LintContext<'s, 'a> + where + 'c: 'a, + { + LintContext::new( + self.sess, + self.with_description, + self.with_json_emitter, + LinterConfig { inline: &source.inline_config, lint_specific: self.lint_specific }, + self.active_lints.clone(), + Some(source.file.clone()), + ) + } +} diff --git a/crates/lint/src/sol/info/inline_assembly.rs b/crates/lint/src/sol/info/inline_assembly.rs new file mode 100644 index 0000000000000..1111129dada34 --- /dev/null +++ b/crates/lint/src/sol/info/inline_assembly.rs @@ -0,0 +1,71 @@ +use super::InlineAssembly; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Stmt, StmtKind}, + interface::{BytePos, Span}, +}; + +declare_forge_lint!( + INLINE_ASSEMBLY, + Severity::Info, + "inline-assembly", + "usage of inline assembly; assembly bypasses Solidity safety features and should be reviewed" +); + +const ASSEMBLY_KW_LEN: u32 = 8; +const NATSPEC_MEMORY_SAFE_MARKER: &str = "@solidity memory-safe-assembly"; + +impl<'ast> EarlyLintPass<'ast> for InlineAssembly { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + let StmtKind::Assembly(asm) = &stmt.kind else { return }; + + let kw_span = assembly_keyword_span(stmt.span); + + let memory_safe = asm.flags.iter().any(|f| f.value.as_str() == "memory-safe") + || has_memory_safe_natspec(ctx, stmt.span.lo()); + + let msg = if memory_safe { + "inline assembly (declared memory-safe); review business logic and side effects" + } else { + "inline assembly used; review for memory safety and side effects" + }; + + ctx.emit_with_msg(&INLINE_ASSEMBLY, kw_span, msg); + } +} + +/// Narrows a span to the leading `assembly` keyword to keep diagnostics readable. +fn assembly_keyword_span(span: Span) -> Span { + span.with_hi(span.lo() + BytePos(ASSEMBLY_KW_LEN)) +} + +/// Returns `true` when the lines immediately preceding `stmt_lo` form a `///` NatSpec block +/// containing `@solidity memory-safe-assembly`. +fn has_memory_safe_natspec(ctx: &LintContext, stmt_lo: BytePos) -> bool { + let Some(source_file) = ctx.source_file() else { return false }; + let src = source_file.src.as_str(); + let start_pos = source_file.start_pos.to_u32(); + let lo_abs = stmt_lo.to_u32(); + if lo_abs < start_pos { + return false; + } + let offset = (lo_abs - start_pos) as usize; + if offset > src.len() { + return false; + } + + for line in src[..offset].lines().rev() { + let trimmed = line.trim_start(); + if trimmed.is_empty() { + continue; + } + let Some(rest) = trimmed.strip_prefix("///") else { return false }; + if rest.trim_start().starts_with(NATSPEC_MEMORY_SAFE_MARKER) { + return true; + } + } + false +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index c7800a417bafc..913c5d2ea9da3 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -30,6 +30,15 @@ use multi_contract_file::MULTI_CONTRACT_FILE; mod interface_naming; use interface_naming::{INTERFACE_FILE_NAMING, INTERFACE_NAMING}; +mod too_many_digits; +use too_many_digits::TOO_MANY_DIGITS; + +mod pragma_directive; +use pragma_directive::PRAGMA_INCONSISTENT; + +mod inline_assembly; +use inline_assembly::INLINE_ASSEMBLY; + register_lints!( (BooleanCst, early, (BOOLEAN_CST)), (BooleanEqual, early, (BOOLEAN_EQUAL)), @@ -42,4 +51,7 @@ register_lints!( (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)), (MultiContractFile, early, (MULTI_CONTRACT_FILE)), (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)), + (TooManyDigits, early, (TOO_MANY_DIGITS)), + (PragmaDirective, project, (PRAGMA_INCONSISTENT)), + (InlineAssembly, early, (INLINE_ASSEMBLY)), ); diff --git a/crates/lint/src/sol/info/pragma_directive.rs b/crates/lint/src/sol/info/pragma_directive.rs new file mode 100644 index 0000000000000..b66b6bcff6ade --- /dev/null +++ b/crates/lint/src/sol/info/pragma_directive.rs @@ -0,0 +1,71 @@ +use crate::{ + linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource}, + sol::{Severity, SolLint, info::PragmaDirective}, +}; +use solar::{ast, interface::Span}; + +declare_forge_lint!( + PRAGMA_INCONSISTENT, + Severity::Info, + "pragma-inconsistent", + "inconsistent Solidity pragma version requirements across the project" +); + +impl<'ast> ProjectLintPass<'ast> for PragmaDirective { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) { + if !ctx.is_lint_enabled(PRAGMA_INCONSISTENT.id()) { + return; + } + + // Collect every `pragma solidity` directive across input sources, with its rendered + // version-requirement string for grouping. Stores source index to avoid lifetime + // invariance issues with `&ProjectSource<'ast>`. + let mut entries: Vec<(usize, Span, String)> = Vec::new(); + for (idx, source) in sources.iter().enumerate() { + for (span, req) in solidity_pragmas(source.ast) { + entries.push((idx, span, req.to_string())); + } + } + + // Stable order for snapshots and JSON output. + entries.sort_by(|a, b| { + sources[a.0].path.cmp(&sources[b.0].path).then(a.1.lo().cmp(&b.1.lo())) + }); + + // Build the distinct list once and bail if all sources agree. + let mut distinct: Vec<&str> = entries.iter().map(|(_, _, s)| s.as_str()).collect(); + distinct.sort_unstable(); + distinct.dedup(); + if distinct.len() < 2 { + return; + } + + for (idx, span, req_str) in &entries { + let others = distinct + .iter() + .filter(|v| **v != req_str.as_str()) + .copied() + .collect::>() + .join(", "); + let msg = format!( + "'pragma solidity {req_str};' conflicts with other version requirements in the project: {others}" + ); + ctx.emit_with_msg(&sources[*idx], &PRAGMA_INCONSISTENT, *span, msg); + } + } +} + +/// Yields every top-level `pragma solidity ...;` directive in `unit`. +fn solidity_pragmas<'ast>( + unit: &'ast ast::SourceUnit<'ast>, +) -> impl Iterator)> + 'ast { + unit.items.iter().filter_map(|item| match &item.kind { + ast::ItemKind::Pragma(p) => match &p.tokens { + ast::PragmaTokens::Version(ident, req) if ident.as_str() == "solidity" => { + Some((item.span, req)) + } + _ => None, + }, + _ => None, + }) +} diff --git a/crates/lint/src/sol/info/too_many_digits.rs b/crates/lint/src/sol/info/too_many_digits.rs new file mode 100644 index 0000000000000..3ba9e8abba2de --- /dev/null +++ b/crates/lint/src/sol/info/too_many_digits.rs @@ -0,0 +1,50 @@ +use super::TooManyDigits; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::ast::{Expr, ExprKind, LitKind}; + +declare_forge_lint!( + TOO_MANY_DIGITS, + Severity::Info, + "too-many-digits", + "numeric literal with many digits is error-prone; \ + use scientific notation, sub-denominations, or underscore separators" +); + +impl<'ast> EarlyLintPass<'ast> for TooManyDigits { + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + let ExprKind::Lit(lit, sub_denom) = &expr.kind else { return }; + + // Only plain integer literals. `LitKind::Address` (40-hex-digit address) is a + // distinct variant and is therefore skipped automatically. + if !matches!(lit.kind, LitKind::Number(_)) { + return; + } + + // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`. + if sub_denom.is_some() { + return; + } + + let s = lit.symbol.as_str(); + + // Skip hex literals — long zero runs in hex are usually intentional (masks, + // selectors, bit patterns) and there is no scientific-notation alternative. + if s.starts_with("0x") || s.starts_with("0X") { + return; + } + + // Skip if the user already used scientific notation (`1e18`). + if s.contains('e') || s.contains('E') { + return; + } + + // 5+ consecutive zeros in the literal as written. Underscores are + // preserved, so `1_000_000` passes while `1_000000` is flagged. + if s.contains("00000") { + ctx.emit(&TOO_MANY_DIGITS, lit.span); + } + } +} diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 00d764770374a..8540ab8b95b8f 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -9,9 +9,11 @@ /// - `$desc`: A short description of the lint. /// /// # Note -/// Each lint must have a `help` section in the foundry book. This help field is auto-generated by -/// the macro. Because of that, to ensure that new lint rules have their corresponding docs in the -/// book, the existence of the lint rule's help section is validated with a unit test. +/// Each lint must have a corresponding markdown documentation file at +/// `crates/lint/docs/.md`. The `help` URL is auto-generated by the macro and points to +/// the per-lint page on the Foundry docs site (`getfoundry.sh/forge/linting/`). To +/// ensure that new lint rules have their corresponding docs, the existence of every registered +/// lint's markdown file is validated by a unit test (see `crates/lint/src/sol/mod.rs`). #[macro_export] macro_rules! declare_forge_lint { ($id:ident, $severity:expr, $str_id:expr, $desc:expr) => { @@ -20,7 +22,7 @@ macro_rules! declare_forge_lint { id: $str_id, severity: $severity, description: $desc, - help: concat!("https://book.getfoundry.sh/reference/forge/forge-lint#", $str_id), + help: concat!("https://getfoundry.sh/forge/linting/", $str_id), }; }; } @@ -53,6 +55,7 @@ macro_rules! register_lints { register_lints!(@early_impl $pass_id, $pass_type); register_lints!(@late_impl $pass_id, $pass_type); + register_lints!(@project_impl $pass_id, $pass_type); } )* }; @@ -89,10 +92,22 @@ macro_rules! register_lints { .flatten() .collect() } + + pub fn create_project_lint_passes<'ast>() -> Vec<(Box>, &'static [SolLint])> { + [ + $( + register_lints!(@project_create $pass_id, $pass_type), + )* + ] + .into_iter() + .flatten() + .collect() + } }; // --- HELPERS ------------------------------------------------------------ (@early_impl $_pass_id:ident, late) => {}; + (@early_impl $_pass_id:ident, project) => {}; (@early_impl $pass_id:ident, $other:ident) => { pub fn as_early_lint_pass<'a>() -> Box> { Box::new(Self::default()) @@ -100,22 +115,41 @@ macro_rules! register_lints { }; (@late_impl $_pass_id:ident, early) => {}; + (@late_impl $_pass_id:ident, project) => {}; (@late_impl $pass_id:ident, $other:ident) => { pub fn as_late_lint_pass<'hir>() -> Box> { Box::new(Self::default()) } }; + (@project_impl $_pass_id:ident, early) => {}; + (@project_impl $_pass_id:ident, late) => {}; + (@project_impl $_pass_id:ident, both) => {}; + (@project_impl $pass_id:ident, $other:ident) => { + pub fn as_project_lint_pass<'ast>() -> Box> { + Box::new(Self::default()) + } + }; + (@early_create $_pass_id:ident, late) => { None }; + (@early_create $_pass_id:ident, project) => { None }; (@early_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_early_lint_pass(), $pass_id::LINTS)) }; (@late_create $_pass_id:ident, early) => { None }; + (@late_create $_pass_id:ident, project) => { None }; (@late_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_late_lint_pass(), $pass_id::LINTS)) }; + (@project_create $_pass_id:ident, early) => { None }; + (@project_create $_pass_id:ident, late) => { None }; + (@project_create $_pass_id:ident, both) => { None }; + (@project_create $pass_id:ident, $_other:ident) => { + Some(($pass_id::as_project_lint_pass(), $pass_id::LINTS)) + }; + // --- ENTRY POINT --------------------------------------------------------- ( $($tokens:tt)* ) => { register_lints! { @declare_structs $($tokens)* } diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs index ba7a09b0e9bac..2673ba23d3252 100644 --- a/crates/lint/src/sol/med/mod.rs +++ b/crates/lint/src/sol/med/mod.rs @@ -9,6 +9,9 @@ use incorrect_erc20_interface::INCORRECT_ERC20_INTERFACE; mod incorrect_erc721_interface; use incorrect_erc721_interface::INCORRECT_ERC721_INTERFACE; +mod tx_origin; +use tx_origin::TX_ORIGIN; + mod unsafe_typecast; use unsafe_typecast::UNSAFE_TYPECAST; @@ -16,5 +19,6 @@ register_lints!( (DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)), (IncorrectERC20Interface, late, (INCORRECT_ERC20_INTERFACE)), (IncorrectERC721Interface, late, (INCORRECT_ERC721_INTERFACE)), + (TxOrigin, early, (TX_ORIGIN)), (UnsafeTypecast, late, (UNSAFE_TYPECAST)) ); diff --git a/crates/lint/src/sol/med/tx_origin.rs b/crates/lint/src/sol/med/tx_origin.rs new file mode 100644 index 0000000000000..00ff5f939ebfb --- /dev/null +++ b/crates/lint/src/sol/med/tx_origin.rs @@ -0,0 +1,101 @@ +use super::TxOrigin; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Expr, ExprKind, IndexKind, Stmt, StmtKind}, + interface::SpannedOption, +}; + +declare_forge_lint!( + TX_ORIGIN, + Severity::Med, + "tx-origin", + "`tx.origin` should not be used for authorization" +); + +impl<'ast> EarlyLintPass<'ast> for TxOrigin { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + match &stmt.kind { + StmtKind::If(cond, ..) | StmtKind::DoWhile(_, cond) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::While(cond, _) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::For { cond: Some(cond), .. } => { + emit_if_contains_tx_origin(ctx, cond); + } + _ => {} + } + } + + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + if let ExprKind::Call(callee, args) = &expr.kind + && is_require_or_assert_call(callee) + && let Some(cond) = args.exprs().next() + { + emit_if_contains_tx_origin(ctx, cond); + } + } +} + +fn emit_if_contains_tx_origin(ctx: &LintContext, expr: &Expr<'_>) { + if contains_tx_origin(expr) { + ctx.emit(&TX_ORIGIN, expr.span); + } +} + +fn contains_tx_origin(expr: &Expr<'_>) -> bool { + if is_tx_origin(expr) { + return true; + } + match &expr.kind { + ExprKind::Unary(_, inner) => contains_tx_origin(inner), + ExprKind::Binary(lhs, _, rhs) => contains_tx_origin(lhs) || contains_tx_origin(rhs), + ExprKind::Index(base, index) => { + contains_tx_origin(base) + || match index { + IndexKind::Index(Some(index)) => contains_tx_origin(index), + IndexKind::Range(start, end) => { + start.as_ref().is_some_and(|start| contains_tx_origin(start)) + || end.as_ref().is_some_and(|end| contains_tx_origin(end)) + } + _ => false, + } + } + ExprKind::Tuple(elems) => elems.iter().any(|elem| { + if let SpannedOption::Some(inner) = elem.as_ref() { + contains_tx_origin(inner) + } else { + false + } + }), + ExprKind::Call(callee, args) => { + contains_tx_origin(callee) || args.exprs().any(contains_tx_origin) + } + ExprKind::Ternary(cond, then_expr, else_expr) => { + contains_tx_origin(cond) + || contains_tx_origin(then_expr) + || contains_tx_origin(else_expr) + } + _ => false, + } +} + +fn is_tx_origin(expr: &Expr<'_>) -> bool { + matches!( + &expr.kind, + ExprKind::Member(base, member) + if member.as_str() == "origin" + && matches!(&base.kind, ExprKind::Ident(ident) if ident.as_str() == "tx") + ) +} + +fn is_require_or_assert_call(callee: &Expr<'_>) -> bool { + matches!( + &callee.kind, + ExprKind::Ident(ident) if matches!(ident.as_str(), "require" | "assert") + ) +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 1f7c515585fb4..7ae073f2ea20b 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,6 +1,6 @@ use crate::linter::{ EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter, - LinterConfig, + LinterConfig, ProjectLintEmitter, ProjectLintPass, ProjectSource, }; use foundry_common::{ comments::{ @@ -179,6 +179,62 @@ impl<'a> SolidityLinter<'a> { Ok(()) } + /// Runs all enabled project-wide lint passes against the given input sources. + fn process_project<'gcx>(&self, gcx: Gcx<'gcx>, input: &[PathBuf]) { + // Gather enabled project passes from every severity bucket. + let mut passes_and_lints: Vec<(Box>, &'static [SolLint])> = + Vec::new(); + passes_and_lints.extend(high::create_project_lint_passes()); + passes_and_lints.extend(med::create_project_lint_passes()); + passes_and_lints.extend(low::create_project_lint_passes()); + passes_and_lints.extend(info::create_project_lint_passes()); + passes_and_lints.extend(gas::create_project_lint_passes()); + passes_and_lints.extend(codesize::create_project_lint_passes()); + + let (mut passes, lint_ids): (Vec>>, Vec<_>) = passes_and_lints + .into_iter() + .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| { + let included: Vec<_> = lints + .iter() + .filter_map(|lint| self.include_lint(*lint).then_some(lint.id)) + .collect(); + if !included.is_empty() { + passes.push(pass); + ids.extend(included); + } + (passes, ids) + }); + + if passes.is_empty() { + return; + } + + // Pre-load every input source with its inline config, in input order. + let sources: Vec> = input + .iter() + .filter_map(|path| { + let path = self.path_config.root.join(path); + let (_, source) = gcx.get_ast_source(&path)?; + let ast = source.ast.as_ref()?; + let comments = + Comments::new(&source.file, gcx.sess.source_map(), false, false, None); + let inline_config = parse_inline_config(gcx.sess, &comments, ast); + Some(ProjectSource { path, file: source.file.clone(), ast, inline_config }) + }) + .collect(); + + let emitter = ProjectLintEmitter::new( + gcx.sess, + self.with_description, + self.with_json_emitter, + self.lint_specific, + lint_ids, + ); + for pass in &mut passes { + pass.check_project(&emitter, &sources); + } + } + fn process_source_hir<'gcx>( &self, gcx: Gcx<'gcx>, @@ -314,6 +370,9 @@ impl<'a> Linter for SolidityLinter<'a> { ); }); + // Project-wide lints, run once after all per-file passes. + self.process_project(gcx, input); + convert_solar_errors(compiler.dcx()) })?; @@ -453,3 +512,75 @@ impl<'a> TryFrom<&'a str> for SolLint { Err(SolLintError::InvalidId(value.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every registered lint must have a markdown documentation file at + /// `crates/lint/docs/.md`. This test enforces that contract so that the `help` URL + /// generated by `declare_forge_lint!` always resolves to real documentation. + /// + /// When this test fails, add a new file at `crates/lint/docs/.md` describing the + /// lint. See [`crates/lint/docs/_template.md`](../../docs/_template.md) for the expected + /// structure. + #[test] + fn registered_lints_have_docs() { + let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs"); + assert!(docs_dir.is_dir(), "missing docs directory at {}", docs_dir.display()); + + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + let mut missing: Vec<&'static str> = Vec::new(); + let mut empty: Vec<&'static str> = Vec::new(); + for lint in &all_lints { + let path = docs_dir.join(format!("{}.md", lint.id())); + match std::fs::read_to_string(&path) { + Ok(content) => { + // Basic sanity: file should be non-trivial and reference the lint id. + if content.trim().is_empty() || !content.contains(lint.id()) { + empty.push(lint.id()); + } + } + Err(_) => missing.push(lint.id()), + } + } + + assert!( + missing.is_empty(), + "the following registered lints are missing a docs file at \ + `crates/lint/docs/.md`: {missing:?}\n\ + See `crates/lint/docs/_template.md` for the expected structure." + ); + assert!( + empty.is_empty(), + "the following lint docs files are empty or do not reference the lint id: {empty:?}" + ); + } + + /// The auto-generated `help` URL must point at the canonical Foundry docs site so that the + /// link printed in diagnostics resolves correctly. + #[test] + fn registered_lints_have_canonical_help_url() { + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + for lint in all_lints { + let expected = format!("https://getfoundry.sh/forge/linting/{}", lint.id()); + assert_eq!(lint.help(), expected, "lint `{}` has a non-canonical help URL", lint.id()); + } + } +} diff --git a/crates/lint/testdata/BlockTimestamp.stderr b/crates/lint/testdata/BlockTimestamp.stderr index 016f8fa2bdb2d..62ab588ae7340 100644 --- a/crates/lint/testdata/BlockTimestamp.stderr +++ b/crates/lint/testdata/BlockTimestamp.stderr @@ -4,7 +4,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -12,7 +12,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp == 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -20,7 +20,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp != 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -28,7 +28,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp <= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -36,7 +36,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp >= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -44,7 +44,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp < deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -52,7 +52,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return deadline > block.timestamp; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -60,7 +60,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp + 1 > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -68,7 +68,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return (block.timestamp / 3600) == 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -76,7 +76,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ require(block.timestamp > deadline); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -84,7 +84,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ if (block.timestamp > deadline) { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -92,5 +92,5 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return foo(block.timestamp) > 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp diff --git a/crates/lint/testdata/BooleanCst.stderr b/crates/lint/testdata/BooleanCst.stderr index 53b89fcb11735..75fdb0b57cea7 100644 --- a/crates/lint/testdata/BooleanCst.stderr +++ b/crates/lint/testdata/BooleanCst.stderr @@ -4,7 +4,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (false) {} │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -12,7 +12,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag || true) {} │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -20,7 +20,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag ? true : false) {} │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -28,7 +28,7 @@ warning[boolean-cst]: misuse of a boolean constant LL │ if (flag ? true : false) {} │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst warning[boolean-cst]: misuse of a boolean constant ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC @@ -36,5 +36,5 @@ warning[boolean-cst]: misuse of a boolean constant LL │ return assigned && false; │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-cst + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst diff --git a/crates/lint/testdata/BooleanEqual.stderr b/crates/lint/testdata/BooleanEqual.stderr index 11749698f5714..590a85b806fcf 100644 --- a/crates/lint/testdata/BooleanEqual.stderr +++ b/crates/lint/testdata/BooleanEqual.stderr @@ -4,7 +4,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (enabled == true) {} │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -12,7 +12,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (paused == false) {} │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `!paused` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -20,7 +20,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ if (true != ready) {} │ ━━━━━━━━━━━━━ help: consider simplifying to: `!ready` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -28,7 +28,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ while (done != false) { │ ━━━━━━━━━━━━━ help: consider simplifying to: `done` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -36,7 +36,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ for (; enabled == true && paused != false;) { │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -44,7 +44,7 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ for (; enabled == true && paused != false;) { │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `paused` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal note[boolean-equal]: boolean comparisons to constants should be simplified ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC @@ -52,5 +52,5 @@ note[boolean-equal]: boolean comparisons to constants should be simplified LL │ return enabled == true; │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#boolean-equal + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal diff --git a/crates/lint/testdata/CouldBeImmutable.stderr b/crates/lint/testdata/CouldBeImmutable.stderr index 2858b2311cd95..170682baf89d3 100644 --- a/crates/lint/testdata/CouldBeImmutable.stderr +++ b/crates/lint/testdata/CouldBeImmutable.stderr @@ -4,7 +4,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address public owner; │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -12,7 +12,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address public deployer = msg.sender; │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -20,7 +20,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 private configured; │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -28,7 +28,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ bytes32 internal salt = keccak256(abi.encodePacked(block.timestamp)); │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -36,7 +36,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ CouldBeImmutable private peer; │ ━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -44,7 +44,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 internal inheritedBase; │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[could-be-immutable]: state variable could be declared immutable ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC @@ -52,5 +52,5 @@ note[could-be-immutable]: state variable could be declared immutable LL │ uint256 internal baseConfigured; │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable diff --git a/crates/lint/testdata/CustomErrors.stderr b/crates/lint/testdata/CustomErrors.stderr index 66b3c11bc183c..286a649aee269 100644 --- a/crates/lint/testdata/CustomErrors.stderr +++ b/crates/lint/testdata/CustomErrors.stderr @@ -4,7 +4,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ require(a > 0, "Value must be greater than zero"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -12,7 +12,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ … require(a >= 0 && a <= 100 || b == 50, "Complex condition should be linted"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -20,7 +20,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert("Something went wrong"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -28,7 +28,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(""); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -36,5 +36,5 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors diff --git a/crates/lint/testdata/DivideBeforeMultiply.stderr b/crates/lint/testdata/DivideBeforeMultiply.stderr index c0e5ef78e2e1c..95022f65db874 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.stderr +++ b/crates/lint/testdata/DivideBeforeMultiply.stderr @@ -4,7 +4,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -12,7 +12,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 / 2) * 3) * 4; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -20,7 +20,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 * 2) / 3) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -28,7 +28,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2 / 3) * 4; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -36,7 +36,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / (2 + 3)) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -44,5 +44,5 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ 1 / ((2 / 3) * 3); │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply diff --git a/crates/lint/testdata/Imports.stderr b/crates/lint/testdata/Imports.stderr index 8fa9800b27ded..1031f4f6f8ca0 100644 --- a/crates/lint/testdata/Imports.stderr +++ b/crates/lint/testdata/Imports.stderr @@ -4,7 +4,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsSomeFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." as X' ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -12,7 +12,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsAnotherFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -20,7 +20,7 @@ note[unused-import]: unused imports should be removed LL │ symbol2 as notUsed, │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -28,7 +28,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol, │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -36,7 +36,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol2, │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -44,7 +44,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbolWrongTag, │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -52,7 +52,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed, │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -60,7 +60,7 @@ note[unused-import]: unused imports should be removed LL │ IContractNotUsed │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -68,7 +68,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed3 │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -76,7 +76,7 @@ note[unused-import]: unused imports should be removed LL │ CONSTANT_1 │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -84,7 +84,7 @@ note[unused-import]: unused imports should be removed LL │ YetAnotherType │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -92,7 +92,7 @@ note[unused-import]: unused imports should be removed LL │ import "./auxiliary/ImportsAnotherFile2.sol" as AnotherFile2; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -100,5 +100,5 @@ note[unused-import]: unused imports should be removed LL │ import * as OtherUtils from "./auxiliary/ImportsUtils2.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import diff --git a/crates/lint/testdata/IncorrectERC20Interface.stderr b/crates/lint/testdata/IncorrectERC20Interface.stderr index 3bb60ecce8320..33e2f1ca27d22 100644 --- a/crates/lint/testdata/IncorrectERC20Interface.stderr +++ b/crates/lint/testdata/IncorrectERC20Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 {} │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Incorrect is IERC20 { │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Correct is IERC20 { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20NamedCorrect { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC20 { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transferFrom(address from, address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function allowance(address owner, address spender) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function balanceOf(address account) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -116,5 +116,5 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function totalSupply() external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface diff --git a/crates/lint/testdata/IncorrectERC721Interface.stderr b/crates/lint/testdata/IncorrectERC721Interface.stderr index a88db93e39b10..2e68084c1cec1 100644 --- a/crates/lint/testdata/IncorrectERC721Interface.stderr +++ b/crates/lint/testdata/IncorrectERC721Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721 {} │ ━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Incorrect is IERC721 { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Correct is IERC721 { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721NamedCorrect { │ ━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC721 { │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function transferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -116,7 +116,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function approve(address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -124,7 +124,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function setApprovalForAll(address operator, bool approved) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -132,7 +132,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function getApproved(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -140,7 +140,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function isApprovedForAll(address owner, address operator) external view returns (address); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -148,5 +148,5 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function supportsInterface(bytes4 interfaceId) external view returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface diff --git a/crates/lint/testdata/IncorrectShift.stderr b/crates/lint/testdata/IncorrectShift.stderr index bce84c98df432..dfff32db897bb 100644 --- a/crates/lint/testdata/IncorrectShift.stderr +++ b/crates/lint/testdata/IncorrectShift.stderr @@ -4,7 +4,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 2 << stateValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -12,7 +12,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -20,7 +20,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 16 << (stateValue + 1); │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -28,7 +28,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 32 >> getAmount(); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -36,5 +36,5 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ … result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift diff --git a/crates/lint/testdata/InlineAssembly.sol b/crates/lint/testdata/InlineAssembly.sol new file mode 100644 index 0000000000000..05917ea22784c --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.sol @@ -0,0 +1,110 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract InlineAssembly { + function bare() public view returns (uint256 id) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + id := chainid() + } + } + + function withMemorySafe() public view returns (uint256 size) { + assembly ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + size := extcodesize(address()) + } + } + + function withDialectAndMemorySafe() public view returns (uint256 ptr) { + assembly "evmasm" ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + ptr := mload(0x40) + } + } + + function withNatspecMemorySafe() public view returns (uint256 v) { + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := chainid() + } + } + + function withNatspecMemorySafeAndOtherDocs() public view returns (uint256 v) { + /// @notice does a thing + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := gas() + } + } + + function plainCommentDoesNotCount() public view returns (uint256 v) { + // solidity memory-safe-assembly + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := chainid() + } + } + + function nestedInControlFlow(bool flag) public view returns (uint256 v) { + if (flag) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := gas() + } + } + + for (uint256 i = 0; i < 1; ++i) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInUnchecked(uint256 x) public pure returns (uint256 v) { + unchecked { + v = x + 1; + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInTryCatch() public returns (uint256 v) { + try this.bare() returns (uint256) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 1 + } + } catch { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 2 + } + } + } + + function suppressed() public view returns (uint256 id) { + // forge-lint: disable-next-line(inline-assembly) + assembly { + id := chainid() + } + } + + modifier guarded() { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + if iszero(caller()) { revert(0, 0) } + } + _; + } + + function suppressedRegion() public view returns (uint256 a, uint256 b) { + // forge-lint: disable-start(inline-assembly) + assembly { + a := chainid() + } + assembly ("memory-safe") { + b := gas() + } + // forge-lint: disable-end(inline-assembly) + } + + function noAssembly() public pure returns (uint256) { + return 42; + } +} diff --git a/crates/lint/testdata/InlineAssembly.stderr b/crates/lint/testdata/InlineAssembly.stderr new file mode 100644 index 0000000000000..12f8bcbacd14e --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.stderr @@ -0,0 +1,96 @@ +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly "evmasm" ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index 41f856336b5b3..2457aed96d601 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -52,6 +52,7 @@ contract AsmKeccak256 { function assemblyHash(uint256 a, uint256 b) public pure returns (bytes32) { //optimized + // forge-lint: disable-next-line(inline-assembly) assembly { mstore(0x00, a) mstore(0x20, b) diff --git a/crates/lint/testdata/Keccak256.stderr b/crates/lint/testdata/Keccak256.stderr index 4203d950d9cba..a81e429e389a1 100644 --- a/crates/lint/testdata/Keccak256.stderr +++ b/crates/lint/testdata/Keccak256.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `mixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Another_MixedCase = 2; │ ━━━━━━━━━━━━━━━━━ help: consider using: `anotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 YetAnother_MixedCase = 3; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `yetAnotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract AsmKeccak256 { │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract OtherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -60,7 +60,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract YetAnotherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -68,7 +68,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 hash = keccak256(abi.encodePacked(a, b, bytes32(bytes20(c)))); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -76,7 +76,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 afterDisabledBlock = keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -84,7 +84,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromCalldata = keccak256(z); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -92,7 +92,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromMemory = keccak256(y); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -100,7 +100,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 lintWithoutFix = keccak256(abi.encodePacked(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -108,7 +108,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -116,7 +116,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 Enabled_MixedCase_Variable; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -124,7 +124,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesNotUseScratchSpace = keccak256(abi.encode(x, y, x, y, x, y)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -132,7 +132,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesUseScratchSpace = keccak256(abi.encode(x)); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -140,5 +140,5 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(doesUseScratchSpace, doesNotUseScratchSpace)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 diff --git a/crates/lint/testdata/MissingZeroCheck.stderr b/crates/lint/testdata/MissingZeroCheck.stderr index b55a902547fcf..81a9179e79c94 100644 --- a/crates/lint/testdata/MissingZeroCheck.stderr +++ b/crates/lint/testdata/MissingZeroCheck.stderr @@ -4,7 +4,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwner(address newOwner) external { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -12,7 +12,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ constructor(address initialOwner) { │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -20,7 +20,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function pay(address payable to) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -28,7 +28,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function lowLevel(address payable to, bytes calldata data) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -36,7 +36,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function withUselessModifier(address a) external doesNothing(a) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -44,7 +44,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaAlias(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -52,7 +52,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaReassign(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -60,7 +60,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaCast(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -68,7 +68,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payViaAlias(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -76,7 +76,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function mixedParams(address a, address b) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -84,7 +84,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function bothSinks(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -92,7 +92,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function ternaryAlias(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -100,7 +100,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payableWrap(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -108,7 +108,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function modifierWithExpr(address a) external nonZero(addrIdentity(a)) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -116,7 +116,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function delegateCallSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -124,7 +124,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkStmt(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -132,7 +132,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkDecl(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -140,7 +140,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function multiHopTaint(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -148,7 +148,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardAfterSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -156,7 +156,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardOnOneBranch(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -164,7 +164,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInForLoop(address a, uint256 n) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -172,7 +172,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInWhileLoop(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -180,5 +180,5 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInTryClause(address a, address payable target) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check diff --git a/crates/lint/testdata/MixedCase.stderr b/crates/lint/testdata/MixedCase.stderr index d290af5cdb5a8..2db30559ba5a6 100644 --- a/crates/lint/testdata/MixedCase.stderr +++ b/crates/lint/testdata/MixedCase.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Variablemixedcase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variablemixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VARIABLE_MIXED_CASE; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VariableMixedCase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 testVAL; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TestVal; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -44,7 +44,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TESTVAL; │ ━━━━━━━ help: consider using: `testval` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -52,7 +52,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function Functionmixedcase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionmixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -60,7 +60,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FUNCTION_MIXED_CASE() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -68,7 +68,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FunctionMixedCase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -76,7 +76,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function function_mixed_case() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -84,7 +84,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantBalance_MixedCase_Enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantBalanceMixedCaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -92,7 +92,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantbalance_mixedcase_enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantbalanceMixedcaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -100,7 +100,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function ERC20_DoSomething() public {} // invalid because of the underscore │ ━━━━━━━━━━━━━━━━━ help: consider using: `erc20DoSomething` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -108,7 +108,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_PARAMS(address addr) external view returns (uint256) {} │ ━━━━━━━━━━ help: consider using: `hasParams` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -116,7 +116,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_NO_RETURN() external view {} │ ━━━━━━━━━━━━━ help: consider using: `hasNoReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -124,7 +124,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_MORE_THAN_ONE_RETURN() external view returns (uint256, uint256) {} │ ━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `hasMoreThanOneReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -132,7 +132,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function NOT_ELEMENTARY_RETURN() external view returns (uint256[] memory) {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `notElementaryReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -140,7 +140,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -148,5 +148,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract MixedCaseTest { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile.stderr b/crates/lint/testdata/MultiContractFile.stderr index c6e4e32a2df55..e25f3d72ad01a 100644 --- a/crates/lint/testdata/MultiContractFile.stderr +++ b/crates/lint/testdata/MultiContractFile.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract A {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract B {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -36,5 +36,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr index 41fc439ea7d1b..1912f16863712 100644 --- a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr +++ b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -20,5 +20,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/NamedStructFields.stderr b/crates/lint/testdata/NamedStructFields.stderr index 6ee2160791cd2..cfb35637176bd 100644 --- a/crates/lint/testdata/NamedStructFields.stderr +++ b/crates/lint/testdata/NamedStructFields.stderr @@ -4,5 +4,5 @@ note[named-struct-fields]: prefer initializing structs with named fields LL │ Person memory person = Person("Alice", 25, address(0)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using named fields: `Person({ name: "Alice", age: 25, wallet: address(0) })` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#named-struct-fields + ╰ help: https://getfoundry.sh/forge/linting/named-struct-fields diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol new file mode 100644 index 0000000000000..bfc993baab794 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 +pragma solidity 0.8.18; //~NOTE: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr new file mode 100644 index 0000000000000..c2c967dee792f --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.18; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol new file mode 100644 index 0000000000000..75bc17988accc --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr new file mode 100644 index 0000000000000..f60361718ba9b --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol new file mode 100644 index 0000000000000..37b06040c33a6 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 +pragma solidity ~0.8.20; //~NOTE: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr new file mode 100644 index 0000000000000..6c46f2478208d --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ~0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.sol b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol new file mode 100644 index 0000000000000..f85a477cc8744 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20 || 0.8.21; //~NOTE: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr new file mode 100644 index 0000000000000..acf6bd7c2d6e0 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20 || 0.8.21; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol new file mode 100644 index 0000000000000..d8fcb7a0eb4b1 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; //~NOTE: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr new file mode 100644 index 0000000000000..5ac221b924c9a --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0 <0.9.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol new file mode 100644 index 0000000000000..fe208e15efb63 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol @@ -0,0 +1,8 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; //~NOTE: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 +pragma solidity ~0.8.0; //~NOTE: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr new file mode 100644 index 0000000000000..e1e5ad7333fb2 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr @@ -0,0 +1,24 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ~0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/Rtlo.stderr b/crates/lint/testdata/Rtlo.stderr index 2c2b53df646e1..93f5bb191532f 100644 --- a/crates/lint/testdata/Rtlo.stderr +++ b/crates/lint/testdata/Rtlo.stderr @@ -4,7 +4,7 @@ warning[rtlo]: U+202A (Left-to-Right Embedding) detected LL │ string public lre = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -12,7 +12,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public lre = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202B (Right-to-Left Embedding) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -20,7 +20,7 @@ warning[rtlo]: U+202B (Right-to-Left Embedding) detected LL │ string public rle = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -28,7 +28,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public rle = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202A (Left-to-Right Embedding) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -36,7 +36,7 @@ warning[rtlo]: U+202A (Left-to-Right Embedding) detected LL │ string public pdf = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -44,7 +44,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public pdf = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202D (Left-to-Right Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -52,7 +52,7 @@ warning[rtlo]: U+202D (Left-to-Right Override) detected LL │ string public lro = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -60,7 +60,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public lro = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -68,7 +68,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ string public rlo = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -76,7 +76,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ string public rlo = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2066 (Left-to-Right Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -84,7 +84,7 @@ warning[rtlo]: U+2066 (Left-to-Right Isolate) detected LL │ string public lri = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -92,7 +92,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public lri = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2067 (Right-to-Left Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -100,7 +100,7 @@ warning[rtlo]: U+2067 (Right-to-Left Isolate) detected LL │ string public rli = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -108,7 +108,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public rli = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2068 (First Strong Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -116,7 +116,7 @@ warning[rtlo]: U+2068 (First Strong Isolate) detected LL │ string public fsi = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -124,7 +124,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public fsi = unicode"�_�"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2066 (Left-to-Right Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -132,7 +132,7 @@ warning[rtlo]: U+2066 (Left-to-Right Isolate) detected LL │ string public pdi = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+2069 (Pop Directional Isolate) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -140,7 +140,7 @@ warning[rtlo]: U+2069 (Pop Directional Isolate) detected LL │ string public pdi = unicode"��"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -148,7 +148,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ /* hidden� /* text � */ uint256 inBlockComment; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -156,7 +156,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ /* hidden� /* text � */ uint256 inBlockComment; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -164,7 +164,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ // sneaky� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -172,7 +172,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ // sneaky� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+200E (Left-to-Right Mark) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -180,7 +180,7 @@ warning[rtlo]: U+200E (Left-to-Right Mark) detected LL │ string public marks = unicode"left‎right‏end"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+200F (Right-to-Left Mark) detected ╭▸ ROOT/testdata/Rtlo.sol:LL:CC @@ -188,5 +188,5 @@ warning[rtlo]: U+200F (Right-to-Left Mark) detected LL │ string public marks = unicode"left‎right‏end"; │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo diff --git a/crates/lint/testdata/RtloCommentsOnly.stderr b/crates/lint/testdata/RtloCommentsOnly.stderr index 88a354432867e..5a7ec9ee6e69d 100644 --- a/crates/lint/testdata/RtloCommentsOnly.stderr +++ b/crates/lint/testdata/RtloCommentsOnly.stderr @@ -4,7 +4,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ // hidden� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -12,7 +12,7 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ // hidden� payload � trailing │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202E (Right-to-Left Override) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -20,7 +20,7 @@ warning[rtlo]: U+202E (Right-to-Left Override) detected LL │ /* block� comment � end */ │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo warning[rtlo]: U+202C (Pop Directional Formatting) detected ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC @@ -28,5 +28,5 @@ warning[rtlo]: U+202C (Pop Directional Formatting) detected LL │ /* block� comment � end */ │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#rtlo + ╰ help: https://getfoundry.sh/forge/linting/rtlo diff --git a/crates/lint/testdata/ScreamingSnakeCase.stderr b/crates/lint/testdata/ScreamingSnakeCase.stderr index a740506ed74d8..36305bb268d9a 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.stderr +++ b/crates/lint/testdata/ScreamingSnakeCase.stderr @@ -4,7 +4,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -12,7 +12,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screaming_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -20,7 +20,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant ScreamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -28,7 +28,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant SCREAMING_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -36,7 +36,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -44,7 +44,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screaming_snake_case0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -52,7 +52,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable ScreamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -60,5 +60,5 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable SCREAMING_snake_case_0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE_0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable diff --git a/crates/lint/testdata/StructPascalCase.stderr b/crates/lint/testdata/StructPascalCase.stderr index 1c7bfa13ba84b..255c1c4d5d74b 100644 --- a/crates/lint/testdata/StructPascalCase.stderr +++ b/crates/lint/testdata/StructPascalCase.stderr @@ -4,7 +4,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct _PascalCase { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -12,7 +12,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalCase { │ ━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -20,7 +20,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalcase { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -28,7 +28,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascal_case { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -36,7 +36,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCAL_CASE { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -44,5 +44,5 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCALCASE { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct diff --git a/crates/lint/testdata/TooManyDigits.sol b/crates/lint/testdata/TooManyDigits.sol new file mode 100644 index 0000000000000..a56ad67fe379e --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.sol @@ -0,0 +1,73 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TooManyDigits { + // SHOULD FAIL: plain decimal integer literals with 5+ consecutive zeros. + + uint256 stateA = 1000000000000000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + uint256 stateB = 100000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + function asReturn() public pure returns (uint256) { + return 10000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asComparison(uint256 x) public pure returns (bool) { + return x == 1000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArg(address to) public { + _send(to, 50000000000); //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArraySize() public pure { + uint256[100000] memory _arr; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + // Zero-run in the middle (not just trailing). + uint256 middleZeros = 123000007; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscores that don't actually break up the zero run. + uint256 badGrouping = 1_000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscore right after a single digit, leaving a 5-zero group. + uint256 badGrouping2 = 1_00000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // SHOULD PASS: + + // Boundary: 4 consecutive zeros (one short of the threshold). + uint256 fourZeros = 10000; + + // Uppercase scientific notation. + uint256 sciUpper = 1E18; + + // Scientific notation. + uint256 sci = 1e18; + + // Underscore-separated digit groups. + uint256 grouped = 1_000_000_000_000_000_000; + + // Sub-denominations. + uint256 oneEther = 1 ether; + uint256 oneGwei = 1 gwei; + uint256 fiveMin = 5 minutes; + + // Address literal (distinct AST kind, not flagged). + address adr = 0x1234567890123456789012345678901234567890; + + // Hex literal — intentional zero patterns (mask / padded value). + bytes32 mask = 0x0000000000000000000000000000000000000000000000000000000000000001; + uint256 hexNum = 0x100000; + + // Small literals (< 5 consecutive zeros). + uint256 small1 = 100; + uint256 small2 = 9999; + uint256 small3 = 1234; + uint256 spread = 101010; + + // Boolean literal. + bool flag = true; + + function _send(address, uint256) internal pure {} +} diff --git a/crates/lint/testdata/TooManyDigits.stderr b/crates/lint/testdata/TooManyDigits.stderr new file mode 100644 index 0000000000000..7e21a530776c2 --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.stderr @@ -0,0 +1,72 @@ +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateA = 1000000000000000000; + │ ━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateB = 100000; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return 10000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return x == 1000000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … _send(to, 50000000000); + │ ━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … uint256[100000] memory _arr; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 middleZeros = 123000007; + │ ━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping = 1_000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping2 = 1_00000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + diff --git a/crates/lint/testdata/TxOrigin.sol b/crates/lint/testdata/TxOrigin.sol new file mode 100644 index 0000000000000..9728a7e528e5b --- /dev/null +++ b/crates/lint/testdata/TxOrigin.sol @@ -0,0 +1,65 @@ +//@compile-flags: --only-lint tx-origin +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TxOrigin { + address public owner; + mapping(address => bool) public allowed; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(tx.origin == owner, "not owner"); //~WARN: `tx.origin` should not be used for authorization + _; + } + + function guardedByIf() external view { + if (tx.origin != owner) { //~WARN: `tx.origin` should not be used for authorization + revert("not owner"); + } + } + + function guardedByPredicate() external view { + assert(isOwner(tx.origin)); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByWhile() external view { + while (tx.origin == owner) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByFor() external view { + for (; tx.origin == owner;) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByDoWhile() external view { + do { + } while (tx.origin == owner); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByMapping() external view { + require(allowed[tx.origin], "not allowed"); //~WARN: `tx.origin` should not be used for authorization + require(allowed[tx.origin] == true, "not allowed"); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByTernary() external view { + require(tx.origin == owner ? true : false, "not owner"); //~WARN: `tx.origin` should not be used for authorization + } + + function readForLogging() external view returns (address) { + return tx.origin; + } + + function explicitSenderCheck() external view { + require(msg.sender == owner, "not owner"); + } + + function isOwner(address account) internal view returns (bool) { + return account == owner; + } +} diff --git a/crates/lint/testdata/TxOrigin.stderr b/crates/lint/testdata/TxOrigin.stderr new file mode 100644 index 0000000000000..7c2e70225b76d --- /dev/null +++ b/crates/lint/testdata/TxOrigin.stderr @@ -0,0 +1,72 @@ +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner, "not owner"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ if (tx.origin != owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ assert(isOwner(tx.origin)); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ while (tx.origin == owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ for (; tx.origin == owner;) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ } while (tx.origin == owner); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin], "not allowed"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin] == true, "not allowed"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner ? true : false, "not owner"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + diff --git a/crates/lint/testdata/UncheckedCall.stderr b/crates/lint/testdata/UncheckedCall.stderr index afb8ade4ea89b..8a8a9fa9b5e17 100644 --- a/crates/lint/testdata/UncheckedCall.stderr +++ b/crates/lint/testdata/UncheckedCall.stderr @@ -4,7 +4,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call(data); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -12,7 +12,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call{value: value}(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -20,7 +20,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.delegatecall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -28,7 +28,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.staticcall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -36,7 +36,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target1.call(""); │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -44,7 +44,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target2.delegatecall(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -52,7 +52,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, bytes memory data) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -60,5 +60,5 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, existingData) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call diff --git a/crates/lint/testdata/UncheckedTransferERC20.stderr b/crates/lint/testdata/UncheckedTransferERC20.stderr index 733d22ce610d1..2c2caa69e7215 100644 --- a/crates/lint/testdata/UncheckedTransferERC20.stderr +++ b/crates/lint/testdata/UncheckedTransferERC20.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Wrapper { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransfer { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library Currency { │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransferUsingCurrencyLib { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -44,7 +44,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ IERC20(address(token)).transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -52,7 +52,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -60,7 +60,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -68,7 +68,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -76,7 +76,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -84,5 +84,5 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer diff --git a/crates/lint/testdata/UnsafeCheatcodes.stderr b/crates/lint/testdata/UnsafeCheatcodes.stderr index e66a4d72c70de..5b8b429942e80 100644 --- a/crates/lint/testdata/UnsafeCheatcodes.stderr +++ b/crates/lint/testdata/UnsafeCheatcodes.stderr @@ -4,7 +4,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -12,7 +12,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -20,7 +20,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readLine("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -28,7 +28,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeFile("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -36,7 +36,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeLine("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -44,7 +44,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.removeFile("test.txt"); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.closeFile("test.txt"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -60,7 +60,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -68,7 +68,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.deriveKey("mnemonic", 0); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -76,7 +76,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ bytes memory result = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(new string[](1)); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -100,5 +100,5 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode diff --git a/crates/lint/testdata/UnsafeTypecast.stderr b/crates/lint/testdata/UnsafeTypecast.stderr index b3e0334d63d43..d909b90973e00 100644 --- a/crates/lint/testdata/UnsafeTypecast.stderr +++ b/crates/lint/testdata/UnsafeTypecast.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UnsafeTypecast { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract Repros { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -26,7 +26,7 @@ LL │ uint248 b = uint248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -40,7 +40,7 @@ LL │ uint240 c = uint240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -54,7 +54,7 @@ LL │ uint232 d = uint232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -68,7 +68,7 @@ LL │ uint224 e = uint224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -82,7 +82,7 @@ LL │ uint216 f = uint216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -96,7 +96,7 @@ LL │ uint208 g = uint208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -110,7 +110,7 @@ LL │ uint200 h = uint200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -124,7 +124,7 @@ LL │ uint192 i = uint192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -138,7 +138,7 @@ LL │ uint184 j = uint184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -152,7 +152,7 @@ LL │ uint176 k = uint176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -166,7 +166,7 @@ LL │ uint168 l = uint168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -180,7 +180,7 @@ LL │ uint160 m = uint160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -194,7 +194,7 @@ LL │ uint152 n = uint152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -208,7 +208,7 @@ LL │ uint144 o = uint144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -222,7 +222,7 @@ LL │ uint136 p = uint136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -236,7 +236,7 @@ LL │ uint128 q = uint128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -250,7 +250,7 @@ LL │ uint120 r = uint120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -264,7 +264,7 @@ LL │ uint112 s = uint112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -278,7 +278,7 @@ LL │ uint104 t = uint104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -292,7 +292,7 @@ LL │ uint96 u = uint96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -306,7 +306,7 @@ LL │ uint88 v = uint88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -320,7 +320,7 @@ LL │ uint80 w = uint80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -334,7 +334,7 @@ LL │ uint72 x = uint72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -348,7 +348,7 @@ LL │ uint64 y = uint64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -362,7 +362,7 @@ LL │ uint56 z = uint56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -376,7 +376,7 @@ LL │ uint48 A = uint48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -390,7 +390,7 @@ LL │ uint40 B = uint40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -404,7 +404,7 @@ LL │ uint32 C = uint32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -418,7 +418,7 @@ LL │ uint24 D = uint24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -432,7 +432,7 @@ LL │ uint16 E = uint16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -446,7 +446,7 @@ LL │ uint8 F = uint8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -460,7 +460,7 @@ LL │ int248 b = int248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -474,7 +474,7 @@ LL │ int240 c = int240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -488,7 +488,7 @@ LL │ int232 d = int232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -502,7 +502,7 @@ LL │ int224 e = int224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -516,7 +516,7 @@ LL │ int216 f = int216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -530,7 +530,7 @@ LL │ int208 g = int208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -544,7 +544,7 @@ LL │ int200 h = int200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -558,7 +558,7 @@ LL │ int192 i = int192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -572,7 +572,7 @@ LL │ int184 j = int184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -586,7 +586,7 @@ LL │ int176 k = int176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -600,7 +600,7 @@ LL │ int168 l = int168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -614,7 +614,7 @@ LL │ int160 m = int160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -628,7 +628,7 @@ LL │ int152 n = int152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -642,7 +642,7 @@ LL │ int144 o = int144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -656,7 +656,7 @@ LL │ int136 p = int136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -670,7 +670,7 @@ LL │ int128 q = int128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -684,7 +684,7 @@ LL │ int120 r = int120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -698,7 +698,7 @@ LL │ int112 s = int112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -712,7 +712,7 @@ LL │ int104 t = int104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -726,7 +726,7 @@ LL │ int96 u = int96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -740,7 +740,7 @@ LL │ int88 v = int88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -754,7 +754,7 @@ LL │ int80 w = int80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -768,7 +768,7 @@ LL │ int72 x = int72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -782,7 +782,7 @@ LL │ int64 y = int64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -796,7 +796,7 @@ LL │ int56 z = int56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -810,7 +810,7 @@ LL │ int48 A = int48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -824,7 +824,7 @@ LL │ int40 B = int40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -838,7 +838,7 @@ LL │ int32 C = int32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -852,7 +852,7 @@ LL │ int24 D = int24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -866,7 +866,7 @@ LL │ int16 E = int16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -880,7 +880,7 @@ LL │ int8 F = int8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -894,7 +894,7 @@ LL │ bytes31 b = bytes31(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -908,7 +908,7 @@ LL │ bytes30 c = bytes30(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -922,7 +922,7 @@ LL │ bytes29 d = bytes29(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -936,7 +936,7 @@ LL │ bytes28 e = bytes28(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -950,7 +950,7 @@ LL │ bytes27 f = bytes27(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -964,7 +964,7 @@ LL │ bytes26 g = bytes26(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -978,7 +978,7 @@ LL │ bytes25 h = bytes25(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -992,7 +992,7 @@ LL │ bytes24 i = bytes24(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1006,7 +1006,7 @@ LL │ bytes23 j = bytes23(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1020,7 +1020,7 @@ LL │ bytes22 k = bytes22(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1034,7 +1034,7 @@ LL │ bytes21 l = bytes21(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1048,7 +1048,7 @@ LL │ bytes20 m = bytes20(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1062,7 +1062,7 @@ LL │ bytes19 n = bytes19(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1076,7 +1076,7 @@ LL │ bytes18 o = bytes18(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1090,7 +1090,7 @@ LL │ bytes17 p = bytes17(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1104,7 +1104,7 @@ LL │ bytes16 q = bytes16(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1118,7 +1118,7 @@ LL │ bytes15 r = bytes15(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1132,7 +1132,7 @@ LL │ bytes14 s = bytes14(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1146,7 +1146,7 @@ LL │ bytes13 t = bytes13(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1160,7 +1160,7 @@ LL │ bytes12 u = bytes12(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1174,7 +1174,7 @@ LL │ bytes11 v = bytes11(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1188,7 +1188,7 @@ LL │ bytes10 w = bytes10(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1202,7 +1202,7 @@ LL │ bytes9 x = bytes9(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1216,7 +1216,7 @@ LL │ bytes8 y = bytes8(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1230,7 +1230,7 @@ LL │ bytes7 z = bytes7(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1244,7 +1244,7 @@ LL │ bytes6 A = bytes6(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1258,7 +1258,7 @@ LL │ bytes5 B = bytes5(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1272,7 +1272,7 @@ LL │ bytes4 C = bytes4(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1286,7 +1286,7 @@ LL │ bytes3 D = bytes3(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1300,7 +1300,7 @@ LL │ bytes2 E = bytes2(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1314,7 +1314,7 @@ LL │ bytes1 F = bytes1(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1328,7 +1328,7 @@ LL │ int256 b = int256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1342,7 +1342,7 @@ LL │ int248 d = int248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1356,7 +1356,7 @@ LL │ int240 f = int240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1370,7 +1370,7 @@ LL │ int232 h = int232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1384,7 +1384,7 @@ LL │ int224 j = int224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1398,7 +1398,7 @@ LL │ int216 l = int216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1412,7 +1412,7 @@ LL │ int208 n = int208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1426,7 +1426,7 @@ LL │ int200 p = int200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1440,7 +1440,7 @@ LL │ int192 r = int192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1454,7 +1454,7 @@ LL │ int184 t = int184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1468,7 +1468,7 @@ LL │ int176 v = int176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1482,7 +1482,7 @@ LL │ int168 x = int168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1496,7 +1496,7 @@ LL │ int160 z = int160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1510,7 +1510,7 @@ LL │ int152 B = int152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1524,7 +1524,7 @@ LL │ int144 D = int144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1538,7 +1538,7 @@ LL │ int136 F = int136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1552,7 +1552,7 @@ LL │ int128 H = int128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1566,7 +1566,7 @@ LL │ int120 J = int120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1580,7 +1580,7 @@ LL │ int112 L = int112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1594,7 +1594,7 @@ LL │ int104 N = int104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1608,7 +1608,7 @@ LL │ int96 P = int96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1622,7 +1622,7 @@ LL │ int88 R = int88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1636,7 +1636,7 @@ LL │ int80 T = int80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1650,7 +1650,7 @@ LL │ int72 V = int72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1664,7 +1664,7 @@ LL │ int64 X = int64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1678,7 +1678,7 @@ LL │ int56 Z = int56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1692,7 +1692,7 @@ LL │ int48 BB = int48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1706,7 +1706,7 @@ LL │ int40 DD = int40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1720,7 +1720,7 @@ LL │ int32 FF = int32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1734,7 +1734,7 @@ LL │ int24 HH = int24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1748,7 +1748,7 @@ LL │ int16 JJ = int16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1762,7 +1762,7 @@ LL │ int8 LL = int8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1776,7 +1776,7 @@ LL │ uint256 b = uint256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1790,7 +1790,7 @@ LL │ uint248 d = uint248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1804,7 +1804,7 @@ LL │ uint240 f = uint240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1818,7 +1818,7 @@ LL │ uint232 h = uint232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1832,7 +1832,7 @@ LL │ uint224 j = uint224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1846,7 +1846,7 @@ LL │ uint216 l = uint216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1860,7 +1860,7 @@ LL │ uint208 n = uint208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1874,7 +1874,7 @@ LL │ uint200 p = uint200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1888,7 +1888,7 @@ LL │ uint192 r = uint192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1902,7 +1902,7 @@ LL │ uint184 t = uint184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1916,7 +1916,7 @@ LL │ uint176 v = uint176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1930,7 +1930,7 @@ LL │ uint168 x = uint168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1944,7 +1944,7 @@ LL │ uint160 z = uint160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1958,7 +1958,7 @@ LL │ uint152 B = uint152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1972,7 +1972,7 @@ LL │ uint144 D = uint144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1986,7 +1986,7 @@ LL │ uint136 F = uint136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2000,7 +2000,7 @@ LL │ uint128 H = uint128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2014,7 +2014,7 @@ LL │ uint120 J = uint120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2028,7 +2028,7 @@ LL │ uint112 L = uint112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2042,7 +2042,7 @@ LL │ uint104 N = uint104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2056,7 +2056,7 @@ LL │ uint96 P = uint96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2070,7 +2070,7 @@ LL │ uint88 R = uint88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2084,7 +2084,7 @@ LL │ uint80 T = uint80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2098,7 +2098,7 @@ LL │ uint72 V = uint72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2112,7 +2112,7 @@ LL │ uint64 X = uint64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2126,7 +2126,7 @@ LL │ uint56 Z = uint56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2140,7 +2140,7 @@ LL │ uint48 BB = uint48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2154,7 +2154,7 @@ LL │ uint40 DD = uint40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2168,7 +2168,7 @@ LL │ uint32 FF = uint32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2182,7 +2182,7 @@ LL │ uint24 HH = uint24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2196,7 +2196,7 @@ LL │ uint16 JJ = uint16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2210,7 +2210,7 @@ LL │ uint8 LL = uint8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2224,7 +2224,7 @@ LL │ bytes32 dataSlice = bytes32(data); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2238,7 +2238,7 @@ LL │ bytes32 strSlice = bytes32(bytes(str)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2252,7 +2252,7 @@ LL │ uint128 aPlusB = uint128(int128(uint128(a)) + b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2266,7 +2266,7 @@ LL │ uint64 unsafe = uint64(aPlusB); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2280,7 +2280,7 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2294,5 +2294,5 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast diff --git a/crates/lint/testdata/UnusedStateVariables.stderr b/crates/lint/testdata/UnusedStateVariables.stderr index ecc9e87efc105..92a0082bb8293 100644 --- a/crates/lint/testdata/UnusedStateVariables.stderr +++ b/crates/lint/testdata/UnusedStateVariables.stderr @@ -4,7 +4,7 @@ note[could-be-immutable]: state variable could be declared immutable LL │ address usedInBoth; │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#could-be-immutable + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -12,7 +12,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 unused; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -20,7 +20,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 unused; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -28,7 +28,7 @@ note[unused-state-variables]: state variable is never used LL │ uint256 firstUnused; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[unused-state-variables]: state variable is never used ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC @@ -36,5 +36,5 @@ note[unused-state-variables]: state variable is never used LL │ uint256 secondUnused; │ ━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-state-variables + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables diff --git a/crates/lint/testdata/UnwrappedModifierLogic.stderr b/crates/lint/testdata/UnwrappedModifierLogic.stderr index 5e1bf754e60e4..dc5514c2d5e98 100644 --- a/crates/lint/testdata/UnwrappedModifierLogic.stderr +++ b/crates/lint/testdata/UnwrappedModifierLogic.stderr @@ -9,7 +9,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforePlaceholder() { @@ -35,7 +35,7 @@ LL │ ┃ checkInternal(msg.sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleAfterPlaceholder() { @@ -62,7 +62,7 @@ LL │ ┃ checkPublic(sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforeAfterPlaceholder(address sender) { @@ -91,7 +91,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwner() { @@ -113,7 +113,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRole(bytes32 role) { @@ -135,7 +135,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrOpenRole(bytes32 role) { @@ -157,7 +157,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrAdmin(bytes32 role, address admin) { @@ -180,7 +180,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier assign(address sender) { @@ -204,7 +204,7 @@ LL │ ┃ sender; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier uncheckedBlock(address sender) { @@ -228,7 +228,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier emitEvent(address sender) { @@ -250,7 +250,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwnerContract(address sender) { diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 9970616900e6b..15363a6a40bb0 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -23,10 +23,10 @@ alloy-rpc-types-eth.workspace = true alloy-serde.workspace = true alloy-signer.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"] } -op-alloy-rpc-types.workspace = true -alloy-op-evm.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +alloy-op-evm = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } revm.workspace = true serde_json.workspace = true serde = { version = "1.0", features = ["derive"] } @@ -34,3 +34,12 @@ derive_more.workspace = true tempo-primitives.workspace = true tempo-alloy.workspace = true tempo-revm.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", +] diff --git a/crates/primitives/src/network/mod.rs b/crates/primitives/src/network/mod.rs index 7000b580b6a73..0b840185b0383 100644 --- a/crates/primitives/src/network/mod.rs +++ b/crates/primitives/src/network/mod.rs @@ -1,12 +1,20 @@ use alloy_network::Network; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; use alloy_provider::fillers::{ BlobGasFiller, ChainIdFiller, GasFiller, JoinFill, NonceFiller, RecommendedFillers, }; +#[cfg(feature = "optimism")] +pub use optimism::FoundryTransactionResponse; pub use receipt::*; +/// Default JSON-RPC transaction response when the `optimism` feature is disabled. +#[cfg(not(feature = "optimism"))] +pub type FoundryTransactionResponse = alloy_rpc_types_eth::Transaction; + /// Foundry network type. /// /// This network type supports Foundry-specific transaction types, including @@ -36,7 +44,7 @@ impl Network for FoundryNetwork { type TransactionRequest = crate::FoundryTransactionRequest; - type TransactionResponse = op_alloy_rpc_types::Transaction; + type TransactionResponse = FoundryTransactionResponse; type ReceiptResponse = crate::FoundryTxReceipt; diff --git a/crates/primitives/src/network/optimism.rs b/crates/primitives/src/network/optimism.rs new file mode 100644 index 0000000000000..aff30a755663f --- /dev/null +++ b/crates/primitives/src/network/optimism.rs @@ -0,0 +1,47 @@ +//! OP-stack-specific helpers and type aliases used by [`super::FoundryNetwork`] and +//! [`super::FoundryTxReceipt`]. + +use alloy_consensus::{Receipt, ReceiptWithBloom, TxReceipt}; +use alloy_primitives::U64; +use alloy_rpc_types::Log; +use alloy_serde::OtherFields; +use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; + +use crate::FoundryReceiptEnvelope; + +/// JSON-RPC transaction response type used by [`super::FoundryNetwork`]. +pub type FoundryTransactionResponse = op_alloy_rpc_types::Transaction; + +/// Build a [`FoundryReceiptEnvelope::Deposit`] from a `ReceiptWithBloom` plus the OP +/// deposit-specific fields decoded from the [`OtherFields`] of an `AnyTransactionReceipt`. +pub(super) fn build_deposit_receipt_envelope( + receipt_with_bloom: ReceiptWithBloom>, + other: &OtherFields, +) -> FoundryReceiptEnvelope { + // These fields may not be present in all receipts, so missing/invalid values are None. + let deposit_nonce = other + .get_deserialized::("depositNonce") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + let deposit_receipt_version = other + .get_deserialized::("depositReceiptVersion") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + + FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { + status: alloy_consensus::Eip658Value::Eip658(receipt_with_bloom.status()), + cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), + logs: receipt_with_bloom.receipt.logs, + }, + deposit_nonce, + deposit_receipt_version, + }, + logs_bloom: receipt_with_bloom.logs_bloom, + }) +} diff --git a/crates/primitives/src/network/receipt.rs b/crates/primitives/src/network/receipt.rs index b727b4c39b345..6b01f9eaa9ee9 100644 --- a/crates/primitives/src/network/receipt.rs +++ b/crates/primitives/src/network/receipt.rs @@ -1,13 +1,13 @@ -use alloy_consensus::{Receipt, TxReceipt}; use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt, ReceiptResponse}; -use alloy_primitives::{Address, B256, BlockHash, TxHash, U64}; +use alloy_primitives::{Address, B256, BlockHash, TxHash}; use alloy_rpc_types::{ConversionError, Log, TransactionReceipt}; use alloy_serde::WithOtherFields; use derive_more::AsRef; -use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; use serde::{Deserialize, Serialize}; use tempo_primitives::TEMPO_TX_TYPE_ID; +#[cfg(feature = "optimism")] +use super::optimism::build_deposit_receipt_envelope; use crate::FoundryReceiptEnvelope; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AsRef)] @@ -144,38 +144,8 @@ impl TryFrom for FoundryTxReceipt { 0x03 => FoundryReceiptEnvelope::Eip4844(receipt_with_bloom), 0x04 => FoundryReceiptEnvelope::Eip7702(receipt_with_bloom), TEMPO_TX_TYPE_ID => FoundryReceiptEnvelope::Tempo(receipt_with_bloom), - 0x7E => { - // Construct the deposit receipt, extracting optional deposit fields - // These fields may not be present in all receipts, so missing/invalid - // values are None - let deposit_nonce = other - .get_deserialized::("depositNonce") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - let deposit_receipt_version = other - .get_deserialized::("depositReceiptVersion") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - - FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { - receipt: OpDepositReceipt { - inner: Receipt { - status: alloy_consensus::Eip658Value::Eip658( - receipt_with_bloom.status(), - ), - cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), - logs: receipt_with_bloom.receipt.logs, - }, - deposit_nonce, - deposit_receipt_version, - }, - logs_bloom: receipt_with_bloom.logs_bloom, - }) - } + #[cfg(feature = "optimism")] + 0x7E => build_deposit_receipt_envelope(receipt_with_bloom, &other), _ => { let tx_type = r#type; return Err(ConversionError::Custom(format!( diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index ab4aafcc294c0..0a009a1931f06 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,6 +1,7 @@ +#[cfg(feature = "optimism")] +use alloy_consensus::{Sealed, Transaction as _}; use alloy_consensus::{ - Sealed, Signed, Transaction as _, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, - TxLegacy, TxType, Typed2718, + Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType, Typed2718, crypto::RecoveryError, transaction::{ SignerRecoverable, TxEip7702, TxHashRef, @@ -9,14 +10,10 @@ use alloy_consensus::{ }; use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, TxHash}; use alloy_rpc_types::ConversionError; -use op_alloy_consensus::{ - DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, POST_EXEC_TX_TYPE_ID, TxDeposit, - TxPostExec, -}; -use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +#[cfg(feature = "optimism")] +use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit, TxPostExec}; use revm::context::TxEnv; use tempo_primitives::{AASigned, TempoTransaction}; use tempo_revm::TempoTxEnv; @@ -57,9 +54,11 @@ pub enum FoundryTxEnvelope { /// OP stack deposit transaction. /// /// See . + #[cfg(feature = "optimism")] #[envelope(ty = 126)] Deposit(Sealed), /// OP stack post-execution synthetic transaction. + #[cfg(feature = "optimism")] #[envelope(ty = 0x7D)] PostExec(Sealed), /// Tempo transaction type. @@ -80,7 +79,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)), Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)), Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)), + #[cfg(feature = "optimism")] Self::Deposit(_) => Err(self), + #[cfg(feature = "optimism")] Self::PostExec(_) => Err(self), Self::Tempo(_) => Err(self), } @@ -109,7 +110,9 @@ impl FoundryTxEnvelope { Self::Eip1559(t) => *t.hash(), Self::Eip4844(t) => *t.hash(), Self::Eip7702(t) => *t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.tx_hash(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.tx_hash(), Self::Tempo(t) => *t.hash(), } @@ -128,7 +131,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => tx.recover_signer()?, Self::Eip4844(tx) => tx.recover_signer()?, Self::Eip7702(tx) => tx.recover_signer()?, + #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.from, + #[cfg(feature = "optimism")] Self::PostExec(tx) => tx.inner().signer_address(), Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?, }) @@ -143,7 +148,9 @@ impl TxHashRef for FoundryTxEnvelope { Self::Eip1559(t) => t.hash(), Self::Eip4844(t) => t.hash(), Self::Eip7702(t) => t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.hash_ref(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.hash_ref(), Self::Tempo(t) => t.hash(), } @@ -160,23 +167,6 @@ impl SignerRecoverable for FoundryTxEnvelope { } } -impl OpTransactionTrait for FoundryTxEnvelope { - fn is_deposit(&self) -> bool { - matches!(self, Self::Deposit(_)) - } - - fn as_deposit(&self) -> Option<&Sealed> { - match self { - Self::Deposit(tx) => Some(tx), - _ => None, - } - } - - fn as_post_exec(&self) -> Option<&Sealed> { - if let Self::PostExec(tx) = self { Some(tx) } else { None } - } -} - impl TryFrom for TxEnvelope { type Error = FoundryTxEnvelope; @@ -197,19 +187,6 @@ impl From for FoundryTxEnvelope { } } -impl From for FoundryTxEnvelope { - fn from(tx: op_alloy_consensus::OpTxEnvelope) -> Self { - match tx { - op_alloy_consensus::OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), - op_alloy_consensus::OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), - op_alloy_consensus::OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), - op_alloy_consensus::OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), - op_alloy_consensus::OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), - op_alloy_consensus::OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), - } - } -} - impl From for FoundryTxEnvelope { fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self { match tx { @@ -236,33 +213,50 @@ impl TryFrom for FoundryTxEnvelope { TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)), TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)), }, - AnyTxEnvelope::Unknown(mut tx) => { - // Try to convert to deposit transaction - if tx.ty() == DEPOSIT_TX_TYPE_ID { - tx.inner.fields.insert("from".to_string(), serde_json::to_value(from).unwrap()); - let deposit_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize deposit tx: {e}" - )) - })?; - - return Ok(Self::Deposit(Sealed::new(deposit_tx))); + AnyTxEnvelope::Unknown(tx) => { + #[cfg(feature = "optimism")] + { + let mut tx = tx; + let _ = from; + // Try to convert to deposit transaction + if tx.ty() == DEPOSIT_TX_TYPE_ID { + tx.inner + .fields + .insert("from".to_string(), serde_json::to_value(from).unwrap()); + let deposit_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize deposit tx: {e}" + )) + })?; + + return Ok(Self::Deposit(Sealed::new(deposit_tx))); + } + + if tx.ty() == POST_EXEC_TX_TYPE_ID { + let post_exec_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize post-exec tx: {e}" + )) + })?; + + return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + } + + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - if tx.ty() == POST_EXEC_TX_TYPE_ID { - let post_exec_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize post-exec tx: {e}" - )) - })?; - - return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + #[cfg(not(feature = "optimism"))] + { + let _ = from; + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - let tx_type = tx.ty(); - Err(ConversionError::Custom(format!("Unknown transaction type: 0x{tx_type:02X}"))) } } } @@ -276,6 +270,7 @@ impl FromRecoveredTx for TxEnv { FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -288,6 +283,7 @@ impl FromRecoveredTx for TxEnv { ..Default::default() } } + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -303,63 +299,6 @@ impl FromRecoveredTx for TxEnv { } } -impl FromRecoveredTx for OpTransaction { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: None, deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl FromTxWithEncoded for TxEnv { fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self { Self::from_recovered_tx(tx, sender) @@ -384,7 +323,9 @@ impl FromRecoveredTx for TempoTxEnv { FoundryTxEnvelope::Eip7702(signed_tx) => { Self::from(TxEnv::from_recovered_tx(signed_tx, caller)) } + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"), FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller), } @@ -397,75 +338,6 @@ impl FromTxWithEncoded for TempoTxEnv { } } -impl FromRecoveredTx for OpTx { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - Self(OpTransaction::::from_recovered_tx(tx, caller)) - } -} - -impl FromTxWithEncoded for OpTx { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) - } -} - -impl FromTxWithEncoded for OpTransaction { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: Some(encoded), deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl std::fmt::Display for FoundryTxType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -474,7 +346,9 @@ impl std::fmt::Display for FoundryTxType { Self::Eip1559 => write!(f, "eip1559"), Self::Eip4844 => write!(f, "eip4844"), Self::Eip7702 => write!(f, "eip7702"), + #[cfg(feature = "optimism")] Self::Deposit => write!(f, "deposit"), + #[cfg(feature = "optimism")] Self::PostExec => write!(f, "post-exec"), Self::Tempo => write!(f, "tempo"), } @@ -501,7 +375,9 @@ impl From for FoundryTypedTx { FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()), FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()), FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.into_inner()), FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()), } @@ -609,28 +485,6 @@ mod tests { assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2")); } - #[test] - fn test_decode_encode_deposit_tx() { - // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" - .parse::() - .unwrap(); - - // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let raw_tx = alloy_primitives::hex::decode( - "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", - ) - .unwrap(); - let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); - - let mut encoded = Vec::new(); - dep_tx.encode_2718(&mut encoded); - - assert_eq!(raw_tx, encoded); - - assert_eq!(tx_hash, dep_tx.hash()); - } - #[test] fn can_recover_sender_not_normalized() { let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap(); @@ -707,11 +561,6 @@ mod tests { assert_eq!(tx_env.caller, sender); assert_eq!(tx_env.gas_limit, 0x5208); assert_eq!(tx_env.gas_price, 1); - - // Test OpTransaction conversion via FromRecoveredTx trait - let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); - assert_eq!(op_tx.base.caller, sender); - assert_eq!(op_tx.base.gas_limit, 0x5208); } // Test vector from Tempo testnet: diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 7dccf3e30752f..18f39c437bfbc 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1,7 +1,11 @@ mod envelope; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; mod request; pub use envelope::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; +#[cfg(feature = "optimism")] +pub use optimism::get_deposit_tx_parts; pub use receipt::FoundryReceiptEnvelope; -pub use request::{FoundryTransactionRequest, get_deposit_tx_parts}; +pub use request::FoundryTransactionRequest; diff --git a/crates/primitives/src/transaction/optimism.rs b/crates/primitives/src/transaction/optimism.rs new file mode 100644 index 0000000000000..5952b5d6a9020 --- /dev/null +++ b/crates/primitives/src/transaction/optimism.rs @@ -0,0 +1,300 @@ +//! OP-stack-specific impls for [`FoundryTxEnvelope`] and [`FoundryTransactionRequest`]. + +use alloy_consensus::{Sealed, Transaction as _, Typed2718}; +use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::OpTx; +use alloy_primitives::{Address, B256, Bytes, U256}; +use alloy_serde::OtherFields; +use op_alloy_consensus::{ + OpDepositReceipt, OpDepositReceiptWithBloom, OpTransaction as OpTransactionTrait, OpTxEnvelope, + TxDeposit, TxPostExec, +}; +use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +use revm::context::TxEnv; + +use super::{FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope}; + +impl OpTransactionTrait for FoundryTxEnvelope { + fn is_deposit(&self) -> bool { + matches!(self, Self::Deposit(_)) + } + + fn as_deposit(&self) -> Option<&Sealed> { + match self { + Self::Deposit(tx) => Some(tx), + _ => None, + } + } + + fn as_post_exec(&self) -> Option<&Sealed> { + if let Self::PostExec(tx) = self { Some(tx) } else { None } + } +} + +impl From for FoundryTxEnvelope { + fn from(tx: OpTxEnvelope) -> Self { + match tx { + OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), + OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), + OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), + OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), + OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), + OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), + } + } +} + +impl FromRecoveredTx for OpTransaction { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: None, deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl FromRecoveredTx for OpTx { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + Self(OpTransaction::::from_recovered_tx(tx, caller)) + } +} + +impl FromTxWithEncoded for OpTx { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) + } +} + +impl FromTxWithEncoded for OpTransaction { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: Some(encoded), deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl From> for FoundryTransactionRequest { + fn from(tx: op_alloy_rpc_types::Transaction) -> Self { + tx.inner.into_inner().into() + } +} + +/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields. +pub fn get_deposit_tx_parts( + other: &OtherFields, +) -> Result> { + let mut missing = Vec::new(); + let source_hash = + other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("sourceHash"); + Default::default() + }, + ); + let mint = other + .get_deserialized::("mint") + .transpose() + .unwrap_or_else(|_| { + missing.push("mint"); + Default::default() + }) + .map(|value| value.to::()); + let is_system_transaction = + other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("isSystemTx"); + Default::default() + }, + ); + if missing.is_empty() { + Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) + } else { + Err(missing) + } +} + +/// OP-stack-specific accessors on [`FoundryReceiptEnvelope`]. +impl FoundryReceiptEnvelope { + /// Return the receipt's deposit_nonce if it is a deposit receipt. + pub fn deposit_nonce(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_nonce) + } + + /// Return the receipt's deposit version if it is a deposit receipt. + pub fn deposit_receipt_version(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { + match self { + Self::Deposit(t) => Some(t), + _ => None, + } + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { + match self { + Self::Deposit(t) => Some(&t.receipt), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use alloy_network::eip2718::Encodable2718; + use alloy_primitives::TxHash; + use alloy_rlp::Decodable; + + use super::*; + + #[test] + fn test_from_recovered_tx_legacy_op() { + use alloy_consensus::transaction::SignerRecoverable; + + let tx = r#" + { + "type": "0x0", + "chainId": "0x1", + "nonce": "0x0", + "gas": "0x5208", + "gasPrice": "0x1", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "value": "0x1", + "input": "0x", + "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0", + "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd", + "v": "0x1b", + "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515" + }"#; + + let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap(); + let sender = typed_tx.recover_signer().unwrap(); + + // Test OpTransaction conversion via FromRecoveredTx trait + let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); + assert_eq!(op_tx.base.caller, sender); + assert_eq!(op_tx.base.gas_limit, 0x5208); + } + + #[test] + fn test_decode_encode_deposit_tx() { + // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" + .parse::() + .unwrap(); + + // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let raw_tx = alloy_primitives::hex::decode( + "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", + ) + .unwrap(); + let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); + + let mut encoded = Vec::new(); + dep_tx.encode_2718(&mut encoded); + + assert_eq!(raw_tx, encoded); + + assert_eq!(tx_hash, dep_tx.hash()); + } +} diff --git a/crates/primitives/src/transaction/receipt.rs b/crates/primitives/src/transaction/receipt.rs index fe209fc72c907..78bbcd1efa2fb 100644 --- a/crates/primitives/src/transaction/receipt.rs +++ b/crates/primitives/src/transaction/receipt.rs @@ -8,6 +8,7 @@ use alloy_network::eip2718::{ use alloy_primitives::{Bloom, Log, TxHash, logs_bloom}; use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes}; use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{ DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID, }; @@ -29,8 +30,10 @@ pub enum FoundryReceiptEnvelope { Eip4844(ReceiptWithBloom>), #[serde(rename = "0x4", alias = "0x04")] Eip7702(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7D", alias = "0x7d")] PostExec(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7E", alias = "0x7e")] Deposit(OpDepositReceiptWithBloom), #[serde(rename = "0x76")] @@ -44,7 +47,8 @@ impl FoundryReceiptEnvelope { cumulative_gas_used: u64, logs: impl IntoIterator, tx_type: FoundryTxType, - deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_receipt_version: Option, ) -> Self { let logs = logs.into_iter().collect::>(); @@ -67,9 +71,11 @@ impl FoundryReceiptEnvelope { FoundryTxType::Eip7702 => { Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => { Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { let inner = OpDepositReceiptWithBloom { receipt: OpDepositReceipt { @@ -112,13 +118,18 @@ impl FoundryReceiptEnvelope { removed: false, }) .collect::>(); + #[cfg(feature = "optimism")] + let (deposit_nonce, deposit_receipt_version) = + (self.deposit_nonce(), self.deposit_receipt_version()); + #[cfg(not(feature = "optimism"))] + let (deposit_nonce, deposit_receipt_version) = (None, None); FoundryReceiptEnvelope::::from_parts( self.status(), self.cumulative_gas_used(), logs, self.tx_type(), - self.deposit_nonce(), - self.deposit_receipt_version(), + deposit_nonce, + deposit_receipt_version, ) } } @@ -132,7 +143,9 @@ impl FoundryReceiptEnvelope { Self::Eip1559(_) => FoundryTxType::Eip1559, Self::Eip4844(_) => FoundryTxType::Eip4844, Self::Eip7702(_) => FoundryTxType::Eip7702, + #[cfg(feature = "optimism")] Self::PostExec(_) => FoundryTxType::PostExec, + #[cfg(feature = "optimism")] Self::Deposit(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -158,8 +171,12 @@ impl FoundryReceiptEnvelope { Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)), Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)), Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)), + #[cfg(feature = "optimism")] Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)), - Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(r.map_receipt(|r| r.map_logs(f))), + #[cfg(feature = "optimism")] + Self::Deposit(r) => FoundryReceiptEnvelope::Deposit( + r.map_receipt(|r: OpDepositReceipt| r.map_logs(f)), + ), Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)), } } @@ -182,38 +199,14 @@ impl FoundryReceiptEnvelope { Self::Eip1559(t) => &t.logs_bloom, Self::Eip4844(t) => &t.logs_bloom, Self::Eip7702(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::PostExec(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.logs_bloom, Self::Tempo(t) => &t.logs_bloom, } } - /// Return the receipt's deposit_nonce if it is a deposit receipt. - pub fn deposit_nonce(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_nonce) - } - - /// Return the receipt's deposit version if it is a deposit receipt. - pub fn deposit_receipt_version(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { - match self { - Self::Deposit(t) => Some(t), - _ => None, - } - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { - match self { - Self::Deposit(t) => Some(&t.receipt), - _ => None, - } - } - /// Consumes the type and returns the underlying [`Receipt`]. pub fn into_receipt(self) -> Receipt { match self { @@ -222,8 +215,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => t.receipt.into_inner(), } } @@ -236,8 +231,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => &t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => &t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.receipt.inner, } } @@ -287,7 +284,9 @@ impl Encodable for FoundryReceiptEnvelope { Self::Eip1559(r) => r.length() + 1, Self::Eip4844(r) => r.length() + 1, Self::Eip7702(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::PostExec(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::Deposit(r) => r.length() + 1, Self::Tempo(r) => r.length() + 1, _ => unreachable!("receipt already matched"), @@ -314,11 +313,13 @@ impl Encodable for FoundryReceiptEnvelope { EIP7702_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::PostExec(r) => { Header { list: true, payload_length: payload_len }.encode(out); POST_EXEC_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::Deposit(r) => { Header { list: true, payload_length: payload_len }.encode(out); DEPOSIT_TX_TYPE_ID.encode(out); @@ -371,18 +372,23 @@ impl Decodable for FoundryReceiptEnvelope { buf.advance(1); ::decode(buf) .map(FoundryReceiptEnvelope::Eip7702) - } else if receipt_type == POST_EXEC_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::PostExec) - } else if receipt_type == DEPOSIT_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::Deposit) } else if receipt_type == TEMPO_TX_TYPE_ID { buf.advance(1); ::decode(buf).map(FoundryReceiptEnvelope::Tempo) } else { + #[cfg(feature = "optimism")] + { + if receipt_type == POST_EXEC_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::PostExec); + } + if receipt_type == DEPOSIT_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::Deposit); + } + } Err(alloy_rlp::Error::Custom("invalid receipt type")) } } @@ -404,7 +410,9 @@ impl Typed2718 for FoundryReceiptEnvelope { Self::Eip1559(_) => EIP1559_TX_TYPE_ID, Self::Eip4844(_) => EIP4844_TX_TYPE_ID, Self::Eip7702(_) => EIP7702_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::PostExec(_) => POST_EXEC_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::Deposit(_) => DEPOSIT_TX_TYPE_ID, Self::Tempo(_) => TEMPO_TX_TYPE_ID, } @@ -419,7 +427,9 @@ impl Encodable2718 for FoundryReceiptEnvelope { Self::Eip1559(r) => 1 + r.length(), Self::Eip4844(r) => 1 + r.length(), Self::Eip7702(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::PostExec(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::Deposit(r) => 1 + r.length(), Self::Tempo(r) => 1 + r.length(), } @@ -435,8 +445,10 @@ impl Encodable2718 for FoundryReceiptEnvelope { | Self::Eip1559(r) | Self::Eip4844(r) | Self::Eip7702(r) - | Self::PostExec(r) | Self::Tempo(r) => r.encode(out), + #[cfg(feature = "optimism")] + Self::PostExec(r) => r.encode(out), + #[cfg(feature = "optimism")] Self::Deposit(r) => r.encode(out), } } @@ -444,15 +456,18 @@ impl Encodable2718 for FoundryReceiptEnvelope { impl Decodable2718 for FoundryReceiptEnvelope { fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { - if ty == DEPOSIT_TX_TYPE_ID { - return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + #[cfg(feature = "optimism")] + { + if ty == DEPOSIT_TX_TYPE_ID { + return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + } + if ty == POST_EXEC_TX_TYPE_ID { + return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); + } } if ty == TEMPO_TX_TYPE_ID { return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?)); } - if ty == POST_EXEC_TX_TYPE_ID { - return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); - } match ReceiptEnvelope::typed_decode(ty, buf)? { ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)), ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)), @@ -646,8 +661,11 @@ mod tests { assert!(receipt.status()); assert_eq!(receipt.cumulative_gas_used(), 100000); assert!(receipt.logs().is_empty()); - assert!(receipt.deposit_nonce().is_none()); - assert!(receipt.deposit_receipt_version().is_none()); + #[cfg(feature = "optimism")] + { + assert!(receipt.deposit_nonce().is_none()); + assert!(receipt.deposit_receipt_version().is_none()); + } } #[test] diff --git a/crates/primitives/src/transaction/request.rs b/crates/primitives/src/transaction/request.rs index 2c4dbae8fdcd4..8ae31efbd5cb1 100644 --- a/crates/primitives/src/transaction/request.rs +++ b/crates/primitives/src/transaction/request.rs @@ -3,15 +3,19 @@ use alloy_network::{ BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder, TransactionBuilder4844, TransactionBuilderError, }; -use alloy_primitives::{Address, B256, ChainId, TxKind, U256}; +use alloy_primitives::{Address, ChainId, TxKind, U256}; use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest}; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit}; +#[cfg(feature = "optimism")] use op_revm::transaction::deposit::DepositTransactionParts; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionRequest; use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType}; +#[cfg(feature = "optimism")] +use super::optimism::get_deposit_tx_parts; use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; use crate::FoundryNetwork; @@ -28,6 +32,7 @@ use crate::FoundryNetwork; #[derive(Clone, Debug, PartialEq, Eq)] pub enum FoundryTransactionRequest { Ethereum(TransactionRequest), + #[cfg(feature = "optimism")] Op(WithOtherFields), Tempo(Box), } @@ -44,6 +49,7 @@ impl FoundryTransactionRequest { pub fn into_inner(self) -> TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx.inner, Self::Tempo(tx) => tx.inner, } @@ -55,6 +61,7 @@ impl FoundryTransactionRequest { /// # Returns /// - Ok(deposit_tx_parts) if all necessary keys are present to build a deposit transaction. /// - Err(missing) if some keys are missing to build a deposit transaction. + #[cfg(feature = "optimism")] pub fn get_deposit_tx_parts(&self) -> Result> { match self { Self::Op(tx) => get_deposit_tx_parts(&tx.other), @@ -69,9 +76,11 @@ impl FoundryTransactionRequest { pub fn preferred_type(&self) -> FoundryTxType { match self { Self::Ethereum(tx) => tx.preferred_type().into(), + #[cfg(feature = "optimism")] Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => { FoundryTxType::PostExec } + #[cfg(feature = "optimism")] Self::Op(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -95,6 +104,7 @@ impl FoundryTransactionRequest { /// Check if all necessary keys are present to build a Deposit transaction, returning a list of /// keys that are missing. + #[cfg(feature = "optimism")] pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> { self.get_deposit_tx_parts().map(|_| ()) } @@ -123,7 +133,9 @@ impl FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } { @@ -138,9 +150,10 @@ impl FoundryTransactionRequest { /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction /// types. pub fn build_typed_tx(self) -> Result { + #[cfg(feature = "optimism")] if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() { // Build deposit transaction - Ok(FoundryTypedTx::Deposit(TxDeposit { + return Ok(FoundryTypedTx::Deposit(TxDeposit { from: self.from().unwrap_or_default(), source_hash: deposit_tx_parts.source_hash, to: self.kind().unwrap_or_default(), @@ -149,8 +162,9 @@ impl FoundryTransactionRequest { gas_limit: self.gas_limit().unwrap_or_default(), is_system_transaction: deposit_tx_parts.is_system_transaction, input: self.input().cloned().unwrap_or_default(), - })) - } else if self.complete_tempo().is_ok() + })); + } + if self.complete_tempo().is_ok() && let Self::Tempo(tx_req) = self { // Build Tempo transaction @@ -192,6 +206,7 @@ impl Serialize for FoundryTransactionRequest { { match self { Self::Ethereum(tx) => tx.serialize(serializer), + #[cfg(feature = "optimism")] Self::Op(tx) => tx.serialize(serializer), Self::Tempo(tx) => tx.serialize(serializer), } @@ -211,6 +226,7 @@ impl AsRef for FoundryTransactionRequest { fn as_ref(&self) -> &TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_ref(), } @@ -221,6 +237,7 @@ impl AsMut for FoundryTransactionRequest { fn as_mut(&mut self) -> &mut TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_mut(), } @@ -244,15 +261,16 @@ impl From> for FoundryTransactionRequest { { tempo_tx_req.set_nonce_key(nonce_key); } - Self::Tempo(Box::new(tempo_tx_req)) - } else if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) + return Self::Tempo(Box::new(tempo_tx_req)); + } + #[cfg(feature = "optimism")] + if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID) || get_deposit_tx_parts(&tx.other).is_ok() { - Self::Op(tx) - } else { - Self::Ethereum(tx.into_inner()) + return Self::Op(tx); } + Self::Ethereum(tx.into_inner()) } } @@ -264,6 +282,7 @@ impl From for FoundryTransactionRequest { FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::::into(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => { let other = OtherFields::from_iter([ ("sourceHash", tx.source_hash.to_string().into()), @@ -272,6 +291,7 @@ impl From for FoundryTransactionRequest { ]); WithOtherFields { inner: Into::::into(tx), other }.into() } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(tx) => WithOtherFields { inner: Into::::into(tx), other: OtherFields::default(), @@ -307,8 +327,9 @@ impl From for FoundryTransactionRequest { } } -impl From> for FoundryTransactionRequest { - fn from(tx: op_alloy_rpc_types::Transaction) -> Self { +#[cfg(not(feature = "optimism"))] +impl From> for FoundryTransactionRequest { + fn from(tx: alloy_rpc_types_eth::Transaction) -> Self { tx.inner.into_inner().into() } } @@ -437,7 +458,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.as_ref().complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } @@ -448,9 +471,14 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { } fn can_build(&self) -> bool { - self.as_ref().can_build() - || self.complete_deposit().is_ok() - || self.complete_tempo().is_ok() + if self.as_ref().can_build() || self.complete_tempo().is_ok() { + return true; + } + #[cfg(feature = "optimism")] + if self.complete_deposit().is_ok() { + return true; + } + false } fn output_tx_type(&self) -> FoundryTxType { @@ -465,7 +493,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(), FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit().ok(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => self.complete_type(pref).ok(), FoundryTxType::Tempo => self.complete_tempo().ok(), }?; @@ -479,11 +509,21 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { let inner = self.as_mut(); inner.transaction_type = Some(preferred_type as u8); inner.gas.is_none().then(|| inner.set_gas_limit(Default::default())); - if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) { + let is_deposit = { + #[cfg(feature = "optimism")] + { + preferred_type == FoundryTxType::Deposit + } + #[cfg(not(feature = "optimism"))] + { + false + } + }; + if !is_deposit && preferred_type != FoundryTxType::Tempo { inner.trim_conflicting_keys(); inner.populate_blob_hashes(); } - if preferred_type != FoundryTxType::Deposit { + if !is_deposit { inner.nonce.is_none().then(|| inner.set_nonce(Default::default())); } if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) { @@ -548,42 +588,10 @@ impl TransactionBuilder4844 for FoundryTransactionRequest { } } -/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields -pub fn get_deposit_tx_parts( - other: &OtherFields, -) -> Result> { - let mut missing = Vec::new(); - let source_hash = - other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("sourceHash"); - Default::default() - }, - ); - let mint = other - .get_deserialized::("mint") - .transpose() - .unwrap_or_else(|_| { - missing.push("mint"); - Default::default() - }) - .map(|value| value.to::()); - let is_system_transaction = - other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("isSystemTx"); - Default::default() - }, - ); - if missing.is_empty() { - Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) - } else { - Err(missing) - } -} - #[cfg(test)] mod tests { + use alloy_primitives::B256; + use super::*; fn default_tx_req() -> TransactionRequest { @@ -618,6 +626,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_routing_op_by_deposit_fields() { let tx = default_tx_req(); let mut other = OtherFields::default(); @@ -669,6 +678,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_serialization_op() { let tx = default_tx_req(); let mut other = OtherFields::default(); diff --git a/crates/script-sequence/Cargo.toml b/crates/script-sequence/Cargo.toml index 7f112ce1bbda8..ce94945fb27cf 100644 --- a/crates/script-sequence/Cargo.toml +++ b/crates/script-sequence/Cargo.toml @@ -27,3 +27,7 @@ revm-inspectors.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index acdcbbdbf95ea..6de71571ac7eb 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -62,3 +62,15 @@ tempo-primitives.workspace = true [dev-dependencies] tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-cli/optimism", + "forge-script-sequence/optimism", + "forge-verify/optimism", +] diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 5ac7e4c669ade..f1bda2ccdcf55 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -1,8 +1,8 @@ -use std::{cmp::Ordering, sync::Arc, time::Duration}; +use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration}; use crate::{ - ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress, - sequence::ScriptSequenceKind, verify::BroadcastedState, + ScriptArgs, ScriptConfig, build::LinkedBuildData, needs_script_rpc_estimate, + progress::ScriptProgress, sequence::ScriptSequenceKind, verify::BroadcastedState, }; use alloy_chains::{Chain, NamedChain}; use alloy_consensus::{SignableTransaction, Signed}; @@ -21,11 +21,12 @@ use alloy_signer::Signature; use eyre::{Context, Result, bail}; use forge_verify::provider::VerificationProviderType; use foundry_cheatcodes::Wallets; -use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; +use foundry_cli::utils::has_batch_support; use foundry_common::{ FoundryTransactionBuilder, TransactionMaybeSigned, provider::{ProviderBuilder, try_get_http_provider}, shell, + tempo::TempoSponsor, }; use foundry_config::Config; use foundry_evm::core::evm::{FoundryEvmNetwork, TempoEvmNetwork}; @@ -100,7 +101,17 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result<()> { + let access_key_authorization = match self { + Self::AccessKey(_, _, access_key) => Some(( + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.clone(), + )), + _ => None, + }; + if let Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) @@ -137,11 +148,28 @@ where } } + if let Some((wallet_address, key_address, key_authorization)) = + access_key_authorization.as_ref() + { + tx.prepare_access_key_authorization( + provider, + *wallet_address, + *key_address, + key_authorization.as_ref(), + ) + .await?; + } + // Chains which use `eth_estimateGas` are being sent sequentially and require their // gas to be re-estimated right before broadcasting. if !is_fixed_gas_limit && estimate_via_rpc { estimate_gas(tx, provider, estimate_multiplier).await?; } + + if let Some(sponsor) = tempo_sponsor { + let from = tx.from().expect("no sender"); + sponsor.attach_and_print::(tx, from).await?; + } } Ok(()) @@ -211,6 +239,7 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result { self.prepare( &provider, @@ -218,6 +247,7 @@ where is_fixed_gas_limit, estimate_via_rpc, estimate_multiplier, + tempo_sponsor, ) .await?; @@ -387,6 +417,27 @@ impl BundledState { SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys } }; + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new); + if tempo_sponsor.is_some() && self.script_config.tempo.sponsor_sig.is_some() { + let remaining = self + .sequence + .sequences() + .iter() + .map(|sequence| { + sequence + .transactions() + .skip(sequence.receipts.len()) + .filter(|tx| tx.is_unsigned()) + .count() + }) + .sum::(); + if remaining > 1 { + eyre::bail!( + "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts" + ); + } + } + let progress = ScriptProgress::default(); for i in 0..self.sequence.sequences().len() { @@ -464,6 +515,11 @@ impl BundledState { let kind = match tx_with_metadata.tx().clone() { TransactionMaybeSigned::Signed { tx, .. } => { + if tempo_sponsor.is_some() { + eyre::bail!( + "cannot attach Tempo sponsor signature to an already signed script transaction" + ); + } SendTransactionKind::Signed(tx) } TransactionMaybeSigned::Unsigned(mut tx) => { @@ -487,6 +543,8 @@ impl BundledState { tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); } + self.script_config.tempo.apply::(&mut tx, None); + send_kind.for_sender(&from, tx)? } }; @@ -495,9 +553,13 @@ impl BundledState { }) .collect::>>()?; - let estimate_via_rpc = has_different_gas_calc(sequence.chain) - || self.script_config.evm_opts.networks.is_tempo() - || self.args.skip_simulation; + let estimate_via_rpc = needs_script_rpc_estimate( + sequence.chain, + self.script_config.evm_opts.networks.is_tempo(), + self.script_config.batch, + &self.script_config.tempo, + self.args.skip_simulation, + ); // We only wait for a transaction receipt before sending the next transaction, if // there is more than one signer. There would be no way of assuring @@ -525,6 +587,7 @@ impl BundledState { let pending_transactions = batch.iter().map(|(kind, is_fixed_gas_limit)| { let provider = provider.clone(); + let tempo_sponsor = tempo_sponsor.clone(); async move { let res = kind .clone() @@ -534,22 +597,36 @@ impl BundledState { *is_fixed_gas_limit, estimate_via_rpc, self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), ) .await; - (res, kind, 0, None) + (res, kind, *is_fixed_gas_limit, 0, None) } .boxed() }); let mut buffer = pending_transactions.collect::>(); - 'send: while let Some((res, kind, attempt, original_res)) = - buffer.next().await + 'send: while let Some(( + res, + kind, + is_fixed_gas_limit, + attempt, + original_res, + )) = buffer.next().await { - if res.is_err() && attempt <= 3 { + if res.is_err() + && self.script_config.tempo.sponsor_sig.is_some() + && attempt == 0 + { + debug!( + "not retrying transaction because --tempo.sponsor-sig is a static signature" + ); + } else if res.is_err() && attempt <= 3 { // Try to resubmit the transaction let provider = provider.clone(); let progress = seq_progress.inner.clone(); + let tempo_sponsor = tempo_sponsor.clone(); buffer.push(Box::pin(async move { debug!(err=?res, ?attempt, "retrying transaction "); let attempt = attempt + 1; @@ -557,8 +634,24 @@ impl BundledState { "retrying transaction {res:?} (attempt {attempt})" )); tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - let r = kind.clone().send(provider).await; - (r, kind, attempt, original_res.or(Some(res))) + let r = kind + .clone() + .prepare_and_send( + provider, + sequential_broadcast, + is_fixed_gas_limit, + estimate_via_rpc, + self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), + ) + .await; + ( + r, + kind, + is_fixed_gas_limit, + attempt, + original_res.or(Some(res)), + ) })); continue 'send; @@ -675,6 +768,7 @@ impl BundledState { let sequence = self.sequence.sequences_mut().get_mut(0).unwrap(); let provider = Arc::new(ProviderBuilder::::new(sequence.rpc_url()).build()?); + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?; // Collect sender addresses - batch mode requires single sender let senders: AddressHashSet = sequence @@ -794,16 +888,35 @@ impl BundledState { max_priority_fee_per_gas: Some(max_priority_fee_per_gas), ..Default::default() }, - fee_token: self.script_config.fee_token, + fee_token: self.script_config.tempo.common.fee_token, calls: calls.clone(), + nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX), + valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new), ..Default::default() }; + self.script_config.tempo.apply::(&mut batch_tx, None); + + if let BatchSigner::TempoKeychain(_, ak) = &batch_signer { + batch_tx.key_id = Some(ak.key_address); + batch_tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } // Estimate gas for the batch transaction estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier).await?; sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut batch_tx, sender).await?; + } + // Sign and send let tx_hash = match batch_signer { BatchSigner::Wallet(wallet) => { @@ -816,8 +929,6 @@ impl BundledState { *pending.tx_hash() } BatchSigner::TempoKeychain(signer, access_key) => { - batch_tx.key_id = Some(access_key.key_address); - let raw_tx = batch_tx .sign_with_access_key( provider.as_ref(), diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index f5e4d46de0344..ea906c9b872e1 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -28,8 +28,8 @@ use eyre::{ContextCompat, Result}; use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::{RetryArgs, VerifierArgs}; use foundry_cli::{ - opts::{BuildOpts, EvmArgs, GlobalArgs}, - utils::{LoadConfig, parse_fee_token_address}, + opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts}, + utils::{LoadConfig, has_different_gas_calc}, }; use foundry_common::{ CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN, @@ -44,11 +44,13 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ backend::Backend, core::{ Breakpoints, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, tempo::PATH_USD_ADDRESS, }, executors::ExecutorBuilder, @@ -140,9 +142,9 @@ pub struct ScriptArgs { #[arg(long, requires = "batch", default_value = "100")] pub batch_size: usize, - /// Tempo fee token address for paying transaction fees. - #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] - pub fee_token: Option
, + /// Tempo transaction options. + #[command(flatten)] + pub tempo: TempoOpts, /// Skips on-chain simulation. #[arg(long)] @@ -246,13 +248,43 @@ pub struct ScriptArgs { pub retry: RetryArgs, } +const fn should_default_tempo_fee_token( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + // Plain `--network tempo` should stay an ordinary transaction; only Tempo AA opts get defaults. + is_tempo_network && tempo.common.fee_token.is_none() && (batch || tempo.is_tempo()) +} + +const fn needs_tempo_aa_rpc_estimate( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + is_tempo_network && (batch || tempo.is_tempo()) +} + +pub(crate) fn needs_script_rpc_estimate( + chain_id: u64, + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, + skip_simulation: bool, +) -> bool { + // Tempo AA needs RPC estimation; plain Tempo scripts can use the local simulation result. + (has_different_gas_calc(chain_id) && !is_tempo_network) + || needs_tempo_aa_rpc_estimate(is_tempo_network, batch, tempo) + || skip_simulation +} + impl ScriptArgs { /// Loads config, resolves evm_opts (including network inference from fork), and returns them. async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> { let (config, mut evm_opts) = self.load_config_and_evm_opts()?; - if self.fee_token.is_some() { - // If fee token is set directly select tempo + if self.tempo.is_tempo() { + // If fee token or expiry is set directly select tempo evm_opts.networks = NetworkConfigs::with_tempo(); } else { // Auto-detect network from fork chain ID when not explicitly configured. @@ -285,13 +317,14 @@ impl ScriptArgs { } } - let fee_token = if evm_opts.networks.is_tempo() && self.fee_token.is_none() { - Some(PATH_USD_ADDRESS) - } else { - self.fee_token - }; + let mut tempo = self.tempo.clone(); + tempo.resolve_expires(); + + if should_default_tempo_fee_token(evm_opts.networks.is_tempo(), self.batch, &tempo) { + tempo.common.fee_token = Some(PATH_USD_ADDRESS); + } - let script_config = ScriptConfig::new(config, evm_opts, self.batch, fee_token).await?; + let script_config = ScriptConfig::new(config, evm_opts, self.batch, tempo).await?; Ok(PreprocessedState { args: self, script_config, script_wallets, browser_wallet }) } @@ -320,12 +353,15 @@ impl ScriptArgs { if broadcasted.args.verify { broadcasted.verify().await?; } - Ok(()) - } else if evm_opts.networks.is_optimism() { - self.run_generic_script::(config, evm_opts).await - } else { - self.run_generic_script::(config, evm_opts).await + return Ok(()); } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_generic_script::(config, evm_opts).await; + } + + self.run_generic_script::(config, evm_opts).await } /// Prepares the bundled state (compile, simulate, bundle) and returns it @@ -708,8 +744,8 @@ pub struct ScriptConfig { pub backends: HashMap>, /// Whether to batch all broadcast transactions into a single Tempo batch transaction. pub batch: bool, - /// Tempo fee token address for paying transaction fees. - pub fee_token: Option
, + /// Tempo transaction options applied to broadcast transactions. + pub tempo: TempoOpts, } impl ScriptConfig { @@ -717,7 +753,7 @@ impl ScriptConfig { config: Config, evm_opts: EvmOpts, batch: bool, - fee_token: Option
, + tempo: TempoOpts, ) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await? @@ -726,7 +762,7 @@ impl ScriptConfig { 1 }; - Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, fee_token }) + Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, tempo }) } pub async fn update_sender(&mut self, sender: Address) -> Result<()> { @@ -802,7 +838,7 @@ impl ScriptConfig { self.evm_opts.clone(), Some(known_contracts), Some(target), - self.fee_token, + self.tempo.common.fee_token, ) .into(), ) @@ -813,7 +849,7 @@ impl ScriptConfig { // Propagate fee token to the transaction environment so that internal EVM calls // (e.g. script deployment, setUp) use the correct fee token for Tempo networks. - tx_env.set_fee_token(self.fee_token); + tx_env.set_fee_token(self.tempo.common.fee_token); Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())) } @@ -823,6 +859,7 @@ impl ScriptConfig { mod tests { use super::*; use alloy_network::Ethereum; + use alloy_primitives::address; use foundry_config::{NamedChain, UnresolvedEnvVarError}; use std::fs; use tempfile::tempdir; @@ -834,6 +871,50 @@ mod tests { assert_eq!(args.sig, sig); } + #[test] + fn can_parse_shared_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.fee-token", + "1", + "--tempo.expires", + "10", + ]); + + assert_eq!( + args.tempo.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000001")) + ); + assert_eq!(args.tempo.common.expires, Some(10)); + } + + #[test] + fn can_parse_sponsor_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]); + + assert_eq!( + args.tempo.sponsor, + Some(address!("0x1111111111111111111111111111111111111111")) + ); + assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + } + + #[test] + fn can_parse_full_tempo_opts() { + let args = + ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]); + + assert_eq!(args.tempo.nonce_key, Some(U256::from(1))); + } + #[test] fn can_parse_unlocked() { let args = ScriptArgs::parse_from([ diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index e2404d60ce2b9..b085f8eaf4545 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -6,7 +6,7 @@ use alloy_network::TransactionBuilder; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; -use foundry_common::{FoundryTransactionBuilder, TransactionMaybeSigned}; +use foundry_common::TransactionMaybeSigned; use foundry_config::Config; use foundry_evm::{ constants::CALLER, @@ -84,9 +84,7 @@ impl ScriptRunner { .with_input(code.clone()) .with_nonce(sender_nonce + library_transactions.len() as u64); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), @@ -122,9 +120,7 @@ impl ScriptRunner { .with_nonce(sender_nonce + library_transactions.len() as u64) .with_to(create2_deployer); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index ef10a1ce94082..fe1e9345fa23c 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -129,7 +129,7 @@ impl VerifyBundle { path: Some(artifact.source.to_string_lossy().to_string()), name: artifact .name - .strip_suffix(&format!(".{}", &artifact.profile)) + .strip_suffix(&format!(".{}", artifact.profile)) .unwrap_or_else(|| &artifact.name) .to_string(), }; diff --git a/crates/sol-macro-gen/Cargo.toml b/crates/sol-macro-gen/Cargo.toml index 69ea952d4d040..d3ad56a96cdd2 100644 --- a/crates/sol-macro-gen/Cargo.toml +++ b/crates/sol-macro-gen/Cargo.toml @@ -27,3 +27,7 @@ prettyplease.workspace = true eyre.workspace = true heck.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index d29f6358e93d2..8dc652bcb32bd 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -44,3 +44,7 @@ idna_adapter.workspace = true [dev-dependencies] tokio.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/verify/Cargo.toml b/crates/verify/Cargo.toml index 65a202911509f..e3372a2494f34 100644 --- a/crates/verify/Cargo.toml +++ b/crates/verify/Cargo.toml @@ -48,3 +48,12 @@ url.workspace = true tokio = { workspace = true, features = ["macros"] } foundry-test-utils.workspace = true tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-cli/optimism", +] diff --git a/docs/dev/lintrules.md b/docs/dev/lintrules.md index 6f5dbbd850784..969d7effe142f 100644 --- a/docs/dev/lintrules.md +++ b/docs/dev/lintrules.md @@ -60,6 +60,8 @@ Next, choose whether you want an [early or late lint pass](#choosing-between-ear - Implement the appropriate trait logic (`EarlyLintPass` or `LateLintPass`) for your lint. Do it in a new file within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). +- Add a markdown documentation file for the lint at `crates/lint/docs/.md`. The file is referenced by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the [Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. Use [`crates/lint/docs/_template.md`](../../crates/lint/docs/_template.md) as a starting point. The presence of this file is enforced by the `registered_lints_have_docs` unit test in `crates/lint/src/sol/mod.rs`. + ### Choosing Between Early and Late Passes - **Use `EarlyLintPass`** for: diff --git a/flake.lock b/flake.lock index 27f426f491da6..343a79c0cda3d 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1777102577, - "narHash": "sha256-ycoy9svZOQgyInu/lwO7IEQtlP5liqYhEcF9m9hPRbM=", + "lastModified": 1777708550, + "narHash": "sha256-Qif3UXT0l5OQq8H9pRWt4/ia4gF48MWK2oHKL8uVx8U=", "owner": "nix-community", "repo": "fenix", - "rev": "f37403486c59376cd285f9685a8ef8ff25c09a3c", + "rev": "74c1591efaff494756b8d35ebe357c6c2bbdca96", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1777641297, + "narHash": "sha256-WNGcmeOZ8Tr9dq6ztCspYbzWFswr2mPebM9LpsfGxPk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "c6d65881c5624c9cae5ea6cedef24699b0c0a4c0", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1776800521, - "narHash": "sha256-f8YJfwAOsLFpIoqZuX3yF69UvMLrkx7iVzMH1pJU7cM=", + "lastModified": 1777639980, + "narHash": "sha256-6d7Hdurvbjc5uwJuc0YiK7rZBGj6Gs3uzfBFcTs+xCc=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "8954b66d43225e62c92e8bbcc8500191b5cceb1e", + "rev": "64cdaeb06f69b6b769a492edd88b022ae88e8ca2", "type": "github" }, "original": { diff --git a/foundryup/README.md b/foundryup/README.md index 29e91378929cc..2cf61f8725227 100644 --- a/foundryup/README.md +++ b/foundryup/README.md @@ -30,10 +30,10 @@ To install the latest **nightly** version: foundryup --install nightly ``` -To install a specific version (e.g. `v1.6.0`): +To install a specific version (e.g. `v1.7.0`): ```sh -foundryup --install v1.6.0 +foundryup --install v1.7.0 ``` To **list** all **versions** installed: diff --git a/foundryup/foundryup b/foundryup/foundryup index 7576501b4619e..5fb086a75f8c1 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -3,7 +3,7 @@ set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -FOUNDRYUP_INSTALLER_VERSION="1.8.1" +FOUNDRYUP_INSTALLER_VERSION="1.8.3" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} @@ -15,6 +15,13 @@ FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" FOUNDRYUP_JOBS="" FOUNDRYUP_IGNORE_VERIFICATION=false +# Retry/backoff settings used for `fetch` (GitHub API calls). +# Recovers from transient HTTP 403/429/5xx responses returned by +# api.github.com under heavy load or per-IP rate limiting. +FOUNDRYUP_MAX_RETRIES=5 +FOUNDRYUP_RETRY_DELAY=2 +FOUNDRYUP_RETRY_MAX_TIME=60 + BINS=(forge cast anvil chisel) HASH_NAMES=() HASH_VALUES=() @@ -111,51 +118,7 @@ main() { # Install by downloading binaries if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-latest} - - # Normalize versions (handle channels, versions without v prefix) - if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then - # Resolve to the latest release (non-prerelease) via the GitHub API - say "fetching latest release from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' - /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } - ') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a latest release for ${FOUNDRYUP_REPO}" - fi - say "resolved release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" - elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then - # Resolve to the latest nightly (prerelease) release via the GitHub API. - # The GitHub API does not guarantee that releases are returned in - # chronological order, so we collect all matching nightlies along with - # their `published_at` timestamps and sort them ourselves. - say "fetching latest nightly releases from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' - /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } - /"published_at"[[:space:]]*:[[:space:]]*"/ { - pub=$0 - gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub) - gsub(/".*/, "", pub) - if (tag ~ /^nightly-/) print pub "\t" tag - tag="" - } - ' | sort -r | awk -F '\t' 'NR==1 { print $2 }') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a nightly release for ${FOUNDRYUP_REPO}" - fi - say "resolved nightly release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then - # Specific nightly tag (e.g. nightly-abc123...) - FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then - # Add v prefix - FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - else - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - fi + resolve_version_and_tag say "installing foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})" @@ -179,7 +142,7 @@ main() { tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp_dir/attestation.txt" ensure download "$ATTESTATION_URL" "$tmp" - + # Read the first line of the attestation file to get the artifact link. # The first line should contain the link to the attestation artifact. attestation_artifact_link="$(head -n1 "$tmp" | tr -d '\r')" @@ -292,7 +255,7 @@ main() { else say 'skipping manpage download: missing "tar"' fi - + if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = true ]; then say "skipped SHA verification for downloaded binaries due to --force flag" else @@ -505,6 +468,18 @@ list() { use() { [ -z "$FOUNDRYUP_VERSION" ] && err "no version provided" + + # If the requested version is a channel (`latest`, `stable`, `nightly`) or a bare semver + # version (e.g. `1.7.0`, `1.6.0-rc1`), resolve it to the immutable tag directory created by + # `--install` (channels hit the GitHub API; semver versions get a `v` prefix). + # Falls back to the literal value for locally-built versions (branches, PRs, commits, custom names). + case "$FOUNDRYUP_VERSION" in + latest|stable|nightly|[0-9]*.[0-9]*.[0-9]*) + resolve_version_and_tag + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + ;; + esac + FOUNDRY_VERSION_DIR="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" if [ -d "$FOUNDRY_VERSION_DIR" ]; then @@ -683,12 +658,70 @@ ensure() { if ! "$@"; then err "command failed: $*"; fi } -# Silently fetches $1 to stdout +# Normalizes `FOUNDRYUP_VERSION` and resolves it to a concrete release tag, +# populating `FOUNDRYUP_TAG`. Handles the `latest`/`stable`/`nightly` channels +# (looked up via the GitHub API). +resolve_version_and_tag() { + FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} + if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then + # Resolve to the latest release (non-prerelease) via the GitHub API. + say "fetching latest release tag from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' + /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } + ') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a latest release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then + # Resolve to the latest nightly (prerelease) release via the GitHub API. + # The GitHub API does not guarantee that releases are returned in + # chronological order, so we collect all matching nightlies along with + # their `published_at` timestamps and sort them ourselves. + say "fetching latest nightly release tags from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' + /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } + /"published_at"[[:space:]]*:[[:space:]]*"/ { + pub=$0 + gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub) + gsub(/".*/, "", pub) + if (tag ~ /^nightly-/) print pub "\t" tag + tag="" + } + ' | sort -r | awk -F '\t' 'NR==1 { print $2 }') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a nightly release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved nightly release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then + # Specific nightly tag (e.g. nightly-abc123...) + FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then + # Add v prefix + FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + else + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + fi +} + +# Silently fetches $1 to stdout. fetch() { if check_cmd curl; then - curl -fsSL "$1" + curl -fsSL \ + --retry "$FOUNDRYUP_MAX_RETRIES" \ + --retry-delay "$FOUNDRYUP_RETRY_DELAY" \ + --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \ + --retry-all-errors \ + "$1" else - wget -qO- "$1" + wget --tries="$FOUNDRYUP_MAX_RETRIES" \ + --waitretry="$FOUNDRYUP_RETRY_DELAY" \ + --retry-on-http-error=403,408,429,500,502,503,504 \ + -qO- "$1" fi } diff --git a/testdata/default/cheats/ExpectRevert.t.sol b/testdata/default/cheats/ExpectRevert.t.sol index 839d97962aa94..ae0c8ed844f5d 100644 --- a/testdata/default/cheats/ExpectRevert.t.sol +++ b/testdata/default/cheats/ExpectRevert.t.sol @@ -305,6 +305,91 @@ contract ExpectRevertWithReverterTest is Test { vm.expectRevert(address(cContract)); aContract.createDContractThroughCContract(); } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // reverts directly, the reverter address argument must be enforced (it used to + // be silently ignored). The matched reverter is the would-be-deployed address. + function testExpectRevertsWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new DContract(); + + expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(abi.encodePacked("Reverted by DContract"), expected); + new DContract(); + } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // synchronously creates another contract that reverts (i.e. innermost frame is + // a CREATE), the matched reverter is the outer would-be-deployed address (the + // contract whose deployment failed). + function testExpectRevertsWithReverterNestedCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new NestedDContractCreator(); + } + + // + // Regression: `expectPartialRevert(bytes4, address)` overload must enforce + // the reverter address argument when matching a top-level CREATE revert. + function testExpectPartialRevertWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + // `Reverted by DContract` triggers Solidity's `Error(string)` selector. + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), expected); + new DContract(); + } + + // + // Regression: `expectRevert(bytes4, address)` (exact 4-byte selector + reverter) + // overload must enforce the reverter address argument for a top-level CREATE. + function testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(DCustomErrorContract.CustomError.selector, expected); + new DCustomErrorContract(); + } + + // + // Regression: `expectRevert(address, uint64)` count-bearing overload must + // exercise the `count > 1` branch in `create_end`. Use CREATE2 with the same + // salt so both deploys would resolve to the same would-be address (each + // constructor reverts so no contract is ever actually placed there). + function testExpectRevertsWithReverterCountTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0x42)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected, 2); + new DContract{salt: salt}(); + new DContract{salt: salt}(); + } + + // + // Regression: CREATE2 deploys must also enforce the reverter address argument. + function testExpectRevertsWithReverterTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0xC0FFEE)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected); + new DContract{salt: salt}(); + } +} + +// Used by `testExpectRevertsWithReverterNestedCreate`: a contract whose constructor +// directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } +} + +// Used by `testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate`: constructor +// reverts with a parameter-less custom error so the full revert data is exactly the +// 4-byte selector. +contract DCustomErrorContract { + error CustomError(); + + constructor() { + revert CustomError(); + } } contract ExpectRevertCount is Test { diff --git a/testdata/default/cheats/GetFoundryVersion.t.sol b/testdata/default/cheats/GetFoundryVersion.t.sol index 6139b8b6b6a5e..f01b7cdd7d213 100644 --- a/testdata/default/cheats/GetFoundryVersion.t.sol +++ b/testdata/default/cheats/GetFoundryVersion.t.sol @@ -84,4 +84,55 @@ contract GetFoundryVersionTest is Test { // Should return true for past versions assertTrue(vm.foundryVersionAtLeast("0.2.0")); } + + /// Returns the `MAJOR.MINOR.PATCH` prefix of `vm.getFoundryVersion()`, + /// stripping any pre-release suffix (`-nightly`, `-dev`, …) and the + /// `+..` build metadata. + function _semverPrefix() internal view returns (string memory) { + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + require(plusSplit.length == 2, "Invalid version format: Missing '+' separator"); + string[] memory dashSplit = vm.split(plusSplit[0], "-"); + return dashSplit[0]; + } + + function testGetFoundryVersionMajorMinorPatchIsParseable() public view { + // The MAJOR.MINOR.PATCH prefix must always be three numeric components, + // regardless of build kind (tagged release / nightly / dev). + string[] memory parts = vm.split(_semverPrefix(), "."); + require(parts.length == 3, "Invalid semver prefix: expected MAJOR.MINOR.PATCH"); + // Each component must parse as a uint (this reverts on garbage). + vm.parseUint(parts[0]); + vm.parseUint(parts[1]); + vm.parseUint(parts[2]); + } + + function testGetFoundryVersionBuildProfile() public view { + // The build profile must be present and non-empty (e.g. "debug", "release", "dist", …). + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + string[] memory metadataComponents = vm.split(plusSplit[1], "."); + require(bytes(metadataComponents[2]).length > 0, "Build profile is empty"); + } + + function testFoundryVersionCmpAndAtLeastAreConsistent() public { + // `foundryVersionAtLeast(v)` must equal `foundryVersionCmp(v) >= 0` for any input. + string[3] memory probes = ["0.0.1", _semverPrefix(), "99.0.0"]; + for (uint256 i = 0; i < probes.length; i++) { + assertEq(vm.foundryVersionAtLeast(probes[i]), vm.foundryVersionCmp(probes[i]) >= 0); + } + } + + function testFoundryVersionCmpRejectsPreRelease() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0-nightly"); + } + + function testFoundryVersionCmpRejectsBuildMetadata() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0+abc1234567.1700000000.release"); + } + + function testFoundryVersionCmpRejectsInvalidVersion() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("not-a-version"); + } } diff --git a/testdata/default/cheats/MockCall.t.sol b/testdata/default/cheats/MockCall.t.sol index e2ac74d6f70fa..d8019ab4f6ee8 100644 --- a/testdata/default/cheats/MockCall.t.sol +++ b/testdata/default/cheats/MockCall.t.sol @@ -158,6 +158,35 @@ contract MockCallTest is Test { assertEq(mock.pay{value: 50}(1), 100); } + function testMockCallWithValueTransfersBalance() public { + Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + assertEq(address(mock).balance, 0); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + } + + function testMockCallWithValueTransfersPrankedSenderBalance() public { + Mock mock = new Mock(); + address sender = address(0xBEEF); + uint256 value = 10; + vm.deal(address(this), 0); + vm.deal(sender, value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + vm.prank(sender); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + assertEq(sender.balance, 0); + } + function testMockCallWithValueCalldataPrecedence() public { Mock mock = new Mock(); @@ -279,17 +308,25 @@ contract MockCallRevertTest is Test { function testMockCallRevertWithValue() public { Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); - vm.mockCallRevert(address(mock), 10, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); + vm.mockCallRevert(address(mock), value, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); assertEq(mock.pay(1), 1); assertEq(mock.pay(2), 2); - try mock.pay{value: 10}(1) { + uint256 initSenderBalance = address(this).balance; + uint256 initTargetBalance = address(mock).balance; + + try mock.pay{value: value}(1) { revert(); } catch (bytes memory err) { require(keccak256(err) == keccak256(ERROR_MESSAGE)); } + + assertEq(address(this).balance, initSenderBalance); + assertEq(address(mock).balance, initTargetBalance); } function testMockCallResetsMockCallRevert() public { diff --git a/testdata/default/cheats/MockCalls.t.sol b/testdata/default/cheats/MockCalls.t.sol index e0f5eef151db6..777543f28e361 100644 --- a/testdata/default/cheats/MockCalls.t.sol +++ b/testdata/default/cheats/MockCalls.t.sol @@ -28,13 +28,17 @@ contract MockCallsTest is Test { mocks[0] = abi.encode(2 ether); mocks[1] = abi.encode(1 ether); mocks[2] = abi.encode(6.423 ether); + vm.deal(address(this), 3 ether); vm.mockCalls(mockErc20, 1 ether, data, mocks); (, bytes memory ret1) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret1, (uint256)), 2 ether); + assertEq(mockErc20.balance, 1 ether); (, bytes memory ret2) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret2, (uint256)), 1 ether); + assertEq(mockErc20.balance, 2 ether); (, bytes memory ret3) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret3, (uint256)), 6.423 ether); + assertEq(mockErc20.balance, 3 ether); } function testMockCalls() public { From b2931c358e7d33b11c12886b6c20808fbbe43f50 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 07:29:33 +0700 Subject: [PATCH 10/14] Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 53a505774ac88..edd3e4a15ddbc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,6 +25,8 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] + - Browser [e.g. Chrome, Safari] + - Version [e.g. 22] - Browser [e.g. Chrome, safari] - Version [e.g. 22] From fc617f4b1e82ecceb1f4850d373e8d8373b5f38e Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 08:36:51 +0700 Subject: [PATCH 11/14] Tempo signer lookup and access key signing (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix formatting in cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix indentation for on_fail condition in CI config Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix indentation in CircleCI configuration Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.21 to 2.62.31 (#139) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.21 to 2.62.31. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/v2.62.21...0005e0116e92d8489d8d96fbff83f061c79ba95a) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.31 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github/codeql-action from 3 to 4 (#138) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump snyk/actions Bumps [snyk/actions](https://github.com/snyk/actions) from 14818c4695ecc4045f33c9cee9e795a788711ca4 to 9adf32b1121593767fc3c057af55b55db032dc04. - [Release notes](https://github.com/snyk/actions/releases) - [Commits](https://github.com/snyk/actions/compare/14818c4695ecc4045f33c9cee9e795a788711ca4...9adf32b1121593767fc3c057af55b55db032dc04) --- updated-dependencies: - dependency-name: snyk/actions dependency-version: 9adf32b1121593767fc3c057af55b55db032dc04 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Update CircleCI config with comments and formatting Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update and rename ci-say-hello.yml to ci-web3-defi-gamefi.yml (#154) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci-web3-defi-gamefi.yml (#155) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_deploy.yml (#158) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/cargo.yml (#159) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.31 to 2.62.33 (#162) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.31 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/0005e0116e92d8489d8d96fbff83f061c79ba95a...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump actions/checkout from 4 to 5 (#163) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Merge branch 'foundry-rs:master' (#164) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * feat(forge): add bypass prevrandao (#12125) * feat(forge): add bypass prevrandao * Update crates/evm/networks/src/lib.rs Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * changes after review: remove duped code --------- Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * fix(fmt): filter libs when recursing (#12119) * fix(fmt): account for ternary operators when estimating size * fix(fmt): filter libs when recursing * style: clippy * test: wipe contracts before formatting * test: explicitly test ignore * fix(fmt): break try stmts in a fn header-like fashion (#12131) * chore(deps): bump softprops/action-gh-release from 2.3.4 to 2.4.1 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.4 to 2.4.1. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...6da8fa9354ddfdc4aeace5fc48d7f679b5214090) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * chore(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#161) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.28 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/e7ef886cf8f69c25ecef6bbc2858a42e273496ec...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: dependabot[bot] Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(anvil): always disable nonce check (foundry-rs#12144) (#165) * test: refactor testdata/ tests to be run in `forge test` (#12049) * test: run forge test on testdata/ * chore: refactor to use common Test contract * chore: disable testGasMeteringExternal, via-ir * test: rm unused repros * fix: paths * upd * fmt * fix more tests * test: turn testNonExistingContractRevert into expectRevert * fix some more paths * legacy assertions * compile paris with paris * fix: set configs for fs tests * fix remaining paths in cheats * restrict fs permissions * fix: set runtime evm_version too * fix vyper * fix: a couple of repros * fix: we have storage layouts * fix: 3223, 3674: set sender * reorder * feat: move repros expected failures to snapshots * feat: migrate remaining repros tests * feat: rm migrated files * skip testRevertIfGetUnlinked * move expected core/ failures * upd * move logs/ * move all forgetest tests from it/ to cli/ * fix fork test * move trace/ * tmp: move fuzz/invariant out of fuzz/ * move fuzz/ * forge fmt * wips * fix: both vyper and paris; set src/ * canon * lib log * logs * Revert "fix: set runtime evm_version too" This reverts commit 7ca544b10047f608d57c74fb3500a5fbe7e2650e. Contract-level inline config will set evm version for libraries too, which means we fail on deploying libraries that are compiled with newer evm version. * fix: set evm version where needed, per test function * test: reduce gas wastage * chore: clippy * invariant mod.rs * test: fix linking tests with new utils * redact_with * Revert "wips" This reverts commit ee2c17a3023ca7ce8e7effccf0ea0a0f28f6e510. * migrate invariant/target{,Abi} * migrate InvariantAfterInvariant.t.sol * migrate InvariantAssume.t.sol * migrate InvariantCalldataDictionary.t.sol, more test utils * migrate InvariantCustomError.t.sol * migrate InvariantExcludedSenders.t.sol * migrate InvariantFixtures.t.sol * migrate InvariantHandlerFailure.t.sol * interlude: forgot to use a new file * migrate InvariantInnerContract.t.sol * migrate InvariantPreserveState.t.sol * migrate InvariantReentrancy.t.sol * migrate InvariantRollFork.t.sol * migrate InvariantScrapeValues.t.sol * migrate InvariantSequenceNoReverts.t.sol * migrate InvariantShrinkBigSequence.t.sol * migrate InvariantShrinkFailOnRevert.t.sol * migrate InvariantShrinkWithAssert.t.sol * migrate InvariantTest1.t.sol * fix InvariantInnerContract.t.sol * update new Rlp test * com * better com * nuke tests/it * test: fix testdata paths in script tester * test: fix relative paths in test_cmd * test: redact more in issue_2851 * fix: copy testdata correctly * trace addrs * manual retry logic with --retry * fix nondeterministic output * debug: fs lock error context * test: fix project root for windows * test: skip project root test if unset * normalize both * typo * Revert "typo" This reverts commit 402bea105c6f38b82664b50ca854f95e456df795. * Revert "debug: fs lock error context" This reverts commit e5caeddd1e4cb457d7b24d7d7fdfdb370e2feabf. * fix * fix: locked_write_line for windows * chore: clippy * fmt * chore: speed up fuzzed_selected_targets * other way * fix nondeterministic output 2 * fix: disable persistence * test: revert old via-ir * ci: tweak cache key * do not run trace test when isolate --------- Co-authored-by: grandizzy * fix(anvil): always disable nonce check (#12144) * deps: bump deps (#12149) * deps: bump deps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * minimum Cargo.lock --------- Co-authored-by: rplusq Co-authored-by: Claude Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --------- Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: grandizzy Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: Rafael Quintero <32346241+rplusq@users.noreply.github.com> Co-authored-by: rplusq Co-authored-by: Claude * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#167) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#168) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#171) CI/CD Configuration Update: The CircleCI configuration file, cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring the CI pipeline utilizes a more recent Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#173) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#174) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#175) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.28 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/v2.62.28...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Delete .circleci/cargo.yml (#179) I Configuration Removal: The .circleci/cargo.yml file, which defined CircleCI jobs for building and testing Rust projects, has been completely removed from the repository. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#182) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#183) Configuration File Cleanup: Removed an unnecessary blank line in the .circleci/config.yml file, improving its formatting and readability. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#187) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci directory Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci_v1.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Rust Docker image version to 1.89.0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump alloy-dyn-abi in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [alloy-dyn-abi](https://github.com/alloy-rs/core). Updates `alloy-dyn-abi` from 0.8.25 to 0.8.26 - [Release notes](https://github.com/alloy-rs/core/releases) - [Changelog](https://github.com/alloy-rs/core/blob/v0.8.26/CHANGELOG.md) - [Commits](https://github.com/alloy-rs/core/compare/v0.8.25...v0.8.26) --- updated-dependencies: - dependency-name: alloy-dyn-abi dependency-version: 0.8.26 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] * Create ci-web3-gamefi.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 83: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 93: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create codeql.yml (#208) * Update ci.yml (#209) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- https://github.com/apps/gemini-code-assist Code Review This pull request updates the Rust version in the CI from 1.88.0 to 1.89.0. While this is a good maintenance step, I've identified a potential improvement for your CI configuration. The project's Cargo.toml specifies a Minimum Supported Rust Version (MSRV) of 1.86, but the CI doesn't test against it. I've added a comment suggesting the addition of an MSRV check to prevent compatibility issues. * Update cargo.yml (#210) https://github.com/apps/gemini-code-assist ------------------- Code Review This pull request downgrades the Rust version in the CI pipeline from 1.88.0 to 1.87.0. This is inconsistent with the project's declared Minimum Supported Rust Version (MSRV) of 1.89 in Cargo.toml. My review highlights this discrepancy and suggests aligning the CI's Rust version with the MSRV to ensure the project's compatibility guarantees are properly tested. --------------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Foundry rs maste 1f4b36a (#214) * Create jekyll.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 58: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/docker-image.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "chore: fix isolate tests (#10344)" This reverts commit 70ded2b35f95ee9b4ee94f5e44961914d30a87f7. * Delete .github/workflows/jekyll.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update and rename docker-image.yml to docker.yml (#218) Streamline the Docker CI workflow by renaming the file and enhancing it with scheduled runs, Buildx multi-platform builds, metadata tagging, conditional pushes, and automated image signing with Cosign. CI: Rename and replace the legacy docker-image.yml workflow with docker.yml Add scheduled cron runs and triggers on pushes to master, semver tags, and PRs Configure Docker Buildx for multi-platform builds with cache Extract Docker metadata and conditionally push images to GHCR on non-PR events Install Cosign and sign published Docker images using ephemeral identity tokens Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create docker-image.yml (#224) CI: Introduce docker-image.yml GitHub Actions workflow to checkout code and build Docker image on ubuntu-latest Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#225) CI: Insert comment lines to delineate and structure sections in .circleci/config.yml for enhanced clarity Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update sequence.rs (#226) Enhancements: Add standalone # lines in sequence.rs to serve as hidden placeholders for rustdoc examples Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#227) * Update dependencies.yml Refactor the weekly dependencies workflow to inline cargo update steps, auto-generate commit messages and PR bodies with update logs, and use the create-pull-request action to open update PRs on a dedicated branch. Enhancements: Define environment variables for GitHub token, branch name, PR title, and PR body including cargo update logs Inline checkout, Rust toolchain setup, and cargo update command with log cleanup instead of relying on an external workflow Craft commit messages and PR bodies dynamically by capturing and formatting cargo update output Use peter-evans/create-pull-request to push Cargo.lock updates to a 'cargo-update' branch CI: Move permissions and GitHub token configuration into the job context Explicitly set the runner to ubuntu-latest and remove the top-level empty permissions block Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/dependencies.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update npm.yml (#228) CI: Add comment to the Publish Binary step indicating it runs automatically after a successful release workflow or can be triggered manually with a run_id Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update snyk-container.yml (#229) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update nextest.yml (#230) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update const.ts (#231) Code Formatting: Removed an extraneous blank line in npm/src/const.ts to improve code cleanliness and consistency. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "Create web3_defi_gamefi.yml (#61)" (#233) This reverts commit 8575916b7675f246b54daf70cfddccb3f5b97fb0. * Create deploy.yml (#240) * Create deploy.yml CI: Add GitHub Actions workflow to build the Rust project, run tests, and build a Docker image on pushes to main/master Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 106: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#247) Improve readability of the GitHub Actions dependencies workflow by adjusting whitespace and adding blank lines CI: Add blank line before the workflow name declaration Insert blank line after the scheduled cron job entry Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#248) CI: Remove extraneous blank line in .github/workflows/dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#249) CI: Remove dev branch from test workflow triggers Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.lock (#253) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.lock (#254) Chores: Regenerate Cargo.lock to update dependencies Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#255) * Create config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/config.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update config.yml (#256) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix: upgrade tsdown from 0.15.12 to 0.16.1 Snyk has created this PR to upgrade tsdown from 0.15.12 to 0.16.1. See this package in npm: tsdown See this project in Snyk: https://app.snyk.io/org/dargon789/project/8da85645-409e-46fa-bd46-9b58e7905fb8?utm_source=github-cloud-app&utm_medium=referral&page=upgrade-pr * Create google.yml (#266) CI: Introduce a Google Cloud deployment workflow that builds a Docker image, pushes it to Artifact Registry, and deploys it to a GKE cluster on pushes to the main branches. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update flake.lock (#269) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update flake.nix (#270) Adjust Nix flake development shell configuration for better cross-platform support and simplify dependencies. Enhancements: Remove the dprint dependency from the Nix development shell. Add conditional AppKit framework linkage on Darwin systems in the Nix shell configuration. Drop custom hardeningDisable settings from the Nix development shell definition. https://github.com/apps/gemini-code-assist Code Review This pull request updates the Nix flake configuration to improve cross-platform support and simplify dependencies. The changes include removing dprint and hardeningDisable settings, and conditionally adding the AppKit framework for Darwin systems. While most changes are beneficial, removing dprint from the development shell dependencies while its configuration file remains could cause issues for contributors. I've added a comment regarding this potential inconsistency. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.toml (#271) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update nextest.toml (#272) Adjust test runner configuration for nextest to better handle long-running and specific tests. Enhancements: Introduce a dedicated test group that limits chisel-serial tests to a single thread. Increase the default slow-test timeout period to reduce premature terminations for longer-running tests. Expand the slow-timeout override filter to include both ext_integration and can_test_forge_std tests. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dprint.json (#273) (https://github.com/apps/gemini-code-assist) Code Review This pull request updates the dprint.json configuration file. The changes correctly enable formatting for dprint.json itself by modifying the excludes list, update the JSON and Markdown dprint plugins to their latest versions, and add a final newline to the file for POSIX compliance. These are all good maintenance improvements. The changes have been reviewed and appear to be correct and beneficial. No issues were found. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/apisec-scan.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update counter/README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Dependabot/cargo/cargo 38744a1864 (#282) * chore(deps): bump alloy-dyn-abi in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [alloy-dyn-abi](https://github.com/alloy-rs/core). Updates `alloy-dyn-abi` from 0.8.25 to 0.8.26 - [Release notes](https://github.com/alloy-rs/core/releases) - [Changelog](https://github.com/alloy-rs/core/blob/v0.8.26/CHANGELOG.md) - [Commits](https://github.com/alloy-rs/core/compare/v0.8.25...v0.8.26) --- updated-dependencies: - dependency-name: alloy-dyn-abi dependency-version: 0.8.26 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] * Update and rename ci.yml to cargo.yml (#268) Update CircleCI configuration to use a different Rust toolchain image and rename the workflow file. Build: Rename the CircleCI configuration file from ci.yml to cargo.yml. Change the CircleCI Docker image to use Rust 1.78.0 instead of 1.88.0. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Update config.yml (#283) Summary by Sourcery Update CircleCI pipeline to use a custom Docker executor and job tailored to the project instead of the example hello-world workflow. Enhancements: Introduce a reusable custom executor that pulls from the stable cimg/base Docker image with Docker Hub authentication. CI: Replace the sample say-hello job and workflow with a project-specific job and workflow wired to the new custom executor in .circleci/config.yml. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix: use network-specific BaseFeeParams for Optimism in Anvil * Dargon789 patch 1 (#285) * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#167) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#173) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#174) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 83: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 93: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#210) https://github.com/apps/gemini-code-assist ------------------- Code Review This pull request downgrades the Rust version in the CI pipeline from 1.88.0 to 1.87.0. This is inconsistent with the project's declared Minimum Supported Rust Version (MSRV) of 1.89 in Cargo.toml. My review highlights this discrepancy and suggests aligning the CI's Rust version with the MSRV to ensure the project's compatibility guarantees are properly tested. --------------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Gengar * merge gh-master (#287) * Create config.yml (#236) Create .circleci/config.yml defining a version 2.1 pipeline with a docker-based "say-hello" job, checkout and echo steps, and a workflow to orchestrate it Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix(evm): use timestamp-based blob base fee calculation (#12959) * fix(evm): use timestamp-based blob base fee calculation * chore: use patch * Now BPO1 is default * bump to hardforks to 0.4.7 --------- Co-authored-by: Matthias Seitz * fix(config): reject bare versions in compilation restrictions (#12955) fmt Co-authored-by: tefyosL-sol * Revert "fix(config): err on unknown profile (#12946)" (#12964) This reverts commit 6ff4b52e2e572e93d0cd81591b1bd0e6ad9ed507. * Update crates/config/src/compilation.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: cakevm Co-authored-by: Matthias Seitz Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Foundry/ethereum ux (#284) * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 61: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#105) * Create cargo.yml (#106) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .github/workflows/docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "Create cargo.yml (#106)" This reverts commit 251a2b4fce0c50e3426ffb2022d9abef5b948fa9. * Create cargo.yml (#213) https://github.com/apps/gemini-code-assist Code Review This pull request introduces a CircleCI workflow to automate formatting checks and tests. My review has identified two main issues in the configuration: redundant steps that would unnecessarily increase job execution time, and a mismatch between the Rust version in the CI environment and the one specified in the project's Cargo.toml. I've provided suggestions to fix these issues for a more efficient and consistent CI process. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Gamefi defi (#288) * chore: ignore RUSTSEC-2025-0137 (#12941) Co-authored-by: Claude * chore(deps): weekly `cargo update` (#12940) * chore(deps): weekly `cargo update` Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar.git` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git repository `https://github.com/paradigmxyz/reth` Locking 71 packages to latest compatible versions Updating alloy-chains v0.2.23 -> v0.2.24 Updating alloy-consensus v1.1.3 -> v1.2.1 Updating alloy-consensus-any v1.1.3 -> v1.2.1 Updating alloy-contract v1.1.3 -> v1.2.1 Updating alloy-dyn-abi v1.5.1 -> v1.5.2 Updating alloy-eip5792 v1.1.3 -> v1.2.1 Updating alloy-eips v1.1.3 -> v1.2.1 Updating alloy-ens v1.1.3 -> v1.2.1 Updating alloy-genesis v1.1.3 -> v1.2.1 Updating alloy-json-abi v1.5.1 -> v1.5.2 Updating alloy-json-rpc v1.1.3 -> v1.2.1 Updating alloy-network v1.1.3 -> v1.2.1 Updating alloy-network-primitives v1.1.3 -> v1.2.1 Updating alloy-primitives v1.5.1 -> v1.5.2 Updating alloy-provider v1.1.3 -> v1.2.1 Updating alloy-pubsub v1.1.3 -> v1.2.1 Updating alloy-rpc-client v1.1.3 -> v1.2.1 Updating alloy-rpc-types v1.1.3 -> v1.2.1 Updating alloy-rpc-types-anvil v1.1.3 -> v1.2.1 Updating alloy-rpc-types-any v1.1.3 -> v1.2.1 Updating alloy-rpc-types-beacon v1.1.3 -> v1.2.1 Updating alloy-rpc-types-debug v1.1.3 -> v1.2.1 Updating alloy-rpc-types-engine v1.1.3 -> v1.2.1 Updating alloy-rpc-types-eth v1.1.3 -> v1.2.1 Updating alloy-rpc-types-trace v1.1.3 -> v1.2.1 Updating alloy-rpc-types-txpool v1.1.3 -> v1.2.1 Updating alloy-serde v1.1.3 -> v1.2.1 Updating alloy-signer v1.1.3 -> v1.2.1 Updating alloy-signer-aws v1.1.3 -> v1.2.1 Updating alloy-signer-gcp v1.1.3 -> v1.2.1 Updating alloy-signer-ledger v1.1.3 -> v1.2.1 Updating alloy-signer-local v1.1.3 -> v1.2.1 Updating alloy-signer-trezor v1.1.3 -> v1.2.1 Updating alloy-signer-turnkey v1.1.3 -> v1.2.1 Updating alloy-sol-macro v1.5.1 -> v1.5.2 Updating alloy-sol-macro-expander v1.5.1 -> v1.5.2 Updating alloy-sol-macro-input v1.5.1 -> v1.5.2 Updating alloy-sol-type-parser v1.5.1 -> v1.5.2 Updating alloy-sol-types v1.5.1 -> v1.5.2 Updating alloy-transport v1.1.3 -> v1.2.1 Updating alloy-transport-http v1.1.3 -> v1.2.1 Updating alloy-transport-ipc v1.1.3 -> v1.2.1 Updating alloy-transport-ws v1.1.3 -> v1.2.1 Updating alloy-trie v0.9.1 -> v0.9.2 Updating alloy-tx-macros v1.1.3 -> v1.2.1 Unchanged annotate-snippets v0.12.5 (available: v0.12.10) Unchanged anstyle-svg v0.1.11 (available: v0.1.12) Downgrading aws-smithy-runtime v1.9.6 -> v1.9.5 Updating axum-core v0.5.5 -> v0.5.6 Updating cc v1.2.50 -> v1.2.51 Updating derive_more v2.1.0 -> v2.1.1 Updating derive_more-impl v2.1.0 -> v2.1.1 Updating dtoa v1.0.10 -> v1.0.11 Updating find-msvc-tools v0.1.5 -> v0.1.6 Unchanged generic-array v0.14.7 (available: v0.14.9) Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating itoa v1.0.15 -> v1.0.17 Updating jiff v0.2.16 -> v0.2.17 Updating jiff-static v0.2.16 -> v0.2.17 Updating libredox v0.1.11 -> v0.1.12 Updating libz-rs-sys v0.5.4 -> v0.5.5 Unchanged matchit v0.8.4 (available: v0.8.6) Unchanged mdbook v0.4.52 (available: v0.5.2) Updating portable-atomic v1.12.0 -> v1.13.0 Updating proc-macro2 v1.0.103 -> v1.0.104 Unchanged protobuf v3.3.0 (available: v3.7.2) Unchanged protobuf-support v3.3.0 (available: v3.7.2) Unchanged rand v0.8.5 (available: v0.9.2) Unchanged ratatui v0.29.0 (available: v0.30.0) Updating reqwest v0.12.26 -> v0.12.28 Updating ruint v1.17.0 -> v1.17.1 Updating rustix v1.1.2 -> v1.1.3 Updating ryu v1.0.21 -> v1.0.22 Updating schemars v1.1.0 -> v1.2.0 Updating schemars_derive v1.1.0 -> v1.2.0 Updating serde_json v1.0.145 -> v1.0.148 Updating signal-hook-registry v1.4.7 -> v1.4.8 Updating syn-solidity v1.5.1 -> v1.5.2 Updating tempfile v3.23.0 -> v3.24.0 Unchanged trezor-client v0.1.4 (available: v0.1.5) Unchanged unicode-width v0.2.0 (available: v0.2.2) Unchanged vergen v8.3.2 (available: v9.0.6) Updating zlib-rs v0.5.4 -> v0.5.5 Adding zmij v1.0.0 note: to see how you depend on a package, run `cargo tree --invert @` * touchups * touchups --------- Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: Matthias Seitz * Update flake.lock (#12939) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/16642c5' (2025-12-20) → 'github:nix-community/fenix/3479aaf' (2025-12-27) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/ea1d299' (2025-12-18) → 'github:rust-lang/rust-analyzer/8c5a68e' (2025-12-26) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/7d853e5' (2025-12-19) → 'github:NixOS/nixpkgs/3edc4a3' (2025-12-27) Co-authored-by: github-actions[bot] Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * fix(chisel): uninitalized variables (#12937) * chore(deps): bump Swatinem/rust-cache from 2.8.1 to 2.8.2 (#12919) Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/swatinem/rust-cache/releases) - [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md) - [Commits](https://github.com/swatinem/rust-cache/compare/f13886b937689c021905a6b90929199931d60db1...779680da715d629ac1d338a641029a2f4372abb5) --- updated-dependencies: - dependency-name: Swatinem/rust-cache dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore(deps): bump peter-evans/create-pull-request from 7.0.11 to 8.0.0 (#12918) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.11 to 8.0.0. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/22a9089034f40e5a961c8808d113e2c98fb63676...98357b18bf14b5342f975ff684046ec3b2a07725) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * chore: sepolia rpc url (#12945) chore: sepolia rpc url private * chore(deps): bump crate-ci/typos from 1.40.0 to 1.40.1 (#12949) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.40.0 to 1.40.1. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/2d0ce569feab1f8752f1dde43cc2f2aa53236e06...1a319b54cc9e3b333fed6a5c88ba1a90324da514) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.40.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.15.0 to 3.15.1 (#12950) chore(deps): bump DeterminateSystems/determinate-nix-action Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.15.0 to 3.15.1. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/95732e95d70db3ba1e0adc26a63c5e0375aba78c...1d699fc25db3f9e079cd2f168ca007a4183389be) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.15.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.65.1 to 2.65.7 (#12951) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.65.1 to 2.65.7. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/b9c5db3aef04caffaf95a1d03931de10fb2a140f...4c6723ec9c638cccae824b8957c5085b695c8085) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.65.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(config): err on unknown profile (#12946) * test: remove duplicate Issue2851 test (#12953) * chore(cheats): make sign(Wallet) pure (#12912) * chore(cheats): make sign(Wallet) pure * ignore --------- Co-authored-by: Matthias Seitz Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * fix(evm): use timestamp-based blob base fee calculation (#12959) * fix(evm): use timestamp-based blob base fee calculation * chore: use patch * Now BPO1 is default * bump to hardforks to 0.4.7 --------- Co-authored-by: Matthias Seitz * fix(config): reject bare versions in compilation restrictions (#12955) fmt Co-authored-by: tefyosL-sol * Revert "fix(config): err on unknown profile (#12946)" (#12964) This reverts commit 6ff4b52e2e572e93d0cd81591b1bd0e6ad9ed507. * fix(anvil): use B256 instead of TxHash for block hash parameters (#12961) Update mod.rs * Update crates/config/src/compilation.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: Claude Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: cakevm Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol Co-authored-by: Desant pivo Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Create ci-web3-gamefi.yml (#217) (#289) Introduce a basic CircleCI pipeline for the web3 GameFi project, providing a custom Docker executor and a stub job within a workflow. CI: Add CircleCI config file ci-web3-gamefi.yml with version 2.1 pipeline Define a custom executor using the cimg/base:stable Docker image with Docker Hub credentials Create a web3-defi-game-project- job and integrate it into a my-custom-workflow Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Merge pull request #47 (#290) * Add .circleci/config.yml * Updated config.yml * Updated config.yml * Updated config.yml * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#46) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump revm to 24.0.0 (#10601) * feat: implement add_balance endpoint (#10636) * fix(bindings): ensure forge bind generates snake_case file names (#10622) * fix(bindings): ensure forge bind generates snake_case file names * refactor: use heck crate for snake_case conversion --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore: standardize lint help + validate docs existance (#10639) * feat(cast mktx): add support for "--ethsign" option (#10641) - Sign transactions using "eth_signTransaction" on local node with unlocked accounts. - Same TX building logic as in "cast send --unlocked". - Added a test case to validate the new functionality. * chore(wallets): improve error message for signer instantiation failure (#10646) chore(wallets): improve error message on signer instantiation failure * chore: replaced anvil hardforks with alloy hardforks (#10612) * chore: replaced anvil hardforks with alloy hardforks * fixes * fixes * fixes * removed redundant op and alloy hardforks enum * fixes * fixes * bumped alloy hardforks and kept default to prague and isthmus * bumped alloy-hardforks and fixes --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(`anvil`): latest evm version should be prague (#10653) * fix(`anvil`): latest evm version should be prague * fix test * nit * chore(deps): bump tracing-subscriber (#51) Bumps the cargo group with 1 update in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update test.yml (#52) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update docker-image.yml (#53) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create ci_cargo.yml (#59) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create web3_defi_gamefi.yml (#61) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 21: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#247) Improve readability of the GitHub Actions dependencies workflow by adjusting whitespace and adding blank lines CI: Add blank line before the workflow name declaration Insert blank line after the scheduled cron job entry Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#248) CI: Remove extraneous blank line in .github/workflows/dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#249) CI: Remove dev branch from test workflow triggers Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: pistomat Co-authored-by: zark <77061323+zarkk01@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com> Co-authored-by: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update crates/evm/evm/src/executors/corpus.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Foundry/master test ux (#295) * Update ci.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yml (#211) This pull request updates the Rust version in the CircleCI workflow to 1.89.0. This is a good maintenance task to keep the CI environment up-to-date. I have one suggestion regarding the Docker image tag to potentially simplify future maintenance by automatically adopting patch releases. Overall, the change is correct and beneficial. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#250) CI: Include the 'main' branch in the push event triggers for the test workflow Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#296) @0xrusowsky @Dargon789 fix(fmt): handle trailing coments between base contracts Revert 142 master (#296) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Remove duplicate logic in TxSigner::address() implementations --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Aganis * fix(fmt): handle trailing coments between base contracts (#296) (#299) @0xrusowsky @Dargon789 fix(fmt): handle trailing coments between base contracts Revert 142 master (#296) * Create ci_cargo.yml (#72) * Create config.yml * Rename ci_cargo.yml to cargo.yml * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Remove duplicate logic in TxSigner::address() implementations --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Aganis * Update CircleCI configuration for dev stage (#300) fix Automatic reruns provide a safety net for your CI/CD pipelines by automatically retrying failed steps and/or workflows. Automatic reruns help teams maintain productivity by reducing the need for manual intervention when steps and workflows fail due to temporary issues. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * EIP-4788 implementation * formatting * add beacon block root tests * Update crates/evm/evm/src/executors/trace.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/cast/src/cmd/run.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * feat: upgrade @types/node from 24.10.4 to 25.0.2 Snyk has created this PR to upgrade @types/node from 24.10.4 to 25.0.2. See this package in npm: @types/node See this project in Snyk: https://app.snyk.io/org/dargon789/project/8da85645-409e-46fa-bd46-9b58e7905fb8?utm_source=github-cloud-app&utm_medium=referral&page=upgrade-pr * fix: `svm fails to download solc 0.8.33 on linux/arm64`, bump `svm-rs` (#13007) (#309) bump svm-rs Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * Ethereumjs/master (#310) * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 61: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#105) * Create cargo.yml (#106) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .github/workflows/docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "Create cargo.yml (#106)" This reverts commit 251a2b4fce0c50e3426ffb2022d9abef5b948fa9. * Create cargo.yml (#213) https://github.com/apps/gemini-code-assist Code Review This pull request introduces a CircleCI workflow to automate formatting checks and tests. My review has identified two main issues in the configuration: redundant steps that would unnecessarily increase job execution time, and a mismatch between the Rust version in the CI environment and the one specified in the project's Cargo.toml. I've provided suggestions to fix these issues for a more efficient and consistent CI process. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create docker.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Remove duplicate logic in TxSigner::address() implementations * fix(fmt): handle trailing coments between base contracts (#296) @0xrusowsky @Dargon789 fix(fmt): handle trailing coments between base contracts Revert 142 master (#296) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Remove duplicate logic in TxSigner::address() implementations --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Aganis --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Aganis Co-authored-by: Gengar * Forge/master (#311) * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 61: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#105) * Create cargo.yml (#106) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .github/workflows/docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "Create cargo.yml (#106)" This reverts commit 251a2b4fce0c50e3426ffb2022d9abef5b948fa9. * Create cargo.yml (#213) https://github.com/apps/gemini-code-assist Code Review This pull request introduces a CircleCI workflow to automate formatting checks and tests. My review has identified two main issues in the configuration: redundant steps that would unnecessarily increase job execution time, and a mismatch between the Rust version in the CI environment and the one specified in the project's Cargo.toml. I've provided suggestions to fix these issues for a more efficient and consistent CI process. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create ci-web3-gamefi.yml (#217) Introduce a basic CircleCI pipeline for the web3 GameFi project, providing a custom Docker executor and a stub job within a workflow. CI: Add CircleCI config file ci-web3-gamefi.yml with version 2.1 pipeline Define a custom executor using the cimg/base:stable Docker image with Docker Hub credentials Create a web3-defi-game-project- job and integrate it into a my-custom-workflow Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Remove duplicate logic in TxSigner::address() implementations * fix(fmt): handle trailing coments between base contracts (#296) @0xrusowsky @Dargon789 fix(fmt): handle trailing coments between base contracts Revert 142 master (#296) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Remove duplicate logic in TxSigner::address() implementations --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Aganis --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Aganis Co-authored-by: Gengar * Update dev_stage.yml (#313) (#315) * Update dev_stage.yml (#313) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/dev_stage.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Foundry/main (#316) * chore(deps): bump the cargo group across 1 directory with 2 updates Bumps the cargo group with 2 updates in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing) and [ammonia](https://github.com/rust-ammonia/ammonia). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) Updates `ammonia` from 4.1.0 to 4.1.2 - [Release notes](https://github.com/rust-ammonia/ammonia/releases) - [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-ammonia/ammonia/compare/v4.1.0...v4.1.2) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo - dependency-name: ammonia dependency-version: 4.1.2 dependency-type: indirect dependency-group: cargo ... Signed-off-by: dependabot[bot] * Update crates/verify/src/provider.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/doc/src/writer/as_doc.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update as_doc.rs (#235) Tidy up formatting in as_doc.rs by removing extraneous blank lines in the Document::as_doc implementation Enhancements: Remove unnecessary blank line before initializing bases Remove unnecessary blank line before writing state variables Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore: ignore RUSTSEC (#13011) * update deny for CI * Update more * chore(chisel): rm dead code (#13014) * chore(cli): rm dead code (#13015) * chore(cheatcodes): rm dead code (#13016) * chore(common): rm dead code (#13018) * chore(bench): rm dead code (#13017) * fix(forge): respect lint ignore config in solar compilation (#12978) Co-authored-by: tefyosL-sol * fix: deduplicate submodule status check logic (#13010) Update mod.rs * Foundry/ethereum ux fix tempo #296 (#319) * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 61: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#105) * Create cargo.yml (#106) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .github/workflows/docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "Create cargo.yml (#106)" This reverts commit 251a2b4fce0c50e3426ffb2022d9abef5b948fa9. * Create cargo.yml (#213) https://github.com/apps/gemini-code-assist Code Review This pull request introduces a CircleCI workflow to automate formatting checks and tests. My review has identified two main issues in the configuration: redundant steps that would unnecessarily increase job execution time, and a mismatch between the Rust version in the CI environment and the one specified in the project's Cargo.toml. I've provided suggestions to fix these issues for a more efficient and consistent CI process. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Remove duplicate logic in TxSigner::address() implementations * fix(fmt): handle trailing coments between base contracts (#296) @0xrusowsky @Dargon789 fix(fmt): handle trailing coments between base contracts Revert 142 master (#296) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * Revert "fix(fmt): handle trailing coments between base contracts (#12127)" This reverts commit b8b5fbb83fa2436063cebc34ddf900abc972b11d. * Update cargo.yml (#172) CI/CD Configuration Update: The CircleCI configuration file, .circleci/cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring builds and tests run with the latest stable Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Remove duplicate logic in TxSigner::address() implementations --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Aganis --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Aganis Co-authored-by: Gengar * Potential fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 104: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 105: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix: add Tempo transaction receipt type support in TryFrom conversion (#334) * fix: add Tempo transaction receipt type support in TryFrom conversion (#13047) Amp-Thread-ID: https://ampcode.com/threads/T-019bbf45-d7c8-75ed-8c05-bc1638d487ee Co-authored-by: Matthias Seitz Co-authored-by: Amp * feat(cheatcodes): add getRecordedLogsJson cheatcode (#13093) Adds a new cheatcode `getRecordedLogsJson` that returns recorded logs as a JSON string, similar to the existing `getStateDiffJson` pattern. This allows users to easily post-process recorded logs externally without needing to manually transform the Log[] array to JSON. JSON format: ```json [{"topics": ["0x..."], "data": "0x...", "emitter": "0x..."}] ``` Closes #12854 * feat: add Sourcify support to forge clone (#12900) * Integrate Sourcify API for contract cloning Added support for Sourcify API in `forge clone` command. * Add reqwest dependency with json feature * Remove unused import in clone.rs Removed unused import of BTreeMap. * Refactor EtherscanClient to ExplorerClient * Change sourcify module from private to public * Implement test for sourcify clone functionality Add test for cloning with sourcify source * Update clone.rs * Add url dependency to Cargo.toml * cargo fmt * Enhance Sourcify client with cached creation data Updated the Sourcify client to cache creation data and reuse it across API calls, improving efficiency. Modified the contract source code retrieval to include additional creation data fields. * Improve error handling for contract data retrieval Refactor contract source code and creation data retrieval to use fallback values when API requests fail or fields are unavailable. * Enhance contract_source_code with improved caching Updated contract_source_code to include additional fields in the API request and improved caching of creation data. Removed fallback logic for fetching creation data from the API. * Refactor creation_data handling in clone.rs Removed redundant creation_data initialization and caching. * Refactor response deserialization to use untagged enum * fix: use serde_json::Value for abi in Sourcify parsing The #[serde(untagged)] enum SourcifyContractResponse failed to deserialize because Box doesn't work with untagged enums. RawValue requires borrowing from the original JSON, but untagged enums buffer data during variant matching. Changes: - Change abi field from Box to serde_json::Value - Truncate response in error messages to avoid huge output * feat: add --sourcify-url option for custom Sourcify API endpoint * feat: imply --source sourcify when --sourcify-url is specified * feat: support full path in --sourcify-url When --sourcify-url contains v2/contract/chain, only append address and fields instead of building the full path again. --------- Co-authored-by: grandizzy * perf: add dist profile for smaller release binaries (#13097) * perf: add dist profile for smaller release binaries Add a new 'dist' Cargo profile optimized for distribution: - Fat LTO and codegen-units=1 for better optimization - Strip symbols for smaller binaries - opt-level="s" overrides for non-perf-critical dependencies Benchmarks on Solady test suite show dist is 8% faster than release while being 45% smaller (43MB vs 78MB). Update release workflows to use the dist profile instead of maxperf. * Apply suggestion from @DaniPopes --------- Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * chore(deps): update figment to figment2 v0.11 (#13099) * chore(deps): update figment to figment2 v0.11 * rename * feat: add precompile decoding for Prague BLS12-381 and Osaka P256VERIFY (#13094) * feat: add precompile decoding for Prague BLS12-381 and Osaka P256VERIFY * wip * wip * fix(traces): use raw byte decoding for P256VERIFY precompile P256VERIFY (RIP-7212) uses concatenated raw bytes, not ABI encoding: - Input: hash (32) + r (32) + s (32) + qx (32) + qy (32) = 160 bytes - Output: 32 bytes where 0x...01 means success * fix(traces): use raw byte decoding for all precompiles Precompiles use concatenated raw bytes, not ABI encoding: - ecrecover: hash (32) + v (32) + r (32) + s (32), returns address in last 20 bytes - sha256/ripemd160: raw input, raw 32-byte output (ripemd in last 20 bytes) - ecadd: x1/y1/x2/y2 (32 each), returns x/y (32 each) - ecmul: x1/y1/s (32 each), returns x/y (32 each) - ecpairing: returns 32-byte bool (1 = success) - bls12PairingCheck: returns 32-byte bool (1 = success) * fix(traces): restore ABI-based precompile decoding * fix * fix(anvil): use suggested priority fee by default (#13092) * fix(anvil): use suggested priority fee by default * test: fix anvil trace expectations --------- Co-authored-by: tefyosL-sol * chore: aggregate PRs (#13100) * chore: aggregate PRs This PR aggregates changes from the following PRs: - Closes #13032 by @\splinter012 - Closes #13059 by @\phrwlk * fmt * chore(evm): misleading error message in traces serialization (#13081) Co-authored-by: tefyosL-sol --------- Co-authored-by: Desant pivo Co-authored-by: Matthias Seitz Co-authored-by: Amp Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: Avory Co-authored-by: grandizzy Co-authored-by: onbjerg Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol * Potential fix for code scanning alert no. 103: Artifact poisoning (#336) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Create Docker.yml (#338) Build: Introduce a Docker GitHub Actions workflow that logs into Docker Hub, builds images with buildx, tags them based on branch, semver, and SHA, and pushes them on non-PR events while only loading them for pull requests. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 108: Artifact poisoning (#345) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 110: Uncontrolled data used in path expression (#347) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 102: Artifact poisoning (#351) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * benches\LATEST.md (#350) * benches\LATEST.md * Update benches/LATEST.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 109: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Wagmi (e604566) (#344) * chore(deps): bump revm to 24.0.0 (#10601) * feat: implement add_balance endpoint (#10636) * fix(bindings): ensure forge bind generates snake_case file names (#10622) * fix(bindings): ensure forge bind generates snake_case file names * refactor: use heck crate for snake_case conversion --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore: standardize lint help + validate docs existance (#10639) * feat(cast mktx): add support for "--ethsign" option (#10641) - Sign transactions using "eth_signTransaction" on local node with unlocked accounts. - Same TX building logic as in "cast send --unlocked". - Added a test case to validate the new functionality. * chore(wallets): improve error message for signer instantiation failure (#10646) chore(wallets): improve error message on signer instantiation failure * chore: replaced anvil hardforks with alloy hardforks (#10612) * chore: replaced anvil hardforks with alloy hardforks * fixes * fixes * fixes * removed redundant op and alloy hardforks enum * fixes * fixes * bumped alloy hardforks and kept default to prague and isthmus * bumped alloy-hardforks and fixes --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(`anvil`): latest evm version should be prague (#10653) * fix(`anvil`): latest evm version should be prague * fix test * nit * chore(deps): bump tracing-subscriber (#51) Bumps the cargo group with 1 update in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update test.yml (#52) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update docker-image.yml (#53) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create ci.yml (#57) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create ci_cargo.yml (#59) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create web3_defi_gamefi.yml (#61) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update ci.yml (#66) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#71) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 21: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 2: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update crates/common/src/contracts.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update ci.yml (#107) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#114) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump github/codeql-action from 3 to 4 (#113) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/determinate-nix-action (#111) Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.11.2 to 3.11.3. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/dbda91f6efef3ee627f56175120aa9543687d830...762d7fdba79d046449732c729c1d3aaad021baa2) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump crate-ci/typos from 1.38.0 to 1.38.1 (#112) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.38.0 to 1.38.1. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/83157de2df0fa7c7ae20f73f9dbed44c41f2bb64...80c8a4945eec0f6d464eaf9e65ed98ef085283d1) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.38.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump softprops/action-gh-release from 2.3.4 to 2.4.1 (#110) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.4 to 2.4.1. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...6da8fa9354ddfdc4aeace5fc48d7f679b5214090) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.21 to 2.62.28 (#109) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.21 to 2.62.28. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/522492a8c115f1b6d4d318581f09638e9442547b...e7ef886cf8f69c25ecef6bbc2858a42e273496ec) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.28 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update test.yml (#115) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update crates/doc/src/writer/buf_writer.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update and rename config.yml to ci.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to ci_v1.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/ci_v1.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Foundry/master (#122) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update and rename config.yml to ci.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to ci_v1.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/ci_v1.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update and rename config.yml to ci_deploy.yml (#123) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create snyk-container.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update and rename ci.yml to ci-say-hello.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.ym (#128) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump alloy-dyn-abi in the cargo group across 1 directory (#129) Bumps the cargo group with 1 update in the / directory: [alloy-dyn-abi](https://github.com/alloy-rs/core). Updates `alloy-dyn-abi` from 1.4.0 to 1.4.1 - [Release notes](https://github.com/alloy-rs/core/releases) - [Changelog](https://github.com/alloy-rs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/alloy-rs/core/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: alloy-dyn-abi dependency-version: 1.4.1 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create cargo.yml (#74) (#130) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix typo in CircleCI config file name Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/config.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix formatting in cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix indentation for on_fail condition in CI config Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Fix indentation in CircleCI configuration Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.21 to 2.62.31 (#139) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.21 to 2.62.31. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/v2.62.21...0005e0116e92d8489d8d96fbff83f061c79ba95a) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.31 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github/codeql-action from 3 to 4 (#138) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump snyk/actions Bumps [snyk/actions](https://github.com/snyk/actions) from 14818c4695ecc4045f33c9cee9e795a788711ca4 to 9adf32b1121593767fc3c057af55b55db032dc04. - [Release notes](https://github.com/snyk/actions/releases) - [Commits](https://github.com/snyk/actions/compare/14818c4695ecc4045f33c9cee9e795a788711ca4...9adf32b1121593767fc3c057af55b55db032dc04) --- updated-dependencies: - dependency-name: snyk/actions dependency-version: 9adf32b1121593767fc3c057af55b55db032dc04 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Update CircleCI config with comments and formatting Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update and rename ci-say-hello.yml to ci-web3-defi-gamefi.yml (#154) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci-web3-defi-gamefi.yml (#155) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_deploy.yml (#158) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/cargo.yml (#159) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.31 to 2.62.33 (#162) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.31 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/0005e0116e92d8489d8d96fbff83f061c79ba95a...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump actions/checkout from 4 to 5 (#163) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Merge branch 'foundry-rs:master' (#164) * Create ci_cargo.yml (#72) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Rename ci_cargo.yml to cargo.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * fix(fmt): handle trailing coments between base contracts (#12127) * fix(fmt): account for ternary operators when estimating size * fix(fmt): handle comments between inherited base contracts * test: layout + base inheritance * feat(forge): add bypass prevrandao (#12125) * feat(forge): add bypass prevrandao * Update crates/evm/networks/src/lib.rs Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * changes after review: remove duped code --------- Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * fix(fmt): filter libs when recursing (#12119) * fix(fmt): account for ternary operators when estimating size * fix(fmt): filter libs when recursing * style: clippy * test: wipe contracts before formatting * test: explicitly test ignore * fix(fmt): break try stmts in a fn header-like fashion (#12131) * chore(deps): bump softprops/action-gh-release from 2.3.4 to 2.4.1 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.4 to 2.4.1. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...6da8fa9354ddfdc4aeace5fc48d7f679b5214090) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * chore(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#161) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.28 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/e7ef886cf8f69c25ecef6bbc2858a42e273496ec...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: dependabot[bot] Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(anvil): always disable nonce check (foundry-rs#12144) (#165) * test: refactor testdata/ tests to be run in `forge test` (#12049) * test: run forge test on testdata/ * chore: refactor to use common Test contract * chore: disable testGasMeteringExternal, via-ir * test: rm unused repros * fix: paths * upd * fmt * fix more tests * test: turn testNonExistingContractRevert into expectRevert * fix some more paths * legacy assertions * compile paris with paris * fix: set configs for fs tests * fix remaining paths in cheats * restrict fs permissions * fix: set runtime evm_version too * fix vyper * fix: a couple of repros * fix: we have storage layouts * fix: 3223, 3674: set sender * reorder * feat: move repros expected failures to snapshots * feat: migrate remaining repros tests * feat: rm migrated files * skip testRevertIfGetUnlinked * move expected core/ failures * upd * move logs/ * move all forgetest tests from it/ to cli/ * fix fork test * move trace/ * tmp: move fuzz/invariant out of fuzz/ * move fuzz/ * forge fmt * wips * fix: both vyper and paris; set src/ * canon * lib log * logs * Revert "fix: set runtime evm_version too" This reverts commit 7ca544b10047f608d57c74fb3500a5fbe7e2650e. Contract-level inline config will set evm version for libraries too, which means we fail on deploying libraries that are compiled with newer evm version. * fix: set evm version where needed, per test function * test: reduce gas wastage * chore: clippy * invariant mod.rs * test: fix linking tests with new utils * redact_with * Revert "wips" This reverts commit ee2c17a3023ca7ce8e7effccf0ea0a0f28f6e510. * migrate invariant/target{,Abi} * migrate InvariantAfterInvariant.t.sol * migrate InvariantAssume.t.sol * migrate InvariantCalldataDictionary.t.sol, more test utils * migrate InvariantCustomError.t.sol * migrate InvariantExcludedSenders.t.sol * migrate InvariantFixtures.t.sol * migrate InvariantHandlerFailure.t.sol * interlude: forgot to use a new file * migrate InvariantInnerContract.t.sol * migrate InvariantPreserveState.t.sol * migrate InvariantReentrancy.t.sol * migrate InvariantRollFork.t.sol * migrate InvariantScrapeValues.t.sol * migrate InvariantSequenceNoReverts.t.sol * migrate InvariantShrinkBigSequence.t.sol * migrate InvariantShrinkFailOnRevert.t.sol * migrate InvariantShrinkWithAssert.t.sol * migrate InvariantTest1.t.sol * fix InvariantInnerContract.t.sol * update new Rlp test * com * better com * nuke tests/it * test: fix testdata paths in script tester * test: fix relative paths in test_cmd * test: redact more in issue_2851 * fix: copy testdata correctly * trace addrs * manual retry logic with --retry * fix nondeterministic output * debug: fs lock error context * test: fix project root for windows * test: skip project root test if unset * normalize both * typo * Revert "typo" This reverts commit 402bea105c6f38b82664b50ca854f95e456df795. * Revert "debug: fs lock error context" This reverts commit e5caeddd1e4cb457d7b24d7d7fdfdb370e2feabf. * fix * fix: locked_write_line for windows * chore: clippy * fmt * chore: speed up fuzzed_selected_targets * other way * fix nondeterministic output 2 * fix: disable persistence * test: revert old via-ir * ci: tweak cache key * do not run trace test when isolate --------- Co-authored-by: grandizzy * fix(anvil): always disable nonce check (#12144) * deps: bump deps (#12149) * deps: bump deps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * minimum Cargo.lock --------- Co-authored-by: rplusq Co-authored-by: Claude Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --------- Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: grandizzy Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: Rafael Quintero <32346241+rplusq@users.noreply.github.com> Co-authored-by: rplusq Co-authored-by: Claude * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#167) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#168) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#171) CI/CD Configuration Update: The CircleCI configuration file, cargo.yml, has been updated to use a newer version of the Rust Docker image. Rust Toolchain Version Bump: The cimg/rust Docker image version has been incremented from 1.88.0 to 1.89.0, ensuring the CI pipeline utilizes a more recent Rust toolchain. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#173) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#174) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.62.28 to 2.62.33 (#175) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.62.28 to 2.62.33. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/v2.62.28...e43a5023a747770bfcb71ae048541a681714b951) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.62.33 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Delete .circleci/cargo.yml (#179) I Configuration Removal: The .circleci/cargo.yml file, which defined CircleCI jobs for building and testing Rust projects, has been completely removed from the repository. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#182) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#183) Configuration File Cleanup: Removed an unnecessary blank line in the .circleci/config.yml file, improving its formatting and readability. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#187) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci directory Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci_v1.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Rust Docker image version to 1.89.0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump alloy-dyn-abi in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [alloy-dyn-abi](https://github.com/alloy-rs/core). Updates `alloy-dyn-abi` from 0.8.25 to 0.8.26 - [Release notes](https://github.com/alloy-rs/core/releases) - [Changelog](https://github.com/alloy-rs/core/blob/v0.8.26/CHANGELOG.md) - [Commits](https://github.com/alloy-rs/core/compare/v0.8.25...v0.8.26) --- updated-dependencies: - dependency-name: alloy-dyn-abi dependency-version: 0.8.26 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] * Create ci-web3-gamefi.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 83: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 93: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create codeql.yml (#208) * Update ci.yml (#209) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- https://github.com/apps/gemini-code-assist Code Review This pull request updates the Rust version in the CI from 1.88.0 to 1.89.0. While this is a good maintenance step, I've identified a potential improvement for your CI configuration. The project's Cargo.toml specifies a Minimum Supported Rust Version (MSRV) of 1.86, but the CI doesn't test against it. I've added a comment suggesting the addition of an MSRV check to prevent compatibility issues. * Update cargo.yml (#210) https://github.com/apps/gemini-code-assist ------------------- Code Review This pull request downgrades the Rust version in the CI pipeline from 1.88.0 to 1.87.0. This is inconsistent with the project's declared Minimum Supported Rust Version (MSRV) of 1.89 in Cargo.toml. My review highlights this discrepancy and suggests aligning the CI's Rust version with the MSRV to ensure the project's compatibility guarantees are properly tested. --------------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Foundry rs maste 1f4b36a (#214) * Create jekyll.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 58: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/docker-image.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "chore: fix isolate tests (#10344)" This reverts commit 70ded2b35f95ee9b4ee94f5e44961914d30a87f7. * Delete .github/workflows/jekyll.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update and rename docker-image.yml to docker.yml (#218) Streamline the Docker CI workflow by renaming the file and enhancing it with scheduled runs, Buildx multi-platform builds, metadata tagging, conditional pushes, and automated image signing with Cosign. CI: Rename and replace the legacy docker-image.yml workflow with docker.yml Add scheduled cron runs and triggers on pushes to master, semver tags, and PRs Configure Docker Buildx for multi-platform builds with cache Extract Docker metadata and conditionally push images to GHCR on non-PR events Install Cosign and sign published Docker images using ephemeral identity tokens Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create docker-image.yml (#224) CI: Introduce docker-image.yml GitHub Actions workflow to checkout code and build Docker image on ubuntu-latest Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update config.yml (#225) CI: Insert comment lines to delineate and structure sections in .circleci/config.yml for enhanced clarity Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update sequence.rs (#226) Enhancements: Add standalone # lines in sequence.rs to serve as hidden placeholders for rustdoc examples Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#227) * Update dependencies.yml Refactor the weekly dependencies workflow to inline cargo update steps, auto-generate commit messages and PR bodies with update logs, and use the create-pull-request action to open update PRs on a dedicated branch. Enhancements: Define environment variables for GitHub token, branch name, PR title, and PR body including cargo update logs Inline checkout, Rust toolchain setup, and cargo update command with log cleanup instead of relying on an external workflow Craft commit messages and PR bodies dynamically by capturing and formatting cargo update output Use peter-evans/create-pull-request to push Cargo.lock updates to a 'cargo-update' branch CI: Move permissions and GitHub token configuration into the job context Explicitly set the runner to ubuntu-latest and remove the top-level empty permissions block Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/dependencies.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update npm.yml (#228) CI: Add comment to the Publish Binary step indicating it runs automatically after a successful release workflow or can be triggered manually with a run_id Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update snyk-container.yml (#229) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update nextest.yml (#230) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update const.ts (#231) Code Formatting: Removed an extraneous blank line in npm/src/const.ts to improve code cleanliness and consistency. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "Create web3_defi_gamefi.yml (#61)" (#233) This reverts commit 8575916b7675f246b54daf70cfddccb3f5b97fb0. * Create deploy.yml (#240) * Create deploy.yml CI: Add GitHub Actions workflow to build the Rust project, run tests, and build a Docker image on pushes to main/master Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 106: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#247) Improve readability of the GitHub Actions dependencies workflow by adjusting whitespace and adding blank lines CI: Add blank line before the workflow name declaration Insert blank line after the scheduled cron job entry Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dependencies.yml (#248) CI: Remove extraneous blank line in .github/workflows/dependencies.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#249) CI: Remove dev branch from test workflow triggers Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.lock (#253) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.lock (#254) Chores: Regenerate Cargo.lock to update dependencies Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#255) * Create config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .circleci/config.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update config.yml (#256) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix: upgrade tsdown from 0.15.12 to 0.16.1 Snyk has created this PR to upgrade tsdown from 0.15.12 to 0.16.1. See this package in npm: tsdown See this project in Snyk: https://app.snyk.io/org/dargon789/project/8da85645-409e-46fa-bd46-9b58e7905fb8?utm_source=github-cloud-app&utm_medium=referral&page=upgrade-pr * Create google.yml (#266) CI: Introduce a Google Cloud deployment workflow that builds a Docker image, pushes it to Artifact Registry, and deploys it to a GKE cluster on pushes to the main branches. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update flake.lock (#269) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update flake.nix (#270) Adjust Nix flake development shell configuration for better cross-platform support and simplify dependencies. Enhancements: Remove the dprint dependency from the Nix development shell. Add conditional AppKit framework linkage on Darwin systems in the Nix shell configuration. Drop custom hardeningDisable settings from the Nix development shell definition. https://github.com/apps/gemini-code-assist Code Review This pull request updates the Nix flake configuration to improve cross-platform support and simplify dependencies. The changes include removing dprint and hardeningDisable settings, and conditionally adding the AppKit framework for Darwin systems. While most changes are beneficial, removing dprint from the development shell dependencies while its configuration file remains could cause issues for contributors. I've added a comment regarding this potential inconsistency. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update Cargo.toml (#271) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update nextest.toml (#272) Adjust test runner configuration for nextest to better handle long-running and specific tests. Enhancements: Introduce a dedicated test group that limits chisel-serial tests to a single thread. Increase the default slow-test timeout period to reduce premature terminations for longer-running tests. Expand the slow-timeout override filter to include both ext_integration and can_test_forge_std tests. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update dprint.json (#273) (https://github.com/apps/gemini-code-assist) Code Review This pull request updates the dprint.json configuration file. The changes correctly enable formatting for dprint.json itself by modifying the excludes list, update the JSON and Markdown dprint plugins to their latest versions, and add a final newline to the file for POSIX compliance. These are all good maintenance improvements. The changes have been reviewed and appear to be correct and beneficial. No issues were found. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/apisec-scan.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update counter/README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Dependabot/cargo/cargo 38744a1864 (#282) * chore(deps): bump alloy-dyn-abi in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [alloy-dyn-abi](https://github.com/alloy-rs/core). Updates `alloy-dyn-abi` from 0.8.25 to 0.8.26 - [Release notes](https://github.com/alloy-rs/core/releases) - [Changelog](https://github.com/alloy-rs/core/blob/v0.8.26/CHANGELOG.md) - [Commits](https://github.com/alloy-rs/core/compare/v0.8.25...v0.8.26) --- updated-dependencies: - dependency-name: alloy-dyn-abi dependency-version: 0.8.26 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] * Update and rename ci.yml to cargo.yml (#268) Update CircleCI configuration to use a different Rust toolchain image and rename the workflow file. Build: Rename the CircleCI configuration file from ci.yml to cargo.yml. Change the CircleCI Docker image to use Rust 1.78.0 instead of 1.88.0. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. * Update config.yml (#283) Summary by Sourcery Update CircleCI pipeline to use a custom Docker executor and job tailored to the project instead of the example hello-world workflow. Enhancements: Introduce a reusable custom executor that pulls from the stable cimg/base Docker image with Docker Hub authentication. CI: Replace the sample say-hello job and workflow with a project-specific job and workflow wired to the new custom executor in .circleci/config.yml. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix: use network-specific BaseFeeParams for Optimism in Anvil * Dargon789 patch 1 (#285) * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#167) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/ci_v1.yml (#173) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#174) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci/config.yml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 83: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 93: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 76: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 80: Server-side request forgery Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update cargo.yml (#210) https://github.com/apps/gemini-code-assist ------------------- Code Review This pull request downgrades the Rust version in the CI pipeline from 1.88.0 to 1.87.0. This is inconsistent with the project's declared Minimum Supported Rust Version (MSRV) of 1.89 in Cargo.toml. My review highlights this discrepancy and suggests aligning the CI's Rust version with the MSRV to ensure the project's compatibility guarantees are properly tested. --------------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Fix cloning of compiler settings for Vyper input Replace context.clone().compiler_settings.vyper with context.compiler_settings.vyper.clone() to avoid unnecessary cloning of the entire VerificationContext. This reduces memory allocations when creating VyperInput instances. Applied to both etherscan and sourcify verification providers. --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Gengar * merge gh-master (#287) * Create config.yml (#236) Create .circleci/config.yml defining a version 2.1 pipeline with a docker-based "say-hello" job, checkout and echo steps, and a workflow to orchestrate it Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * fix(evm): use timestamp-based blob base fee calculation (#12959) * fix(evm): use timestamp-based blob base fee calculation * chore: use patch * Now BPO1 is default * bump to hardforks to 0.4.7 --------- Co-authored-by: Matthias Seitz * fix(config): reject bare versions in compilation restrictions (#12955) fmt Co-authored-by: tefyosL-sol * Revert "fix(config): err on unknown profile (#12946)" (#12964) This reverts commit 6ff4b52e2e572e93d0cd81591b1bd0e6ad9ed507. * Update crates/config/src/compilation.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: cakevm Co-authored-by: Matthias Seitz Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Foundry/ethereum ux (#284) * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 61: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 74: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Create config.yml (#105) * Create cargo.yml (#106) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Delete .github/workflows/docker-image.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Revert "Create cargo.yml (#106)" This reverts commit 251a2b4fce0c50e3426ffb2022d9abef5b948fa9. * Create cargo.yml (#213) https://github.com/apps/gemini-code-assist Code Review This pull request introduces a CircleCI workflow to automate formatting checks and tests. My review has identified two main issues in the configuration: redundant steps that would unnecessarily increase job execution time, and a mismatch between the Rust version in the CI environment and the one specified in the project's Cargo.toml. I've provided suggestions to fix these issues for a more efficient and consistent CI process. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Gamefi defi (#288) * chore: ignore RUSTSEC-2025-0137 (#12941) Co-authored-by: Claude * chore(deps): weekly `cargo update` (#12940) * chore(deps): weekly `cargo update` Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar.git` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git repository `https://github.com/paradigmxyz/reth` Locking 71 packages to latest compatible versions Updating alloy-chains v0.2.23 -> v0.2.24 Updating alloy-consensus v1.1.3 -> v1.2.1 Updating alloy-consensus-any v1.1.3 -> v1.2.1 Updating alloy-contract v1.1.3 -> v1.2.1 Updating alloy-dyn-abi v1.5.1 -> v1.5.2 Updating alloy-eip5792 v1.1.3 -> v1.2.1 Updating alloy-eips v1.1.3 -> v1.2.1 Updating alloy-ens v1.1.3 -> v1.2.1 Updating alloy-genesis v1.1.3 -> v1.2.1 Updating alloy-json-abi v1.5.1 -> v1.5.2 Updating alloy-json-rpc v1.1.3 -> v1.2.1 Updating alloy-network v1.1.3 -> v1.2.1 Updating alloy-network-primitives v1.1.3 -> v1.2.1 Updating alloy-primitives v1.5.1 -> v1.5.2 Updating alloy-provider v1.1.3 -> v1.2.1 Updating alloy-pubsub v1.1.3 -> v1.2.1 Updating alloy-rpc-client v1.1.3 -> v1.2.1 Updating alloy-rpc-types v1.1.3 -> v1.2.1 Updating alloy-rpc-types-anvil v1.1.3 -> v1.2.1 Updating alloy-rpc-types-any v1.1.3 -> v1.2.1 Updating alloy-rpc-types-beacon v1.1.3 -> v1.2.1 Updating alloy-rpc-types-debug v1.1.3 -> v1.2.1 Updating alloy-rpc-types-engine v1.1.3 -> v1.2.1 Updating alloy-rpc-types-eth v1.1.3 -> v1.2.1 Updating alloy-rpc-types-trace v1.1.3 -> v1.2.1 Updating alloy-rpc-types-txpool v1.1.3 -> v1.2.1 Updating alloy-serde v1.1.3 -> v1.2.1 Updating alloy-signer v1.1.3 -> v1.2.1 Updating alloy-signer-aws v1.1.3 -> v1.2.1 Updating alloy-signer-gcp v1.1.3 -> v1.2.1 Updating alloy-signer-ledger v1.1.3 -> v1.2.1 Updating alloy-signer-local v1.1.3 -> v1.2.1 Updating alloy-signer-trezor v1.1.3 -> v1.2.1 Updating alloy-signer-turnkey v1.1.3 -> v1.2.1 Updating alloy-sol-macro v1.5.1 -> v1.5.2 Updating alloy-sol-macro-expander v1.5.1 -> v1.5.2 Updating alloy-sol-macro-input v1.5.1 -> v1.5.2 Updating alloy-sol-type-parser v1.5.1 -> v1.5.2 Updating alloy-sol-types v1.5.1 -> v1.5.2 Updating alloy-transport v1.1.3 -> v1.2.1 Updating alloy-transport-http v1.1.3 -> v1.2.1 Updating alloy-transport-ipc v1.1.3 -> v1.2.1 Updating alloy-transport-ws v1.1.3 -> v1.2.1 Updating alloy-trie v0.9.1 -> v0.9.2 Updating alloy-tx-macros v1.1.3 -> v1.2.1 Unchanged annotate-snippets v0.12.5 (available: v0.12.10) Unchanged anstyle-svg v0.1.11 (available: v0.1.12) Downgrading aws-smithy-runtime v1.9.6 -> v1.9.5 Updating axum-core v0.5.5 -> v0.5.6 Updating cc v1.2.50 -> v1.2.51 Updating derive_more v2.1.0 -> v2.1.1 Updating derive_more-impl v2.1.0 -> v2.1.1 Updating dtoa v1.0.10 -> v1.0.11 Updating find-msvc-tools v0.1.5 -> v0.1.6 Unchanged generic-array v0.14.7 (available: v0.14.9) Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating itoa v1.0.15 -> v1.0.17 Updating jiff v0.2.16 -> v0.2.17 Updating jiff-static v0.2.16 -> v0.2.17 Updating libredox v0.1.11 -> v0.1.12 Updating libz-rs-sys v0.5.4 -> v0.5.5 Unchanged matchit v0.8.4 (available: v0.8.6) Unchanged mdbook v0.4.52 (available: v0.5.2) Updating portable-atomic v1.12.0 -> v1.13.0 Updating proc-macro2 v1.0.103 -> v1.0.104 Unchanged protobuf v3.3.0 (available: v3.7.2) Unchanged protobuf-support v3.3.0 (available: v3.7.2) Unchanged rand v0.8.5 (available: v0.9.2) Unchanged ratatui v0.29.0 (available: v0.30.0) Updating reqwest v0.12.26 -> v0.12.28 Updating ruint v1.17.0 -> v1.17.1 Updating rustix v1.1.2 -> v1.1.3 Updating ryu v1.0.21 -> v1.0.22 Updating schemars v1.1.0 -> v1.2.0 Updating schemars_derive v1.1.0 -> v1.2.0 Updating serde_json v1.0.145 -> v1.0.148 Updating signal-hook-registry v1.4.7 -> v1.4.8 Updating syn-solidity v1.5.1 -> v1.5.2 Updating tempfile v3.23.0 -> v3.24.0 Unchanged trezor-client v0.1.4 (available: v0.1.5) Unchanged unicode-width v0.2.0 (available: v0.2.2) Unchanged vergen v8.3.2 (available: v9.0.6) Updating zlib-rs v0.5.4 -> v0.5.5 Adding zmij v1.0.0 note: to see how you depend on a package, run `cargo tree --invert @` * touchups * touchups --------- Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: Matthias Seitz * Update flake.lock (#12939) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/16642c5' (2025-12-20) → 'github:nix-community/fenix/3479aaf' (2025-12-27) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/ea1d299' (2025-12-18) → 'github:rust-lang/rust-analyzer/8c5a68e' (2025-12-26) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/7d853e5' (2025-12-19) → 'github:NixOS/nixpkgs/3edc4a3' (2025-12-27) Co-authored-by: github-actions[bot] Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * fix(chisel): uninitalized variables (#12937) * chore(deps): bump Swatinem/rust-cache from 2.8.1 to 2.8.2 (#12919) Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/swatinem/rust-cache/releases) - [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md) - [Commits](https://github.com/swatinem/rust-cache/compare/f13886b937689c021905a6b90929199931d60db1...779680da715d629ac1d338a641029a2f4372abb5) --- updated-dependencies: - dependency-name: Swatinem/rust-cache dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore(deps): bump peter-evans/create-pull-request from 7.0.11 to 8.0.0 (#12918) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.11 to 8.0.0. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/22a9089034f40e5a961c8808d113e2c98fb63676...98357b18bf14b5342f975ff684046ec3b2a07725) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * chore: sepolia rpc url (#12945) chore: sepolia rpc url private * chore(deps): bump crate-ci/typos from 1.40.0 to 1.40.1 (#12949) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.40.0 to 1.40.1. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/2d0ce569feab1f8752f1dde43cc2f2aa53236e06...1a319b54cc9e3b333fed6a5c88ba1a90324da514) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.40.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.15.0 to 3.15.1 (#12950) chore(deps): bump DeterminateSystems/determinate-nix-action Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.15.0 to 3.15.1. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/95732e95d70db3ba1e0adc26a63c5e0375aba78c...1d699fc25db3f9e079cd2f168ca007a4183389be) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.15.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.65.1 to 2.65.7 (#12951) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.65.1 to 2.65.7. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/b9c5db3aef04caffaf95a1d03931de10fb2a140f...4c6723ec9c638cccae824b8957c5085b695c8085) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.65.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(config): err on unknown profile (#12946) * test: remove duplicate Issue2851 test (#12953) * chore(cheats): make sign(Wallet) pure (#12912) * chore(cheats): make sign(Wallet) pure * ignore --------- Co-authored-by: Matthias Seitz Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * fix(evm): use timestamp-based blob base fee calculation (#12959) * fix(evm): use timestamp-based blob base fee calculation * chore: use patch * Now BPO1 is default * bump to hardforks to 0.4.7 --------- Co-authored-by: Matthias Seitz * fix(config): reject bare versions in compilation restrictions (#12955) fmt Co-authored-by: tefyosL-sol * Revert "fix(config): err on unknown profile (#12946)" (#12964) This reverts commit 6ff4b52e2e572e93d0cd81591b1bd0e6ad9ed507. * fix(anvil): use B256 instead of TxHash for block hash parameters (#12961) Update mod.rs * Update crates/config/src/compilation.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: Claude Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: cakevm Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol Co-authored-by: Desant pivo Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Create ci-web3-gamefi.yml (#217) (#289) Introduce a basic CircleCI pipeline for the web3 GameFi project, providing a custom Docker executor and a stub job within a workflow. CI: Add CircleCI config file ci-web3-gamefi.yml with version 2.1 pipeline Define a custom executor using the cimg/base:stable Docker image with Docker Hub credentials Create a web3-defi-game-project- job and integrate it into a my-custom-workflow Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Merge pull request #47 (#290) * Add .circleci/config.yml * Updated config.yml * Updated config.yml * Updated config.yml * Update test.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 19: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update test.yml (#46) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * chore(deps): bump revm to 24.0.0 (#10601) * feat: implement add_balance endpoint (#10636) * fix(bindings): ensure forge bind generates snake_case file names (#10622) * fix(bindings): ensure forge bind generates snake_case file names * refactor: use heck crate for snake_case conversion --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore: standardize lint help + validate docs existance (#10639) * feat(cast mktx): add support for "--ethsign" option (#10641) - Sign transactions using "eth_signTransaction" on local node with unlocked accounts. - Same TX building logic as in "cast send --unlocked". - Added a test case to validate the new functionality. * chore(wallets): improve error message for signer instantiation failure (#10646) chore(wallets): improve error message on signer instantiation failure * chore: replaced anvil hardforks with alloy hardforks (#10612) * chore: replaced anvil hardforks with alloy hardforks * fixes * fixes * fixes * removed redundant op and alloy hardforks enum * fixes * fixes * bumped alloy hardforks and kept default to prague and isthmus * bumped alloy-hardforks and fixes --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(`anvil`): latest evm version should be prague (#10653) * fix(`anvil`): latest evm version should be prague * fix test * nit * chore(deps): bump tracing-subscriber (#51) Bumps the cargo group with 1 update in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update test.yml (#52) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * Update docker-image.yml (#53) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> … * Potential fix for code scanning alert no. 102: Artifact poisoning (#354) * Potential fix for code scanning alert no. 102: Artifact poisoning Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/workflows/npm.yml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix(config): Respect user-configured etherscan URL over chain defaults (#13238) (#357) * fix(config): respect user-configured etherscan URL over chain defaults * test(config): add tests for custom etherscan URL handling Co-authored-by: Yuya Maruyama <69783679+YuyaMaruyama21D4E@users.noreply.github.com> * Potential fix for code scanning alert no. 154: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 156: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Hardhat project (#378) * docs(config): complete foundry.toml configuration reference (#13198) * complete config * docs: mark optional fields in config README * use foundry book for full config * docs(config): point to Foundry Book for configuration reference * feat(invariant): add optimization mode for invariant testing (#13196) * feat(invariant): add optimization mode for invariant testing Adds optimization mode for invariant testing, similar to Echidna. This mode maximizes an `int256` return value from a function prefixed with `optimize_`. **Usage:** - Define an invariant function returning `int256` with `optimize_` prefix - Foundry will fuzz to find the sequence that maximizes this value - The best value and sequence are tracked and reported - Sequence shrinking is applied to find the minimal reproducing sequence **Example:** ```solidity function optimize_maxRoundingError() external view returns (int256) { return int256(pool.totalShares()) - int256(pool.expectedShares()); } ``` **Bug fix during implementation:** Fixed a bug in shrink logic where `vm.warp` and `vm.roll` values were not correctly accumulated when replaying shrunk sequences. This caused the "best" sequence to be non-reproducible because time/block values depended on removed calls. The fix: - Keeps reverted calls in sequence during optimization to preserve warp/roll - Accumulates warps/rolls from removed calls into kept calls during shrinking - Updates inspector's cheatcodes.block alongside executor.env Closes #12190 Amp-Thread-ID: https://ampcode.com/threads/T-019bea21-149c-728c-9556-850778b70ea3 Co-authored-by: Amp * refactor: isolate optimization shrinking logic from check mode - Keep shrink_sequence and check_sequence unchanged for regular invariant checks - Add shrink_sequence_value and check_sequence_value for optimization mode - Optimization mode handles warp/roll accumulation from removed calls - replay.rs dispatches to correct shrinking function based on target_value * refactor: cleanup shrink optimization code, extract helpers --------- Co-authored-by: Amp * fix: only classify setUp as test setup if it has no parameters (#13204) Contracts with `setUp(bytes memory)` (common in Gnosis Safe/Zodiac modules) were incorrectly classified as dev/test contracts and excluded from `forge build --sizes` output. Now `setUp` is only classified as a test `Setup` function when it has no parameters, matching Forge's actual test setup behavior. Fixes #11126 * chore: fix clippy lints (#13207) * feat(cast): add --flatten flag to cast interface (#13201) * feat(cast): add --all-in-one flag to cast interface Adds a new `--all-in-one` flag to `cast interface` that inlines inherited/library struct types directly into the generated interface. This addresses the issue where `cast interface` generates a separate `library` block for struct types that originate from inherited interfaces, making the generated interface less usable for some workflows. With `--all-in-one`, all types are consolidated into a single interface: ```solidity // Before (default): library IBase { struct TestStruct { address asset; } } interface Contract { function test(IBase.TestStruct memory) external; } // After (with --all-in-one): interface Contract { struct TestStruct { address asset; } function test(TestStruct memory) external; } ``` Uses alloy-json-abi's `ToSolConfig::one_contract(true)` option introduced in alloy-core 0.8.24. Closes #9960 * chore: cargo fmt * refactor: rename --all-in-one to --flatten per review * chore: fix rustfmt * Update flake.lock (#13212) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/edd5602' (2026-01-17) → 'github:nix-community/fenix/93523fa' (2026-01-24) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/adbff8b' (2026-01-15) → 'github:rust-lang/rust-analyzer/39018ac' (2026-01-23) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/be5afa0' (2026-01-16) → 'github:NixOS/nixpkgs/ab9fbbc' (2026-01-24) Co-authored-by: github-actions[bot] * chore(deps): weekly `cargo update` (#13213) Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git repository `https://github.com/paradigmxyz/reth` Locking 62 packages to latest compatible versions Updating alloy-chains v0.2.29 -> v0.2.30 Updating alloy-consensus v1.4.3 -> v1.5.2 Updating alloy-consensus-any v1.4.3 -> v1.5.2 Updating alloy-contract v1.4.3 -> v1.5.2 Updating alloy-eip5792 v1.4.3 -> v1.5.2 Updating alloy-eip7928 v0.3.0 -> v0.3.2 Updating alloy-eips v1.4.3 -> v1.5.2 Updating alloy-ens v1.4.3 -> v1.5.2 Unchanged alloy-evm v0.26.3 (available: v0.27.0) Updating alloy-genesis v1.4.3 -> v1.5.2 Updating alloy-json-rpc v1.4.3 -> v1.5.2 Updating alloy-network v1.4.3 -> v1.5.2 Updating alloy-network-primitives v1.4.3 -> v1.5.2 Unchanged alloy-op-evm v0.26.3 (available: v0.27.0) Updating alloy-provider v1.4.3 -> v1.5.2 Updating alloy-pubsub v1.4.3 -> v1.5.2 Updating alloy-rpc-client v1.4.3 -> v1.5.2 Updating alloy-rpc-types v1.4.3 -> v1.5.2 Updating alloy-rpc-types-anvil v1.4.3 -> v1.5.2 Updating alloy-rpc-types-any v1.4.3 -> v1.5.2 Updating alloy-rpc-types-beacon v1.4.3 -> v1.5.2 Updating alloy-rpc-types-debug v1.4.3 -> v1.5.2 Updating alloy-rpc-types-engine v1.4.3 -> v1.5.2 Updating alloy-rpc-types-eth v1.4.3 -> v1.5.2 Updating alloy-rpc-types-trace v1.4.3 -> v1.5.2 Updating alloy-rpc-types-txpool v1.4.3 -> v1.5.2 Updating alloy-serde v1.4.3 -> v1.5.2 Updating alloy-signer v1.4.3 -> v1.5.2 Updating alloy-signer-aws v1.4.3 -> v1.5.2 Updating alloy-signer-gcp v1.4.3 -> v1.5.2 Updating alloy-signer-ledger v1.4.3 -> v1.5.2 Updating alloy-signer-local v1.4.3 -> v1.5.2 Updating alloy-signer-trezor v1.4.3 -> v1.5.2 Updating alloy-signer-turnkey v1.4.3 -> v1.5.2 Updating alloy-transport v1.4.3 -> v1.5.2 Updating alloy-transport-http v1.4.3 -> v1.5.2 Updating alloy-transport-ipc v1.4.3 -> v1.5.2 Updating alloy-transport-ws v1.4.3 -> v1.5.2 Updating alloy-tx-macros v1.4.3 -> v1.5.2 Updating aws-lc-rs v1.15.3 -> v1.15.4 Updating aws-lc-sys v0.36.0 -> v0.37.0 Updating cc v1.2.53 -> v1.2.54 Updating clearscreen v4.0.2 -> v4.0.3 Unchanged generic-array v0.14.7 (available: v0.14.9) Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating libm v0.2.15 -> v0.2.16 Unchanged matchit v0.8.4 (available: v0.8.6) Removing nix v0.29.0 Updating num-conv v0.1.0 -> v0.2.0 Updating opener v0.8.3 -> v0.8.4 Updating openssl-probe v0.2.0 -> v0.2.1 Updating proc-macro2 v1.0.105 -> v1.0.106 Updating process-wrap v8.2.1 -> v9.0.1 Updating quote v1.0.43 -> v1.0.44 Unchanged rand v0.8.5 (available: v0.9.2) Unchanged reqwest v0.12.28 (available: v0.13.1) Updating socket2 v0.6.1 -> v0.6.2 Updating time v0.3.45 -> v0.3.46 Updating time-core v0.1.7 -> v0.1.8 Updating time-macros v0.2.25 -> v0.2.26 Updating uuid v1.19.0 -> v1.20.0 Updating watchexec-signals v5.0.0 -> v5.0.1 Updating watchexec-supervisor v5.0.1 -> v5.0.2 Updating web_atoms v0.2.1 -> v0.2.3 Updating windows v0.61.3 -> v0.62.2 Updating windows-collections v0.2.0 -> v0.3.2 Removing windows-core v0.61.2 Updating windows-future v0.2.1 -> v0.3.2 Removing windows-link v0.1.3 Updating windows-numerics v0.2.0 -> v0.3.1 Removing windows-result v0.3.4 Removing windows-strings v0.4.2 Updating windows-threading v0.1.0 -> v0.2.1 Updating zmij v1.0.15 -> v1.0.16 note: to see how you depend on a package, run `cargo tree --invert @` Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> * chore(anvil): clean up remaining dead code after Odyssey sunset (#13211) * fix(coverage): correct BRDA hit values for LCOV consistency (#13151) * fix(coverage): correct BRDA hit values for LCOV consistency Per the LCOV tracefile format specification, BRDA hit values should be: - "-" when the expression was never evaluated (line not executed) - "0" when the branch exists and was evaluated but never taken - "N" when the branch was taken N times Previously, we were outputting "-" for all branches with 0 hits, which caused genhtml to fail with "inconsistent" errors when a line was hit (DA shows hits > 0) but branches on that line showed "-". This fix tracks line hits in a first pass, then uses that information to determine whether to output "-" (line never hit) or "0" (line hit but branch not taken) for branches with 0 hits. Fixes foundry-rs/foundry#11548 * fix: clippy explicit_iter_loop warning * test(coverage): add brda_lcov_consistency test for BRDA hit values Verifies that BRDA outputs follow LCOV spec: - "0" when line was executed but branch not taken - "-" when line was never executed This catches the inconsistency that caused genhtml to fail. --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks * feat(forge): add browser wallet support for `forge script` (#12952) * feat(script): add support for browser wallet * fix: browser wallet opts defaults * chore: bump foundry-browser-wallet v0.1.0 * ci: use shared cache mounts for parallel builds (#13231) perf(docker): use shared cache mounts for parallel builds Change cache mount sharing mode from `locked` to `shared` for cargo registry, git, and sccache directories. With `sharing=locked`, parallel builds must wait for exclusive access to each cache, causing builds to queue up even when compilation itself is fast. Both cargo and sccache handle concurrent access correctly, so `shared` is safe and allows parallel builds to proceed without blocking. Amp-Thread-ID: https://ampcode.com/threads/T-019bfc1d-3cee-70ca-9caa-01e33acdff46 Co-authored-by: Amp * fix(anvil): preserve withdrawals in SerializableBlock for state dump/load (#13227) * fix(anvil): preserve withdrawals in SerializableBlock for state dump/load * add: test * fix ci * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.15.1 to 3.15.2 (#13236) chore(deps): bump DeterminateSystems/determinate-nix-action Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.15.1 to 3.15.2. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/1d699fc25db3f9e079cd2f168ca007a4183389be...89ab342bd48ff7318caf8d39d6a330c7b1df8f2f) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.66.2 to 2.67.13 (#13235) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.66.2 to 2.67.13. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/v2.66.2...710817a1645ef40daad5bcde7431ceccf6cc3528) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.13 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 (#13234) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 8.0.0 to 8.1.0. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/98357b18bf14b5342f975ff684046ec3b2a07725...c0f553fe549906ede9cf27b5156039d195d2ece0) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump crate-ci/typos from 1.42.1 to 1.42.2 (#13233) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.42.1 to 1.42.2. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/65120634e79d8374d1aa2f27e54baa0c364fff5a...a1d64977b4aa1709d6328d518aa753f4899352d8) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.42.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * fix(docker): expose VERGEN_GIT_SHA to build environment (#13237) * docs(coverage): link to stack-too-deep guide in warnings (#13240) Update coverage warnings to point to the new Foundry Book guide instead of the GitHub issue, providing users with actionable techniques to resolve the error. Amp-Thread-ID: https://ampcode.com/threads/T-019bff7a-8502-72b7-af76-794f258e8c67 Co-authored-by: Amp * Polkadot/Kusama/PolkadotTestnet for RPC gas estimation (#12537) * adds polkadot testnet * adds polkadot testnet * bump alloy chains * fmt * Remove kluster * update rpc url * update rpc url * fmt * adds polkadot kusama * bump alloy chains * perf(anvil): avoid redundant block queries in ots_getBlockTransactions (#13243) * fix(anvil): use consistent chain_id fallback for blob params (#13241) * Revert "Delete .circleci/ci_deploy.yml (#322)" (#358) This reverts commit 87dd517cf50fef686c8ef39431d06b16d4744d88. * feat(primitives): introduce `NetworkWallet` impl for `EthereumWallet` (#13248) - Seamless support for eth/op/tempo txs - This aims to replace `WalletSigner`'s impl once `FoundryNetwork` will be used everywhere * refactor(common): make `ProviderBuilder` generic over `Network` (#13250) * refactor(common): make `ProviderBuilder` generic over `Network` - Updated `ProviderBuilder` helper to be generic, which will facilate `FoundryNetwork` rollout - Adjusted the instantiation of `ProviderBuilder` in various locations to use `AnyNetwork`. - Enhanced the `build` and `build_with_wallet` methods to accommodate the new generic structure. * fix: relax trait bound on `N::TransactionRequest` * fix: comment * fix: comment * fix(anvil): return error instead of empty vec for out-of-range log queries (#13251) * fix(config): respect user-configured etherscan URL over chain defaults (#13239) fix(config): Respect user-configured etherscan URL over chain defaults (#13238) * fix(config): respect user-configured etherscan URL over chain defaults * test(config): add tests for custom etherscan URL handling Co-authored-by: Yuya Maruyama <69783679+YuyaMaruyama21D4E@users.noreply.github.com> * fix(primitives): track both 4844/7594 sidecars presence in `FoundryTransactionRequest::build_typed_tx` (#13218) * fix(primitives): track both 4844/7594 sidecars presence in `FoundryTransactionRequest::build_typed_tx` * fix: after `TransactionBuilder4844` impl * feat(common): introduce generic `ProviderBuilder::from_config` method (#13268) - keep the existing helpers that erase generic to avoid breaking change * fix(cast): remove redundant chain() call in explorer_client (#13272) Co-authored-by: tefyosL-sol * fix(verify): respect user-configured etherscan URL over chain defaults (#13275) Co-authored-by: tefyosL-sol * Update flake.lock (#13279) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/93523fa' (2026-01-24) → 'github:nix-community/fenix/b2344f3' (2026-01-31) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/39018ac' (2026-01-23) → 'github:rust-lang/rust-analyzer/eb05888' (2026-01-30) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/ab9fbbc' (2026-01-24) → 'github:NixOS/nixpkgs/6308c3b' (2026-01-30) Co-authored-by: github-actions[bot] * fix(invariant): remove unused cloned calldata (#12893) * fix(invariant): prune calldata to bound memory usage in long runs * style: fix formatting in invariant executor * chore: remove vyper files from testdata to fix CI * chore: trigger ci update * fix(invariant): remove unused FuzzCase.calldata field to prevent OOM The calldata field in FuzzCase was stored but never read after construction. Removing it entirely eliminates memory accumulation during long invariant runs. Changes: - Remove FuzzCase.calldata field (unused after construction) - Remove prune_calldata() methods (no longer needed) - Restore vyper test files that were incorrectly deleted Fixes #12397 Amp-Thread-ID: https://ampcode.com/threads/T-019c17c9-d969-7370-bf0d-495e473e8e30 Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c17c9-d969-7370-bf0d-495e473e8e30 Co-authored-by: Amp --------- Co-authored-by: Georgios Konstantopoulos Co-authored-by: Amp * feat(debugger): display actual gas usage alongside refund counter (#13271) * feat(debugger): display actual gas usage alongside refund counter * fix(forge-test): fix flamegraph gas inaccuracy issues * reverse `--decode-internal` not default with `--flamechart` * make gas unsigned as `inferno` doesn't support anyway * replace revm-inspectors patch * fix clippy * update tests * update tests * fmt * fix(config): handle decimal string in U256 deserialization (#13284) * fix: adjust numerical cells in gas report to be right aligned (#12883) * Adjust Cells to be Right Aligned in Gas Report * fix test and fmt --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore(deps): weekly `cargo update` (#13280) Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git repository `https://github.com/paradigmxyz/reth` Locking 35 packages to latest compatible versions Updating alloy-dyn-abi v1.5.2 -> v1.5.4 Unchanged alloy-evm v0.26.3 (available: v0.27.0) Updating alloy-json-abi v1.5.2 -> v1.5.4 Unchanged alloy-op-evm v0.26.3 (available: v0.27.0) Updating alloy-primitives v1.5.2 -> v1.5.4 Updating alloy-sol-macro v1.5.2 -> v1.5.4 Updating alloy-sol-macro-expander v1.5.2 -> v1.5.4 Updating alloy-sol-macro-input v1.5.2 -> v1.5.4 Updating alloy-sol-type-parser v1.5.2 -> v1.5.4 Updating alloy-sol-types v1.5.2 -> v1.5.4 Updating annotate-snippets v0.12.10 -> v0.12.11 Updating aws-smithy-async v1.2.7 -> v1.2.10 Updating aws-smithy-http-client v1.1.5 -> v1.1.8 Updating aws-smithy-observability v0.2.0 -> v0.2.3 Updating aws-smithy-query v0.60.9 -> v0.60.12 Updating aws-smithy-runtime-api v1.10.0 -> v1.11.2 Updating aws-smithy-types v1.3.6 -> v1.4.2 Updating bytemuck v1.24.0 -> v1.25.0 Updating cc v1.2.54 -> v1.2.55 Updating clap v4.5.54 -> v4.5.56 Updating clap_builder v4.5.54 -> v4.5.56 Updating clap_derive v4.5.49 -> v4.5.55 Updating cliclack v0.3.7 -> v0.3.8 Updating find-msvc-tools v0.1.8 -> v0.1.9 Unchanged generic-array v0.14.7 (available: v0.14.9) Updating iana-time-zone v0.1.64 -> v0.1.65 Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating keccak-asm v0.1.4 -> v0.1.5 Unchanged matchit v0.8.4 (available: v0.8.6) Updating notify-types v2.0.0 -> v2.1.0 Updating portable-atomic v1.13.0 -> v1.13.1 Updating portable-atomic-util v0.2.4 -> v0.2.5 Unchanged rand v0.8.5 (available: v0.9.2) Unchanged reqwest v0.12.28 (available: v0.13.1) Updating revm-inspectors v0.34.0 -> v0.34.2 Updating sha3-asm v0.1.4 -> v0.1.5 Updating siphasher v1.0.1 -> v1.0.2 Updating slab v0.4.11 -> v0.4.12 Updating syn-solidity v1.5.2 -> v1.5.4 Removing tiny-keccak v2.0.2 Updating zerocopy v0.8.33 -> v0.8.37 Updating zerocopy-derive v0.8.33 -> v0.8.37 Updating zmij v1.0.16 -> v1.0.18 note: to see how you depend on a package, run `cargo tree --invert @` Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * chore: update RPC URLs from ithaca.xyz to reth.rs (#13261) * chore: update RPC URLs from ithaca.xyz to reth.rs Co-authored-by: Tim Beiko Amp-Thread-ID: https://ampcode.com/threads/T-019c0a51-3f0a-76eb-ba4a-bfb6a697d9ba Co-authored-by: Amp * fix * fmt * Update rpc.rs * Update rpc.rs * test: update test --------- Co-authored-by: Tim Beiko Co-authored-by: Amp Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: Oliver Nordbjerg Co-authored-by: onbjerg * ci(bench): use depot runner for benchmarks (#13288) * feat(cli): cli markdown docs (#13291) * feat: add foundry-cli-markdown crate Add a new crate for generating Markdown documentation from clap CLIs. This is a fork of clap-markdown with the following enhancements: - Support for grouped options by help heading (PR #48) - Show environment variable names for arguments (PR #50) - Add version information to generated Markdown (PR #52) * feat(cli): add hidden --markdown-help flag Add a hidden --markdown-help flag to forge, cast, anvil, and chisel that prints CLI reference documentation as Markdown and exits. This uses the new foundry-cli-markdown crate to generate the output. * chore(deps): bump docker/login-action from 3.6.0 to 3.7.0 (#13298) Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/5e57cd118135c172c3672efd75eb46360885c0ef...c94ce9fb468520275223c153574b00df6fe4bcc9) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 3.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.67.13 to 2.67.18 (#13297) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.67.13 to 2.67.18. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/710817a1645ef40daad5bcde7431ceccf6cc3528...650c5ca14212efbbf3e580844b04bdccf68dac31) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/update-flake-lock from 727cc5b0b19bc265bd5ef28fc66bccb284473b5d to 5adeaaaf36f64df54f62adb34aa5fbfdb0109d34 (#13299) chore(deps): bump DeterminateSystems/update-flake-lock Bumps [DeterminateSystems/update-flake-lock](https://github.com/determinatesystems/update-flake-lock) from 727cc5b0b19bc265bd5ef28fc66bccb284473b5d to 5adeaaaf36f64df54f62adb34aa5fbfdb0109d34. - [Release notes](https://github.com/determinatesystems/update-flake-lock/releases) - [Commits](https://github.com/determinatesystems/update-flake-lock/compare/727cc5b0b19bc265bd5ef28fc66bccb284473b5d...5adeaaaf36f64df54f62adb34aa5fbfdb0109d34) --- updated-dependencies: - dependency-name: DeterminateSystems/update-flake-lock dependency-version: 5adeaaaf36f64df54f62adb34aa5fbfdb0109d34 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump mikepenz/release-changelog-builder-action from 6.0.1 to 6.1.0 (#13300) chore(deps): bump mikepenz/release-changelog-builder-action Bumps [mikepenz/release-changelog-builder-action](https://github.com/mikepenz/release-changelog-builder-action) from 6.0.1 to 6.1.0. - [Release notes](https://github.com/mikepenz/release-changelog-builder-action/releases) - [Commits](https://github.com/mikepenz/release-changelog-builder-action/compare/439f79b5b5428107c7688c1d2b0e8bacc9b8792c...6faf020194b7c8853f9e55c4fd92e40b02122a04) --- updated-dependencies: - dependency-name: mikepenz/release-changelog-builder-action dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore: fix typos CI (#13303) - Add consts to typos ignore list (valid Rust std::env::consts) - Fix LintCotext -> LintContext typos in linter docs * chore(deps): bump crate-ci/typos from 1.42.2 to 1.43.0 (#13296) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.42.2 to 1.43.0. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/a1d64977b4aa1709d6328d518aa753f4899352d8...93cbdb2d23269548cf0db0f74d0bc6a09a3f0d5c) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.43.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * sec: bump to `bytes` `^1.11.1` for `RUSTSEC-2026-0007` (#13306) bump to 1.11.1 for patch: https://rustsec.org/advisories/RUSTSEC-2026-0007 * chore(anvil,cast): remove unnecessary `populate_blob_hashes()` (#13308) Both `set_blob_sidecar`/`set_blob_sidecar_7594` implement a call to `populate_blob_hashes()` * feat(forge): generate random fuzz seed if none provided (#13309) * feat(forge): generate random fuzz seed if none provided Generate a random seed for fuzz/invariant tests when no seed is explicitly configured. This ensures reproducibility by always having a seed available that can be passed via --fuzz-seed to reproduce test runs. * feat(forge): print fuzz seed on fuzz failure (#13310) * feat(forge): print fuzz seed on test failure When a fuzz or invariant test fails, print the seed used so users can reproduce the failure with --fuzz-seed. * test: update snapshots for fuzz seed output Add [SEED] redaction pattern to match 'Fuzz seed: 0x...' output. Update all test snapshots that have fuzz/invariant failures to include the new seed line. * fix: use [SEED] placeholder in issue_3055 test snapshot Amp-Thread-ID: https://ampcode.com/threads/T-019c26cb-9d21-74f9-9e49-7ea59885e827 Co-authored-by: Amp --------- Co-authored-by: Georgios Konstantopoulos Co-authored-by: Amp --------- Co-authored-by: Georgios Konstantopoulos Co-authored-by: Amp * feat(anvil): cache block timestamp in mined receipts (#13311) * fix: use shared `display_chain` helper in CLI error handler (#13314) * Update handler.rs * Update handler.rs * Add --enable-tx-gas-limit CLI flag for EIP-7825 support (#13307) add --enable-tx-gas-limit * fix(anvil): use consistent chain_id fallback in fork setup (#13276) Co-authored-by: tefyosL-sol * fix(test-utils): skip build artifacts when copying to temp workspace (#13266) * feat(lint): add common uppercase abbreviations to mixedCase exceptions (#13305) Co-authored-by: onbjerg * fix(fmt): correct indentation for closing brace in empty contracts with comments (#13319) Co-authored-by: onbjerg * feat(anvil): add `trace_replayBlockTransactions` endpoint for block txs tracing (#13098) Co-authored-by: onbjerg * fix(eip712): write diagnostics to stderr instead of stdout (#13293) Co-authored-by: onbjerg * foundryup: tempo now distributes all binaries (#13337) Amp-Thread-ID: https://ampcode.com/threads/T-019c2ea2-963a-744a-8b1d-57709bc295be Co-authored-by: Amp * fix(config): handle vyper section with skip_serializing_if fields (#13318) * fix(config): handle vyper section with skip_serializing_if fields The vyper config section uses skip_serializing_if = Option::is_none on all fields, causing the default serialization to produce an empty dict. This led to all vyper keys being flagged as unknown. Add explicit VYPER_KEYS constant and special-case the vyper section in collect_standalone_section_warnings to use these known keys instead of deriving them from the (empty) default serialization. Fixes #13316 * test(config): add regression tests for vyper config warnings Tests for #13316: - no_false_warnings_for_vyper_config_keys: valid vyper keys in standalone section - no_false_warnings_for_nested_vyper_config_keys: valid vyper keys in profile - warns_on_unknown_vyper_keys: unknown vyper keys should still warn Amp-Thread-ID: https://ampcode.com/threads/T-019c28b9-9c8c-76bf-96a5-ff5a504c0507 Co-authored-by: Amp * fix build issues * fix(config): handle nested vyper section with skip_serializing_if fields The VyperConfig struct uses skip_serializing_if on all Option fields, causing the default serialization to produce an empty dict. This caused false warnings for valid vyper keys like optimize, path, and experimental_codegen when used in profile nested sections like [profile.default.vyper]. Uses the existing VYPER_KEYS constant for nested vyper sections, matching how standalone [vyper] sections are already handled. Fixes #13316 Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c297e-a282-7188-8f79-5080d3e451a9 --------- Co-authored-by: Amp Co-authored-by: zerosnacks Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix: broken config test, currently blocking CI (#13340) fix broken config * skip checksum hash in create2 mining when case-insensitive (#13331) * kip checksum hash in create2 mining * fix the clippy * fix: avoid setting FOUNDRY_PROFILE: ci in template workflows, profile does not exist (#13339) avoid encoding FOUNDRY_PROFILE: ci, profile does not exist * perf(evm): wrap Executor.backend in Arc for copy-on-write cloning (#13327) * perf(evm): wrap Executor.backend in Arc for copy-on-write cloning During parallel fuzzing, each worker clones the Executor. Previously this deep-cloned the entire Backend (CacheDB, JournaledState, state snapshots), which could be 10-50MB per clone with 16 workers = 160-800MB wasted memory. This change wraps Backend in Arc and uses Arc::make_mut() for copy-on-write semantics. When workers only read state, they share the same backend. When a worker mutates, it gets its own copy. Expected impact: - ~80% memory reduction for parallel fuzz runs - Faster executor clone (pointer copy instead of deep clone) - No behavioral change: mutations still get isolated copies Amp-Thread-ID: https://ampcode.com/threads/T-019c2af1-f00b-723a-a3c3-25cbd6f3e92b Co-authored-by: Amp * test: update config test expectations for new mixed_case_exceptions Fix test expectations after 1bd687f0d added new values (ID, URL, API, JSON, XML, HTML, HTTP, HTTPS) to lint.mixed_case_exceptions defaults. Amp-Thread-ID: https://ampcode.com/threads/T-019c2af1-f00b-723a-a3c3-25cbd6f3e92b Co-authored-by: Amp * Update config.rs * Update config.rs * fix: restore "URI" in config test JSON expectations Amp-Thread-ID: https://ampcode.com/threads/T-019c2f68-f9df-76bc-ba4c-94fbe1789c9c Co-authored-by: Amp --------- Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks * chore(ci): update time crate (#13348) * test(cast): ignore flaky_run_celo_with_precompiles (Celo RPC no longer supports debug_traceTransaction) (#13347) * fix(test-utils): create destination directory in copy_dir_filtered (#13350) * chore(wallets): Remove `NetworkWallet` impl for `WalletSigner` (#13343) - superseeded by `EthereumWallet`'s one, which is integrated in `TransactionBuilder` flow * fix(anvil): return error when querying future block number in with_database_at (#13267) * return error when querying future block number * fix test --------- Co-authored-by: onbjerg Co-authored-by: Matthias Seitz * chore: remove stale `tiny-keccak` references (#13358) * chore: remove stale tiny-keccak profile override * chore: remove stale tiny-keccak deny exception * chore(script): typo (#13353) * perf(cheatcodes): loop invariant code motion by hand (#13357) * chore(anvil): remove unnecessary clone operations (#13330) * perf(linking): replace double hash mpa lookup contains_key + [] with single get (#13361) * fix(verify): correct Sourcify API URL construction for custom chains (#13360) Update verify.rs * chore(common): remove dead `with_spinner_reporter` function (#13366) * resolve absolute and relative paths on Windows (#13364) * fix: unittest failed (#13371) * perf(anvil): reuse storage root from prove_storage instead of recompu… (#13363) perf(anvil): reuse storage root from prove_storage instead of recomputing * chore(deps): weekly `cargo update` (#13384) Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git repository `https://github.com/paradigmxyz/reth` Locking 94 packages to latest compatible versions Updating alloy-consensus v1.5.2 -> v1.6.1 Updating alloy-consensus-any v1.5.2 -> v1.6.1 Updating alloy-contract v1.5.2 -> v1.6.1 Updating alloy-eip5792 v1.5.2 -> v1.6.1 Updating alloy-eips v1.5.2 -> v1.6.1 Updating alloy-ens v1.5.2 -> v1.6.1 Updating alloy-evm v0.26.3 -> v0.26.4 (available: v0.27.2) Updating alloy-genesis v1.5.2 -> v1.6.1 Updating alloy-json-rpc v1.5.2 -> v1.6.1 Updating alloy-network v1.5.2 -> v1.6.1 Updating alloy-network-primitives v1.5.2 -> v1.6.1 Updating alloy-op-evm v0.26.3 -> v0.26.4 (available: v0.27.2) Updating alloy-provider v1.5.2 -> v1.6.1 Updating alloy-pubsub v1.5.2 -> v1.6.1 Updating alloy-rlp v0.3.12 -> v0.3.13 Updating alloy-rlp-derive v0.3.12 -> v0.3.13 Updating alloy-rpc-client v1.5.2 -> v1.6.1 Updating alloy-rpc-types v1.5.2 -> v1.6.1 Updating alloy-rpc-types-anvil v1.5.2 -> v1.6.1 Updating alloy-rpc-types-any v1.5.2 -> v1.6.1 Updating alloy-rpc-types-beacon v1.5.2 -> v1.6.1 Updating alloy-rpc-types-debug v1.5.2 -> v1.6.1 Updating alloy-rpc-types-engine v1.5.2 -> v1.6.1 Updating alloy-rpc-types-eth v1.5.2 -> v1.6.1 Updating alloy-rpc-types-trace v1.5.2 -> v1.6.1 Updating alloy-rpc-types-txpool v1.5.2 -> v1.6.1 Updating alloy-serde v1.5.2 -> v1.6.1 Updating alloy-signer v1.5.2 -> v1.6.1 Updating alloy-signer-aws v1.5.2 -> v1.6.1 Updating alloy-signer-gcp v1.5.2 -> v1.6.1 Updating alloy-signer-ledger v1.5.2 -> v1.6.1 Updating alloy-signer-local v1.5.2 -> v1.6.1 Updating alloy-signer-trezor v1.5.2 -> v1.6.1 Updating alloy-signer-turnkey v1.5.2 -> v1.6.1 Updating alloy-transport v1.5.2 -> v1.6.1 Updating alloy-transport-http v1.5.2 -> v1.6.1 Updating alloy-transport-ipc v1.5.2 -> v1.6.1 Updating alloy-transport-ws v1.5.2 -> v1.6.1 Updating alloy-trie v0.9.3 -> v0.9.4 Updating alloy-tx-macros v1.5.2 -> v1.6.1 Updating anyhow v1.0.100 -> v1.0.101 Updating async-compression v0.4.37 -> v0.4.39 Updating aws-config v1.8.12 -> v1.8.13 Updating aws-runtime v1.5.18 -> v1.6.0 Updating aws-sdk-kms v1.98.0 -> v1.99.0 Updating aws-sdk-sso v1.92.0 -> v1.93.0 Updating aws-sdk-ssooidc v1.94.0 -> v1.95.0 Updating aws-sdk-sts v1.96.0 -> v1.97.0 Updating aws-sigv4 v1.3.7 -> v1.3.8 Updating aws-smithy-async v1.2.10 -> v1.2.11 Updating aws-smithy-http v0.62.6 -> v0.63.3 Updating aws-smithy-http-client v1.1.8 -> v1.1.9 Updating aws-smithy-json v0.61.9 -> v0.62.3 Updating aws-smithy-observability v0.2.3 -> v0.2.4 Updating aws-smithy-query v0.60.12 -> v0.60.13 Updating aws-smithy-runtime v1.9.8 -> v1.10.0 Updating aws-smithy-runtime-api v1.11.2 -> v1.11.3 Updating aws-smithy-types v1.4.2 -> v1.4.3 Updating clap v4.5.56 -> v4.5.57 Updating clap_builder v4.5.56 -> v4.5.57 Updating flate2 v1.1.8 -> v1.1.9 Unchanged generic-array v0.14.7 (available: v0.14.9) Updating hyper-util v0.1.19 -> v0.1.20 Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating interprocess v2.2.3 -> v2.3.1 Updating jiff v0.2.18 -> v0.2.19 Updating jiff-static v0.2.18 -> v0.2.19 Unchanged matchit v0.8.4 (available: v0.8.6) Updating memchr v2.7.6 -> v2.8.0 Updating nybbles v0.4.7 -> v0.4.8 Updating pest v2.8.5 -> v2.8.6 Updating pest_derive v2.8.5 -> v2.8.6 Updating pest_generator v2.8.5 -> v2.8.6 Updating pest_meta v2.8.5 -> v2.8.6 Updating proptest v1.9.0 -> v1.10.0 Unchanged rand v0.8.5 (available: v0.9.2) Updating rapidhash v4.2.1 -> v4.2.2 Updating regex v1.12.2 -> v1.12.3 Updating regex-automata v0.4.13 -> v0.4.14 Updating regex-lite v0.1.8 -> v0.1.9 Updating regex-syntax v0.8.8 -> v0.8.9 Unchanged reqwest v0.12.28 (available: v0.13.2) Updating schemars v1.2.0 -> v1.2.1 Updating schemars_derive v1.2.0 -> v1.2.1 Updating sval v2.16.0 -> v2.17.0 Updating sval_buffer v2.16.0 -> v2.17.0 Updating sval_dynamic v2.16.0 -> v2.17.0 Updating sval_fmt v2.16.0 -> v2.17.0 Updating sval_json v2.16.0 -> v2.17.0 Updating sval_nested v2.16.0 -> v2.17.0 Updating sval_ref v2.16.0 -> v2.17.0 Updating sval_serde v2.16.0 -> v2.17.0 Updating system-configuration v0.6.1 -> v0.7.0 Updating webbrowser v1.0.6 -> v1.1.0 Updating webpki-roots v1.0.5 -> v1.0.6 Updating zerocopy v0.8.37 -> v0.8.39 Updating zerocopy-derive v0.8.37 -> v0.8.39 Updating zlib-rs v0.5.5 -> v0.6.0 Updating zmij v1.0.18 -> v1.0.19 note: to see how you depend on a package, run `cargo tree --invert @` Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> * Update flake.lock (#13383) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/b2344f3' (2026-01-31) → 'github:nix-community/fenix/e1b28f6' (2026-02-07) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/eb05888' (2026-01-30) → 'github:rust-lang/rust-analyzer/d2a00da' (2026-02-05) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/6308c3b' (2026-01-30) → 'github:NixOS/nixpkgs/ae67888' (2026-02-06) Co-authored-by: github-actions[bot] * perf(verify): reuse transaction from earlier RPC call instead of fetching twice (#13391) * perf(verify): reuse transaction from earlier RPC call instead of fetching twice * fix ci * fix(cast): --json support for erc20 cmds (#12727) * refactor(anvil): using is_ok since it's more robust (#13377) * fix: may div by zero (#13369) * refactor(primitives): turn `FoundryTransactionRequest` into an enum (#13278) - Combines Eth's, Op's, and Tempo's transaction requests to inherit Op/Tempo tx building * perf: avoid checksum (#13374) * docs: slim readme (#13393) * fix: correct trace message in dynamic linking preprocessor (#13394) * perf(invariant): avoid cloning state changeset in fuzz runs (#13398) Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> * chore(deps): bump depot/build-push-action from 1.16.2 to 1.17.0 (#13405) Bumps [depot/build-push-action](https://github.com/depot/build-push-action) from 1.16.2 to 1.17.0. - [Release notes](https://github.com/depot/build-push-action/releases) - [Commits](https://github.com/depot/build-push-action/compare/9785b135c3c76c33db102e45be96a25ab55cd507...5f3b3c2e5a00f0093de47f657aeaefcedff27d18) --- updated-dependencies: - dependency-name: depot/build-push-action dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.67.18 to 2.67.27 (#13406) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.67.18 to 2.67.27. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/650c5ca14212efbbf3e580844b04bdccf68dac31...1e67dedb5e3c590e1c9d9272ace46ef689da250d) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.67.27 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump crate-ci/typos from 1.43.0 to 1.43.4 (#13407) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.0 to 1.43.4. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/93cbdb2d23269548cf0db0f74d0bc6a09a3f0d5c...78bc6fb2c0d734235d57a2d6b9de923cc325ebdd) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.43.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci: use dedicated template for isolate flaky test failures (#13409) * chore(deps): bump depot/setup-action from 1.6.0 to 1.7.1 (#13408) * fix(primitives): `FoundryTransactionRequest` conversion w/ tempo variant (#13401) - Fix `TempoTransactionRequest` variant, as inner req was always set to default. - Added unit tests to assess `FoundryTransactionRequest` proper variant routing * return error instead of empty array when filter not found (#13415) * chore(config): remove unused enum accessor methods (#13414) * fix(cast): clean up temp dir in `cast storage` when etherscan cache is unavailable (#13418) * perf(primitives): avoid cloning receipts (#13396) * fix: constructor params and args check (#13375) * fix: correct path format in get_paths doc comment (#13388) * ci: replace merge_group with push on master (#13419) * ci(release): pin action-gh-release to v2.4.2 (#13420) v2.5.0 introduced a draft→finalize flow that races in matrix jobs, causing 'Too many retries' failures in the Create release step. See: https://github.com/softprops/action-gh-release/issues/704 Amp-Thread-ID: https://ampcode.com/threads/T-019c4c6d-c55a-752a-8b27-25413f485bed Co-authored-by: Amp * fix(anvil): handle disk cache write failures in state eviction (#13332) * feat(forge,chisel): realtime `console.log` (#13321) * fix(cast): remove duplicate receipt handling in Tempo transactions (#13378) * perf(traces): deduplicate addresses before external fetching (#13320) * fix: prevent panic on etherscan client creation failure in test command (#13395) * perf(config): skip redundant remapping detection in _with_root (#13389) * fix(common): remove trailing space in `state_root` match pattern (#13426) * chore(config): `curl` mode as config key (#13260) Co-authored-by: onbjerg * fix(config): normalize deny_warnings from env vars (#13434) * fix: correct dead condition in command error formatting (#13427) * add missing JSON output support for `erc20 decimals` (#13438) * fix(anvil): variable shadowing bug in ReadyTransactions::remove_with_markers (#13436) Update transactions.rs * Update flake.lock (#13448) * chore(deps): weekly `cargo update` (#13449) * feat(evm): `ForkDatabase`/`MultiFork` generic over `Network` (#13459) * feat(evm): `ForkDatabase`/`MultiFork` generic over `Network` bump `foundry-fork-db` * fix: typo * fix(cheatcodes): fix vm.expectRevert for direct precompile calls (#13460) Precompile calls don't create an interpreter frame, so `initialize_interp` never fires and `max_depth` never gets bumped beyond the cheatcode call depth. This causes the depth check in `handle_expect_revert` to fail with "call didn't revert at a lower depth than cheatcode call depth". Track `max_depth` in the `call` hook as well, accounting for the callee depth (`curr_depth + 1`). Amp-Thread-ID: https://ampcode.com/threads/T-019c63a2-2c36-7334-ab55-2931a174b59c Co-authored-by: Amp * fix(lint): remove unreachable macro arm in declare_forge_lint (#13452) * chore(flake): use nightly rustfmt (#13441) * chore(flake): use nightly rustfmt * chore(flake): update flake * feat: add `executeTransaction` cheatcode (#13437) feat: add executeTransaction cheatcode Port the executeTransaction cheatcode from tempoxyz/tempo-foundry. Executes RLP-encoded signed transactions in an isolated EVM context with full semantics (like --isolate mode). OP deposit and Tempo AA transactions return errors for now (marked with TODOs). * fix(forge): don't reset snapshot diff result on missing file (#13442) * fix(traces): check HTTP status before JSON parsing in Sourcify fetcher (#13446) * Update external.rs * chore: fmt * test: rm useless tests --------- Co-authored-by: Oliver Nordbjerg * feat(cheatcodes): add Ed25519 crypto cheatcodes (#13450) * feat(cheatcodes): add Ed25519 crypto cheatcodes Add four new cheatcodes for Ed25519 cryptography: - createEd25519Key(bytes32 salt) - deterministic key generation - publicKeyEd25519(bytes32 privateKey) - derive public key - signEd25519(namespace, message, privateKey) - sign with domain separation - verifyEd25519(signature, namespace, message, publicKey) - verify signatures Uses ed25519-consensus crate. Includes comprehensive unit tests for determinism, namespace separation, edge cases, and invalid inputs. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c5f04-a6ed-7015-9b4d-4464a35bc26c * chore: solidity test * test: fix assertions --------- Co-authored-by: Amp Co-authored-by: Oliver Nordbjerg * feat(lint): add missing visit methods to EarlyLintVisitor (#13454) Update early.rs * notify subscribers for txs promoted after block mining (#13464) * notify subscribers for txs promoted after block mining * refactor: extract notify_ready helper to deduplicate notification logic Amp-Thread-ID: https://ampcode.com/threads/T-019c6840-d225-723a-bf92-46e4e29c7ad1 Co-authored-by: Amp --------- Co-authored-by: Matthias Seitz Co-authored-by: Amp * chore(deps): bump taiki-e/install-action from 2.67.27 to 2.68.0 (#13465) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.67.27 to 2.68.0. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/1e67dedb5e3c590e1c9d9272ace46ef689da250d...f8d25fb8a2df08dcd3cead89780d572767b8655f) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.68.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/cache-cargo-install-action from 3.0.1 to 3.0.2 (#13466) Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases) - [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/34ce5120836e5f9f1508d8713d7fdea0e8facd6f...2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f) --- updated-dependencies: - dependency-name: taiki-e/cache-cargo-install-action dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump crate-ci/typos from 1.43.4 to 1.43.5 (#13467) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.4 to 1.43.5. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](https://github.com/crate-ci/typos/compare/78bc6fb2c0d734235d57a2d6b9de923cc325ebdd...57b11c6b7e54c402ccd9cda953f1072ec4f78e33) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: 1.43.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/update-flake-lock from 5adeaaaf36f64df54f62adb34aa5fbfdb0109d34 to a135ea602656a8348c5c34887131dd9f7a28bd8c (#13468) chore(deps): bump DeterminateSystems/update-flake-lock Bumps [DeterminateSystems/update-flake-lock](https://github.com/determinatesystems/update-flake-lock) from 5adeaaaf36f64df54f62adb34aa5fbfdb0109d34 to a135ea602656a8348c5c34887131dd9f7a28bd8c. - [Release notes](https://github.com/determinatesystems/update-flake-lock/releases) - [Commits](https://github.com/determinatesystems/update-flake-lock/compare/5adeaaaf36f64df54f62adb34aa5fbfdb0109d34...a135ea602656a8348c5c34887131dd9f7a28bd8c) --- updated-dependencies: - dependency-name: DeterminateSystems/update-flake-lock dependency-version: a135ea602656a8348c5c34887131dd9f7a28bd8c dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump softprops/action-gh-release from 2.4.2 to 2.5.0 (#13469) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.2 to 2.5.0. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/5be0e66d93ac7ed76da52eca8bb058f665c3a5fe...a06a81a03ee405af7f2048a818ed3f03bbf83c7b) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * test: mark clone CLI tests as flaky (#13472) These tests hit real Etherscan/Sourcify APIs and fail intermittently due to rate limiting and network issues. They will now run in the nightly flaky test workflow with retries instead of blocking every PR. Amp-Thread-ID: https://ampcode.com/threads/T-019c6840-d225-723a-bf92-46e4e29c7ad1 Co-authored-by: Amp * fix: bind TempDir guard in clone test to prevent premature cleanup (#13471) Amp-Thread-ID: https://ampcode.com/threads/T-019c6840-d225-723a-bf92-46e4e29c7ad1 Co-authored-by: Amp * prevent balance overflow in anvil_addBalance (#13457) Co-authored-by: Matthias Seitz * chore(tests): bump forge-std version (#13482) * move `sccache --show-stats` into build RUN to show actual stats (#13483) * fix(anvil): correct blob_gas_used_ratio calculation in fee history (#13491) Update fees.rs * fix(cheatcodes): make vm.executeTransaction work in isolation mode (#13475) * fix(test): exclude ExecuteTransactionTest from isolation mode vm.executeTransaction already performs its own isolated execution (fresh EVM, cloned state, state merging). When isolation mode is enabled, the inspector's transact_inner intercepts CALLs at depth==1 inside the cheatcode's inner EVM, causing double-isolation that results in 'transaction reverted: 0x'. Amp-Thread-ID: https://ampcode.com/threads/T-019c6ad3-d3f0-70d3-8d78-38ccd8444e9e Co-authored-by: Amp * fix(cheatcodes): make vm.executeTransaction work in isolation mode Two bugs prevented vm.executeTransaction from working with --isolate: 1. Double isolation: executeTransaction creates its own inner EVM at depth=1, but the isolation inspector also intercepts CALLs at depth=1, causing a nested transact_inner. Fix: add set_in_inner_context() to CheatcodesExecutor trait and set it before/after the inner EVM run, matching how transact_inner already handles this. 2. Corrupted cfg env: executeTransaction modified env.cfg (disabled nonce checks, set initcode size limit) but never restored it. Subsequent isolated calls then failed nonce validation (NonceTooHigh). Fix: restore env.cfg from the cached copy alongside env.tx and basefee. Amp-Thread-ID: https://ampcode.com/threads/T-019c6ad3-d3f0-70d3-8d78-38ccd8444e9e Co-authored-by: Amp --------- Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test-utils: remove unused `IS_TTY` helper (#13492) Update util.rs * chore: update LATEST_SOLC to 0.8.34 (#13489) Amp-Thread-ID: https://ampcode.com/threads/T-019c7441-de53-7338-86cb-6d84f755016a Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(anvil): add --max-transactions CLI flag (#13495) * remove unimplemented anvil_enableTraces endpoint (#13499) * feat(fmt): pretty printing for generic block/transaction responses (#13497) feat(fmt): pretty printing for generic block/transaction reponses * Refactor `locked_read_to_string` to reuse `locked_read (#13494) Update fs.rs Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(script-sequence): extract duplicated filter logic (#13500) * chore(broadcast): cleanup avg gas price calculation (#13509) * chore(deps): weekly `cargo update` (#13513) Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: Matthias Seitz * feat(common): generic `TransactionReceiptWithRevertReason` + pprinting (#13503) * refactor: Use `fs::write_pretty_json_file` in `MultiChainSequence::save` (#13510) * Update flake.lock (#13511) flake.lock: Update Flake lock file updates: • Updated input 'fenix': 'github:nix-community/fenix/d0555da' (2026-02-14) → 'github:nix-community/fenix/6d86ae5' (2026-02-21) • Updated input 'fenix/rust-analyzer-src': 'github:rust-lang/rust-analyzer/bbc84d3' (2026-02-13) → 'github:rust-lang/rust-analyzer/46a214b' (2026-02-20) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/2343bbb' (2026-02-11) → 'github:NixOS/nixpkgs/d1c15b7' (2026-02-16) Co-authored-by: github-actions[bot] * fix(sol-macro-gen): correct identifier check in write_mod_name (#13508) * chore(deps): bump DeterminateSystems/update-flake-lock from a135ea602656a8348c5c34887131dd9f7a28bd8c to 5909792a83875ddb5dd4b18734534a98a74a709c (#13524) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump taiki-e/install-action from 2.68.0 to 2.68.8 (#13523) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.15.2 to 3.16.1 (#13522) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(anvil): prevent panic in `utc_from_secs` for out-of-range timestamps (#13520) * ci(release): revert action-gh-release to v2.4.2 (#13527) * ci: ignore softprops/action-gh-release in dependabot (#13528) * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.16.1 to 3.16.3 (#13529) chore(deps): bump DeterminateSystems/determinate-nix-action Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.16.1 to 3.16.3. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/681d8e8bfdb5d7af56f113ba2425b1fb00ec9edc...73327eb48f028efaaf5013656ba216ca3cdeca7b) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.16.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(primitives): add `FoundryTransactionBuilder` trait (#13512) Co-authored-by: figtracer <1gusredo@gmail.com> * fix(wallets): use turnkey_unsupported() instead of hardcoded error (#13535) * chore(deps): bump taiki-e/install-action from 2.68.8 to 2.68.9 (#13534) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.8 to 2.68.9. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/cfdb446e391c69574ebc316dfb7d7849ec12b940...7f491e26f71f4ec2e6902c7c95c73043f209ab79) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.68.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(anvil): preserve original error in update_url (#13531) Co-authored-by: Oliver Nordbjerg * fix: swap incorrect doc comments for archive RPC URL functions in rpc. (#13480) * Allow verifier-url for unknown Etherscan chains (#13079) Co-authored-by: Mayank Sharma <82099885+codersharma2001@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: onbjerg * fix: remove unused `no_storage_caching()` method from `NodeConfig` (#13487) * fix: remove code duplication in `get_runtime_codes` (#13502) * forge: avoid repeated selector decoding in find command (#13516) * fix(cli): Git::is_repo_root always returns false (#13505) * fix(verify): remove unused functions from `VerificationContext` (#13481) Co-authored-by: onbjerg * add missing doc section keys to config validation (#13447) * fix(anvil): use EIP-2718 encoding for OP enveloped_tx (#13537) * fix: correct flag name in error message from --compiler-version to --… (#13539) * chore(ci): unblock ci, fix clippy lint (#13543) * add lint-fix * fix: resolve nightly clippy warnings - collapsible_match: collapse plain if into match guards, allow for if-let - iter_kv_map: use .values()/.keys() instead of .iter().flat_map(|(_, v)| v) - useless_conversion: remove unnecessary .into_iter() Amp-Thread-ID: https://ampcode.com/threads/T-019c9930-51be-760a-b2c7-9a029f851fee Co-authored-by: Amp * fix: add missing match arm for Occupied entry in remappings Amp-Thread-ID: https://ampcode.com/threads/T-019c99a5-39f3-72be-ad16-e7d041662ea9 Co-authored-by: Amp * fix: revert incorrect .values() call on Vec in runner Amp-Thread-ID: https://ampcode.com/threads/T-019c99a5-39f3-72be-ad16-e7d041662ea9 Co-authored-by: Amp * fix: resolve irrefutable let pattern warning in MultiForkHandler Amp-Thread-ID: https://ampcode.com/threads/T-019c99a5-39f3-72be-ad16-e7d041662ea9 Co-authored-by: Amp --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Amp * refactor(wallets): browser wallet generic `Network` (#13550) * fix(anvil): use individual tx gas instead of cumulative in fee history (#13552) * fix(anvil): clear tx pool on anvil_reset (#13544) * add EIP-3860 initcode size check in txpool validation (#13473) * fix: clarify Anvil storage caching builder naming (#13546) Co-authored-by: Amp Co-authored-by: Oliver Nordbjerg * refactor(evm): remove dead BackendError::Other variant (#13553) * fix(script): actually skip Vyper contract verification (#13484) Co-authored-by: onbjerg * feat(doc): Adding new 'Constants' section for constants and immutables (#13116) Co-authored-by: onbjerg Co-authored-by: Oliver Nordbjerg * fix(evm): avoid wrong CowBackend initialization in load_allocs and clone_account (#13554) Co-authored-by: Amp Co-authored-by: Matthias Seitz * chore: use `fs::write_pretty_json_file` in `ScriptSequence::save` (#13562) * fix(forge): apply --access-list in forge create (#13557) Co-authored-by: onbjerg * Update flake.lock (#13564) Co-authored-by: github-actions[bot] Co-authored-by: onbjerg * chore(deps): weekly `cargo update` (#13565) Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> * fix(cast): correct max_int boundary check for uint255 (#13568) * fix(fmt): correct total_difficulty attribute name (#13578) * chore(evm): use `AnyRpcTransaction` in `configure_tx_env`/`commit_transaction` (#13572) * feat(evm): add `AsEnvMut::set_env` (#13573) * fix(fmt): don't inline while/for/if blocks with multiple statements (#13566) * refactor(cheatcodes,evm): use ContextTr read accessors for env fields (#13582) Co-authored-by: Amp * chore(evm): simplify `CowBackend` init logic by using `Option` (#13584) * feat(anvil): add EIP-2935 blockhash histo… * Potential fix for code scanning alert no. 155: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 170: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * foundry-rs#13763 (#398) * fix: update EtherlinkTestnet -> EtherlinkShadownet for alloy-chains v0.2.31 (#13763) Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com> * chore(evm): make `FoundryCfg` generic over `Spec` (#13757) * chore(deps): weekly `cargo update` (#13760) Updating git repository `https://github.com/alloy-rs/alloy` Updating git repository `https://github.com/alloy-rs/evm.git` Updating git repository `https://github.com/foundry-rs/optimism` Updating git submodule `https://github.com/flashbots/op-rbuilder` Updating git submodule `https://github.com/foundry-rs/forge-std` Updating git submodule `https://github.com/runtimeverification/kontrol-cheatcodes` Updating git submodule `https://github.com/ethereum-optimism/lib-keccak` Updating git submodule `https://github.com/dapphub/ds-test` Updating git submodule `https://github.com/vectorized/solady` Updating git submodule `https://github.com/OpenZeppelin/openzeppelin-contracts` Updating git submodule `https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable` Updating git submodule `https://github.com/OpenZeppelin/openzeppelin-contracts` Updating git submodule `https://github.com/a16z/erc4626-tests.git` Updating git submodule `https://github.com/safe-global/safe-contracts` Updating git submodule `https://github.com/vectorized/solady` Updating git submodule `https://github.com/transmissions11/solmate` Updating git submodule `https://github.com/ethereum-optimism/superchain-registry` Updating git submodule `https://github.com/flashbots/rollup-boost` Updating git repository `https://github.com/foundry-rs/foundry-fork-db` Updating git repository `https://github.com/paradigmxyz/revm-inspectors.git` Updating git repository `https://github.com/rust-cli/rexpect` Updating git repository `https://github.com/paradigmxyz/solar` Skipping git submodule `https://github.com/argotorg/solidity.git` due to update strategy in .gitmodules Updating git repository `https://github.com/tempoxyz/tempo` Updating git submodule `https://github.com/foundry-rs/forge-std` Updating git submodule `https://github.com/vectorized/solady` Updating git submodule `https://github.com/tempoxyz/tempo-std` Updating git repository `https://github.com/stevencartavia/reth` Locking 31 packages to latest compatible versions Updating alloy-chains v0.2.30 -> v0.2.31 Updating alloy-trie v0.9.4 -> v0.9.5 Adding anstream v1.0.0 Unchanged anstream v0.6.21 (available: v1.0.0) Updating anstyle v1.0.13 -> v1.0.14 Updating anstyle-lossy v1.1.4 -> v1.1.5 Adding anstyle-parse v1.0.0 Updating bon v3.9.0 -> v3.9.1 Updating bon-macros v3.9.0 -> v3.9.1 Updating c-kzg v2.1.6 -> v2.1.7 Updating cc v1.2.56 -> v1.2.57 Updating clap v4.5.60 -> v4.6.0 Updating clap_builder v4.5.60 -> v4.6.0 Updating clap_complete v4.5.66 -> v4.6.0 Updating clap_complete_nushell v4.5.10 -> v4.6.0 Updating clap_derive v4.5.55 -> v4.6.0 Updating clap_lex v1.0.0 -> v1.1.0 Updating colorchoice v1.0.4 -> v1.0.5 Updating console v0.16.2 -> v0.16.3 Removing darling v0.21.3 Removing darling_core v0.21.3 Removing darling_macro v0.21.3 Updating derive-where v1.6.0 -> v1.6.1 Updating evmole v0.8.2 -> v0.8.4 Unchanged generic-array v0.14.7 (available: v0.14.9) Unchanged icu_collections v2.0.0 (available: v2.1.1) Unchanged icu_normalizer v2.0.1 (available: v2.1.1) Unchanged icu_normalizer_data v2.0.0 (available: v2.1.1) Unchanged icu_properties v2.0.2 (available: v2.1.2) Unchanged icu_properties_data v2.0.1 (available: v2.1.2) Unchanged idna_adapter v1.1.0 (available: v1.2.1) Updating kasuari v0.4.11 -> v0.4.12 Unchanged matchit v0.8.4 (available: v0.8.6) Updating once_cell v1.21.3 -> v1.21.4 Unchanged op-revm v15.0.0 (available: v17.0.0) Updating portable-atomic-util v0.2.5 -> v0.2.6 Unchanged quick-junit v0.5.2 (available: v0.6.0) Unchanged rand v0.8.5 (available: v0.10.0) Unchanged rand v0.9.2 (available: v0.10.0) Unchanged revm v34.0.0 (available: v36.0.0) Updating schannel v0.1.28 -> v0.1.29 Updating serde_with v3.17.0 -> v3.18.0 Updating serde_with_macros v3.17.0 -> v3.18.0 Unchanged snapbox v0.6.24 (available: v1.1.0) Unchanged soldeer-commands v0.10.0 (available: v0.10.1) Unchanged soldeer-core v0.10.0 (available: v0.10.1) Unchanged strum v0.27.2 (available: v0.28.0) Updating tempfile v3.26.0 -> v3.27.0 Updating tinyvec v1.10.0 -> v1.11.0 Unchanged toml v0.9.12+spec-1.1.0 (available: v1.0.6+spec-1.1.0) Unchanged toml_edit v0.24.1+spec-1.1.0 (available: v0.25.4+spec-1.1.0) Updating tracing-subscriber v0.3.22 -> v0.3.23 Updating zerocopy v0.8.41 -> v0.8.42 Updating zerocopy-derive v0.8.41 -> v0.8.42 note: to see how you depend on a package, run `cargo tree --invert @` Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: Matthias Seitz * feat(cheatcodes): bubble-up `Network` generic to `Wallets` (#13768) * feat(script): generic `TxStatus` receipt type (#13770) feat(script): generic `TxStatus` * chore(script): idiomatic `BroadcastReader::into_tx_receipts` (#13771) * refactor(evm): simplify `Backend::initialize` and `CowBackend::backend_mut` (#13755) `initialize` now takes `(SpecId, Address, TxKind)` instead of `&Env`, since those are the only fields it reads. This removes the need for `backend_mut` to clone `EvmEnv` — it only needs `&TxEnv` now, because the `SpecId` already comes from `self.spec_id`. Moves towards aligning with alloy-evm's BlockExecutor ownership model where `EvmEnv` is block-scoped and `TxEnv` is tx-scoped. * feat(cheatcodes): make `Cheatcodes` context-generic (#13767) * feat(cheatcodes): make `Cheatcodes` context-generic * fix: merge conflict --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(cast): add `--network` flag to `cast tx` for network-specific raw encoding (#13745) * feat(cast): add --network flag to `cast tx` for multi-network raw encoding Replace the `FoundryNetwork`-based `transaction_raw` workaround with proper network selection via a new `--network` / `-n` CLI flag. This moves raw tx encoding back into `Cast::transaction` dispatching to the correct provider (Ethereum, Optimism, or Tempo) based on the flag. - Add `NetworkVariant` enum (Ethereum/Optimism/Tempo) in foundry-cli opts - use it w/ `--network` through CastSubcommand::Tx and provider construction - Add test for Tempo raw tx encoding (`tx_raw_tempo`) * fix: network num args * feat(cast): `block --raw` network selection (#13754) * rebase PR 13745 * feat(cast): `block` for multi-network raw encoding Co-authored-by: figtracer <1gusredo@gmail.com> * fix: doctest --------- Co-authored-by: figtracer <1gusredo@gmail.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(anvil): make mined_receipts generic (#13761) * chore(evm): split `Executor::env` into `evm_env` and `tx_env` fields (#13773) Split the getters/setters accordingly, and update all call sites. * refactor(cheatcodes): `CheatcodesExecutor` generic (#13774) refactor(cheatcodes): make `CheatcodesExecutor` use generic env types from `ContextTr` Replace concrete `EvmEnv`/`TxEnv` in `with_fresh_nested_evm` with `EvmEnv<::Spec, CTX::Block>` and `CTX::Tx` to support non-Eth networks. Add `FoundryContextTr` trait as fully generic counterpart to the concrete `FoundryContextExt`. Remove unused `clone_to_cfg_env`/`apply_cfg_env` from `FoundryCfg`. * fix(anvil): flaky `test_trace_filter()` (#13764) fix * chore(cast): granular bounds on `Cast` (#13776) * refactor(evm): `FoundryContextExt` generic types (#13778) * fix(cheatcodes): create file in writeJson/writeToml 3-arg overload (#13777) Closes foundry-rs/foundry#13775 Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(anvil): make EthApi generic over `N: Network` (#13751) * refactor(anvil): make EthApi generic over N: Network * rm todo comments --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(evm): move `CheatsCtxExt` trait to `foundry-evm-core` (#13781) Previously `CheatsCtxExt` was defined in `foundry-cheatcodes`. Move it to `foundry-evm-core`, and rename it to `EthCheatCtx` to make clear it pins the context to Ethereum-specific env types (`BlockEnv`/`TxEnv`/`CfgEnv`) as a temporary alias during the transition to fully generic EVM and cheatcodes. * refactor(evm): make `NestedEvm` trait generic with associated types (#13782) Add `Tx`, `Block`, and `Spec` associated types to `NestedEvm` so each network can specify its own environment types. The trait remains object-safe when used as `dyn NestedEvm`. Update `NestedEvmClosure` to pin the associated types to Eth types, and set the concrete types in the `FoundryEvm` implementation. * refactor(anvil): propagate `EthApi` to all holders (#13783) refactor(anvil): propagate EthApi to all holders * refactor(evm): rename `NestedEvmClosure` and move to `foundry-evm-core` (#13785) refactor(evm): rename `NestedEvmClosure` to `EthNestedEvmClosure` and move to `foundry-evm-core` * refactor(evm): remove `Env` abstraction from `Executor` impl (#13790) * refactor(anvil): remove redundant param (#13792) * refactor(cheatcodes): tighten verbose bounds to `EthCheatCtx` (#13791) * refactor(evm): remove `eth_*_mut()` from `FoundryContextExt` (#13789) * feat(script): generic `TransactionWithMetadata` + generic pprinting `TransactionMaybeSigned` (#13795) * refactor(evm): `DatabaseExt` generic over env types (#13797) refactor(evm): make DatabaseExt generic over environment types Add generic parameters with defaults to DatabaseExt: `DatabaseExt` This makes the trait generic over EVM environment types while remaining fully backwards-compatible — all existing code uses the defaults. Non-Ethereum networks (e.g. Tempo) can now implement DatabaseExt with their own environment types. 9 method signatures updated: snapshot_state, revert_state, create_select_fork, create_select_fork_at_transaction, select_fork, roll_fork, roll_fork_to_transaction, transact, transact_from_tx. * test(cast): mark flaky revert_reason_from and wildcard RPC-dependent tail (#13796) * fix(anvil): reject invalid versioned_hashes in beacon blobs endpoint (#13787) * fix(cheatcodes): prevent panic in expectRevert with empty bytes (#13769) * fix(cheatcodes): prevent panic in expectRevert with empty bytes When vm.expectRevert(bytes('')) catches a revert with non-empty data, decode_error in alloy-dyn-abi panics on the empty expected_reason (slice index out of range). Guard the decode_error call with a length check. Closes #13766 Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test: add regression test for expectRevert empty bytes panic Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test: fix snapshot for expectRevert empty bytes regression test Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(evm): add `DB` associated type to `FoundryJournalExt` (#13799) * refactor(evm): make DatabaseExt generic over environment types Add generic parameters with defaults to DatabaseExt: `DatabaseExt` This makes the trait generic over EVM environment types while remaining fully backwards-compatible — all existing code uses the defaults. Non-Ethereum networks (e.g. Tempo) can now implement DatabaseExt with their own environment types. 9 method signatures updated: snapshot_state, revert_state, create_select_fork, create_select_fork_at_transaction, select_fork, roll_fork, roll_fork_to_transaction, transact, transact_from_tx. * refactor(evm): replace dyn DatabaseExt in FoundryJournalExt with associated type Replace `&mut dyn DatabaseExt` return type in `FoundryJournalExt` with an associated type `type DB: DatabaseExt`. This removes 3 uses of `dyn DatabaseExt` while remaining backwards-compatible — `&mut DB` auto-coerces to `&mut dyn DatabaseExt` at call sites that still need it. `FoundryJournalExt` is never used as a trait object itself, so adding the associated type has no object-safety impact. --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(anvil): add `AnvilBlockExecutor` and `FoundryReceiptBuilder` (#13788) feat(anvil): add AnvilBlockExecutor and FoundryReceiptBuilder * fix(anvil): swap param order in get_next_block_blob_excess_gas to match callers (#13740) * feat(script): `Network`-generic `ScriptSequence` (#13803) * fix(config): add symmetric serialization for FuzzDictionaryConfig usize fields (#13723) * chore(evm): remove `Env::new_with_spec_id()` method (#13806) * fix(install): clean up nested submodules when using --no-git (#13779) * fix(install): clean up nested submodules when using --no-git Fixes #13688 Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019cf6ab-6839-70a9-98a9-289974db717b * test: mark regression test as flaky_ instead of ignored Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * style: fix fmt Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019cf6ab-6839-70a9-98a9-289974db717b --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * refactor(evm): use associated types in `with_cloned_context` (#13802) refactor(evm): use associated types in `with_cloned_context` closure signature * refactor(evm): propagate env types through `FoundryJournalExt` (#13808) * refactor(evm): simplify `FoundryCfg` to marker trait (#13810) * feat(anvil): add `AnvilBlockExecutorFactory` (#13811) * feat(script): `Network`-generic `ScriptSequenceKind` (#13809) * feature(evm): owned `Tx`/`Evm` getters and `Evm` setter for `FoundryContextExt` (#13812) * chore(evm): remove `Env::{clone_evm_and_tx,apply_evm_and_tx}` methods (#13813) * chore(evm): rename `InspectorExt` to `EthInspectorExt` (#13815) * refactor(evm): add `Fork::backend()` accessor (#13817) * refactor(evm): remove `Env` from `commit_transaction` and `replay_until` (#13816) * feat(script): generic `BundledState` impl (#13825) * chore: bump alloy chains (#13827) * chore(evm): remove `Env` abstraction (#13826) * chore(evm): remove `Env` abstraction completely * fix: nit Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(macros): use correct index for tuple struct fields in ConsoleFmt (#13829) * chore(evm): fix stale `Env` references in doc comments (#13828) The combined `Env` wrapper struct was removed in #13826. Update remaining doc comments that still reference it to use the current `EvmEnv`/`TxEnv` names instead. * refactor(anvil): wire `AnvilBlockExecutorFactory` into `Backend::mine_block` (#13814) refactor(anvil): wire AnvilBlockExecutorFactory into Backend::mine_block * feat(script): generic `ScriptTransactionBuilder` (#13830) Remove hardcoded `Ethereum` default from `TransactionWithMetadata`, making `ScriptTransactionBuilder`, `simulate_and_fill`, and broadcast reader calls `Network`-generic. Cheatcode broadcast helpers now explicitly use Ethereum. * fix(`foundryup`): bump foundryup version (#13832) bump foundryup version * fix(foundryup): tempo-foundry now ships all binaries (#13834) nit * chore(deps): bump taiki-e/install-action from 2.68.17 to 2.68.35 (#13821) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump oven-sh/setup-bun from 2.1.2 to 2.2.0 (#13819) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump Swatinem/rust-cache from 2.8.2 to 2.9.1 (#13818) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump crate-ci/typos from 1.43.5 to 1.44.0 (#13820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor(evm): backend env helpers generic (#13836) Update `update_env_block` and `update_current_env_with_fork_env` to use generic `FoundryBlock`/`FoundryTransaction` bounds and `BlockHeader` trait methods via setters, replacing direct field access on `AnyRpcBlock`. * refactor(anvil): rm `TransactionExecutor`, mv revm callers to `AnvilBlockExecutorFactory` (#13835) refactor(anvil): rm , mv remai callers AnvilBlockExecutorFactory * refactor(script): extract `BrowserSigner` from `MultiWallet` (#13839) * refactor(anvil): mv `Backend` methods to generic impl, thread N through NodeConfig/spawn (#13840) * refactor(anvil): extract `block_env_from_header` utility (#13838) * chore(evm): clean-up `FoundryEvm` impl (#13844) * feat(cheatcodes): add `currentFilePath` cheatcode (#13735) * feat(cheatcodes): add `currentFilePath` cheatcode Add `vm.currentFilePath()` that returns the source file path of the currently running test or script contract, relative to the project root. This enables contracts to locate sibling files (calldata JSONs, markdown descriptions, configs) without hardcoding paths. A common pattern in proposal/script repos is overriding a `dirPath()` function with a hardcoded string — this cheatcode eliminates that boilerplate. Implementation leverages `CheatsConfig::running_artifact` which already tracks the entry-point `ArtifactId`. The `source` field is stripped of the project root prefix to return a consistent relative path. * fix: rustfmt Amp-Thread-ID: https://ampcode.com/threads/T-019cfb9f-8fce-717d-b9de-fedd8ee7d555 Co-authored-by: Amp * fix: remove view from test functions, fix forge-fmt Amp-Thread-ID: https://ampcode.com/threads/T-019cfba7-4986-77c6-9630-574261e9d580 Co-authored-by: Amp --------- Co-authored-by: Alex Netto Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks Co-authored-by: Amp * refactor(evm): simplify nested Evm handling (#13846) refactor(evm): simplify nested EVM handling - Replace `to_env()` (returning cfg+block+tx tuple) with `to_evm_env()` returning only `EvmEnv` (cfg+block), since tx is not needed by callers - Remove unused `with_cloned_context` generic type parameter `R` - Drop `set_tx` call in `with_cloned_context` since nested tx modifications should not propagate back to the outer context - Remove `Evm` useless `db_mut`, `precompiles`, `precompiles_mut`, `inspector`, `inspector_mut` provideds methods reimplementations * refactor(cheatcodes): extract fork env helper to reduce duplication (#13848) Extract `fork_env_op` helper that handles the common pattern shared by all fork-switching cheatcodes: clone EVM/tx env → run db operation → write env back. Deduplicates 7 call sites (rollFork × 4, selectFork, createSelectFork × 2). * refactor(cheatcodes): `BroadcastableTransaction` network-agnostic (#13849) refactor(cheatcodes): make BroadcastableTransaction network-agnostic Remove the `Network` type parameter from `BroadcastableTransaction` by storing raw EVM data (from, to, value, input, nonce, gas) as primitive fields instead of wrapping `TransactionMaybeSigned`. This prevents the `Network` generic from leaking through `Cheatcodes`, `RawCallResult`, and `ScriptResult`. The conversion to network-specific types (`TransactionMaybeSigned`) now happens in the script layer (`simulate.rs`) at the point where transactions are handed off to `ScriptTransactionBuilder`. * feat(evm): generic `NestedEvmClosure` (#13850) refactor(evm): generalize NestedEvmClosure with generic type params Replace `EthNestedEvmClosure` (pinned to Eth types) with generic `NestedEvmClosure` to support non-Eth networks. * feat(evm): `FoundryContextExt` generic impl (#13857) * feat(evm): wire Inspector and DatabaseExt Context generics (#13856) * refactor(anvil): make `PendingTransaction` generic over tx type (#13854) * chore(cheatcodes): remove `Cheatcodes` context generic (#13861) * refactor(evm): delegate to alloy's `EthEvmFactory` in `new_evm_with_inspector` (#13860) * chore: add `id` attributes to issue templates (#13864) * chore: add `id` attributes to issue templates Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d0b22-cf39-75b7-b3d7-9280780eecd5 * chore: add `id` attributes to required issue template fields only Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d0b22-cf39-75b7-b3d7-9280780eecd5 --------- Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * refactor(evm): merge `FoundryJournalExt` into `FoundryContextExt` (#13863) * refactor(evm): merge `FoundryJournalExt` into `FoundryContextExt` * fix: failed merge * fix(traces): fix verbosity trace mode and unify verbosity handling (#13859) * fix(traces): use max instead of min for verbosity trace mode Add regression tests for TraceMode::with_verbosity to ensure verbosity 5 raises the trace mode to at least Steps rather than lowering it. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-Authored-By: James Niken <155266991+dizer-ti@users.noreply.github.com> * fix(traces): unify verbosity handling in TraceMode with_verbosity now raises to RecordStateDiff at verbosity 5, matching the documented behavior of -vvvvv (storage changes + backtraces). This removes the separate with_state_changes(verbosity() > 4) call in trace_mode() which used the global shell verbosity instead of the local evm_opts.verbosity — these two values can diverge when --gas-report or --flamegraph bumps evm_opts.verbosity to 3. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(forge): only show backtraces at verbosity 5 Backtraces require opcode-level step recording which is expensive. Gate backtrace display at verbosity >= 5 (-vvvvv) instead of >= 3, matching the documented behavior and the step recording threshold. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test(traces): comprehensive unit tests for TraceMode verbosity levels Tests every verbosity level (0-5) × every TraceMode variant: - verbosity 0-2: noop across all modes - verbosity 3-4: raises to Call, never downgrades - verbosity 5: raises to RecordStateDiff, never downgrades - into_config correctness at each level (record_steps, record_state_diff) - monotonicity invariant: with_verbosity never lowers any mode Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test(traces): add integration test for backtrace verbosity levels Runs the same failing test at verbosity 1, 3, 4, and 5 with snapshot assertions to verify backtraces only appear at -vvvvv. Removes the monotonicity unit test in favor of concrete integration coverage. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(traces): keep backtraces at verbosity >= 3, add source locations at 5 Backtraces are useful even without source locations — they show contract/function names at -vvv/-vvvv. Source file locations are only added at -vvvvv when step recording is enabled. Updates integration test to assert all three behaviors: - -vvv: backtrace with function names only - -vvvv: same, plus setup traces - -vvvvv: backtrace with source file:line:col locations Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore: fix rustfmt Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(test): handle compiler warnings in snapshot Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(test): add [staticcall] to snapshot for pure function Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * perf(traces): RecordStateDiff should not enable debug-level recording RecordStateDiff now behaves as Steps + state diff, not Debug + state diff. This avoids recording full stack snapshots (memcpy per opcode), memory snapshots, and unfiltered opcode recording at -vvvvv. Before: 50k opcodes → 50k step records with full stack copies (~16MB) After: 50k opcodes → ~500 step records (JUMP/JUMPDEST only), no copies Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * test(traces): assert Debug mode config is unchanged Locks in that Debug mode still enables full memory/stack snapshots, returndata, immediate bytes, and unfiltered opcode recording — ensuring the RecordStateDiff optimization doesn't affect the debugger or cheatcode recording paths. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(traces): state diff needs unfiltered opcodes for SLOAD/SSTORE record_state_diff captures storage changes in the step() callback, which only fires for recorded opcodes. With a JUMP/JUMPDEST filter, SSTORE steps are skipped and state diffs are lost. RecordStateDiff now disables the opcode filter (like before) but still skips memory/stack snapshots — saving the expensive per-opcode memcpy. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(test): update JSON fixture for RecordStateDiff config Stack and memory snapshots are no longer recorded at -vvvvv since RecordStateDiff now uses Steps-level config. These fields are null in the JSON output instead of populated. Full debugger-level data is still available via --debug. Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: James Niken <155266991+dizer-ti@users.noreply.github.com> * refactor(anvil): make Block generic over tx type (#13865) * refactor(evm): `FoundryContextExt` bound, use generic `Spec` in `EthCheatCtx` (#13866) * refactor(evm): use `TxEnv` directly in `DatabaseExt` instead of `TransactionRequest` (#13867) * chore(deps): weekly `cargo update` (#13878) Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> * Update flake.lock (#13877) Co-authored-by: github-actions[bot] * fix(deps): bump to Foundry browser wallet version 0.2.0 (#13890) bump to https://github.com/foundry-rs/foundry-browser-wallet/releases/tag/v0.2.0 * revert: "BroadcastableTransaction network-agnostic" (#13849) (#13891) * perf(anvil): remove redundant clone in create_access_list (#13887) * perf(evm): make `NestedEvm::to_evm_env` consuming to avoid useless clone (#13893) Change `to_evm_env(&self)` to `to_evm_env(self)` so the implementation can use `Evm::finish()` to destructure the EVM without an extra clone of `cfg_env` and `block_env`. Update `with_fresh_nested_evm` to return `EvmEnv` after consuming the EVM post-closure, so callers no longer need to capture the env inside a `NestedEvmClosure` (which only has `&mut dyn NestedEvm` and cannot call a consuming method). Fix the `sub_inner` / `sub_evm_env` ordering in `with_nested_evm` impls so the journal is cloned before `to_evm_env` consumes the EVM. * chore(common): consistency fix on `TransactionMaybeSigned::to()` (#13895) * feat(cheatcodes): Network/Evm generic `Cheatcodes` (#13894) * feat(cheatcodes): Network/Evm generic `Cheatcodes` * fix: tx create detection * fix(ci): adapt to `TransactionMaybeSigned::to()` return type change (#13896) * chore(cheatcodes): remove unused `cheats` param from `CheatcodesExecutor::console_log` (#13897) * refactor(evm): remove `tx_env` param from EVM constructor helper (#13903) Remove the `tx_env` parameter from `new_eth_evm_with_inspector`, `with_cloned_context`, and `with_fresh_nested_evm`. Instead of setting the transaction environment at EVM construction time (where it may become stale or be set redundantly), callers now pass the tx env directly to `evm.transact()` at execution time. This avoids some clones. * chore: remove dead code `paths_config` and `log_status` from inspector stack (#13905) chore: remove dead code in InspectorStackInner Remove two unused methods: - `InspectorStack::paths_config()`: zero callers across the codebase - `InspectorStackInner::log_status()`: zero callers across the codebase Also removes the now-unused `foundry_compilers::ProjectPathsConfig` import. * chore: remove no-op `spec_id` reassignment in `Executor::clone_with_backend` (#13906) chore: remove no-op spec_id reassignment in clone_with_backend After cloning `self.evm_env`, `evm_env.cfg_env.spec` already equals `self.spec_id()` (which simply returns `self.evm_env.cfg_env.spec`). The reassignment is a no-op. * feat(evm): `Backend` generic network (#13579) * feat(evm): `Backend` generic over `Network` * fix(evm): use `AnyNetwork` for fork env init to support non-standard chains When forking non-standard EVM chains (e.g. Moonbeam, Arbitrum), their block headers may lack standard Ethereum fields like `mixHash`. Using the generic `N`-typed provider (defaulting to `Ethereum`) caused deserialization failures for these chains. * feat(evm): add `TryAnyIntoTxEnv` trait for `AnyTxEnvelope` to `TxEnv` conversion Introduces TryAnyIntoTxEnv trait in foundry-evm-core to replace the TxEnv: FromRecoveredTx bound on Backend. This allows Backend to default to AnyNetwork: - TxEnvelope (Ethereum): delegates to FromRecoveredTx directly - AnyTxEnvelope: extracts inner TxEnvelope via as_envelope(), then delegates to FromRecoveredTx; returns error for unknown tx types Co-authored-by: Amp * chore: fix `cargo deny` failure --------- Co-authored-by: Amp * refactor(evm): `NestedEvm` based on revm's `Evm` instead of alloy-evm (#13908) * Add feature: `forge inspect linearization` to see a Solidity contract's linearized inheritance (#13704) * feat: Tempo wallet access key support for cast (#13909) * refactor(script-sequence): rename misleading `opcode` field to `call_kind` (#13907) * fix(fmt): swap valid_before/valid_after in TempoTransaction pretty print (#13910) * fix(fmt): prefer header total_difficulty for totalDifficulty (#13919) * feat(evm): `FoundryContextExt` impls for OP (#13925) * refactor(evm): simplify `NestedEvm` trait by removing `Block` and `Spec` (#13933) * refactor(evm): make `FoundryInspectorExt` generic over `CTX` and rename traits (#13922) * refactor(evm): make `FoundryInspectorExt` generic over `CTX` and rename traits Restructure the inspector extension traits for clarity and genericity: - Rename FoundryInspectorExt -> InspectorExt: the context-free base trait providing Foundry-specific inspector methods (console logging, network config) without any Inspector supertrait. - Rename EthInspectorExt -> FoundryInspectorExt: the combined trait that unifies Inspector + InspectorExt. Generic over any CTX that implements FoundryContextExt, rather than being hardcoded to specific BLOCK/TX/SPEC type parameters with for<'a> HRTB baked in. - Introduce EthEvmCtx<'db> type alias for the concrete Eth context (Context), keeping usage sites concise. This makes the trait hierarchy composable for multi-network support while keeping the Eth-specific default path ergonomic via the type alias during refactor. Co-authored-by: Amp * fix: use `EthEvmContext` instead of EthEvmCtx as default --------- Co-authored-by: Amp * fix(evm): restore `code_size_limit` config in `CfgEnv` (#13912) `cfg_env()` unconditionally set `limit_contract_code_size = Some(usize::MAX)`, ignoring the user's `code_size_limit` setting from foundry.toml. Use the configured value when present, falling back to `usize::MAX` (disabled). * perf(evm): remove unnecessary clones in do_call_end/do_create_end (#13913) perf(evm): remove unnecessary clones in `do_call_end`/`do_create_end` Both methods returned `CallOutcome`/`CreateOutcome` but all callers discarded the return value — the outcome is already mutated in-place via `&mut`. This removes the final `outcome.clone()` and changes the early-return in the `#[ret]` macro from `then_some(outcome.clone())` to `then(|| ())`, eliminating up to 2 clones per call/create end. * fix(wallets): browser wallet CLI help heading formatting (#13876) fix: browser wallet CLI help heading formatting - Set proper help_heading on BrowserWalletOpts to use 'Wallet options - browser wallet' consistently - Add next_help_heading to Erc20TxOpts to prevent transaction options from leaking into the browser wallet section Co-authored-by: Amp Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat(forge-script): Tempo access key support for forge script (#13917) * refactor(anvil): encapsulate per-tx inspector lifecycle in `finish_transaction` (#13932) * refactor(anvil): encapsulate per-tx inspector lifecycle in `finish_transaction` Extract the repeated ~20-line drain-and-reset block from 3 locations in the block mining loop into `AnvilInspector::finish_transaction()`. The method prints traces/logs, drains the tracer into `Vec`, and reinstalls fresh tracer + log collector for the next transaction. Introduces `InspectorTxConfig` to carry the print/tracing settings, replacing direct field access into the inspector internals. * style: fix rustfmt * refactor(evm): generalize `TryAnyToTxEnv` trait over TxEnv + simplification (#13924) * refactor(evm): generalize `TryAnyToTxEnv` trait over TxEnv + simplification The old trait converted `TxEnvelope` (the consensus-layer type) into `TxEnv`, requiring callers to separately pass the sender address. This was a leaky abstraction: the sender is already carried by the RPC transaction wrapper (TransactionResponse::from()), so callers had to extract it themselves before calling `try_into_tx_env`. The new trait `TryAnyToTxEnv` is generic over the output TxEnv type and takes the full RPC transaction as receiver, which means: - Implementations for `alloy_rpc_types::Transaction` and `AnyRpcTransaction` can call ToTxEnv / FromRecoveredTx internally without leaking address handling to callers. - A new impl for `op_alloy_rpc_types::Transaction` produces `OpTransaction`, enabling OP-stack fork replay without a separate code path. - `Backend` now constrains `N::TransactionResponse: TryAnyToTxEnv` instead of `N::TxEnvelope: TryAnyIntoTxEnv`, which is both more accurate and unlocks multi-network support. * fix: docs --------- Co-authored-by: zerosnacks Co-authored-by: Amp * refactor(evm): simplify `replay_until` to use single `ForkDB` clone (#13931) * refactor: simplify replay_until to use single ForkDB clone Replace the per-transaction Backend + EVM creation in replay_until with a single cloned ForkDB and one persistent EthEvm instance. Before: for each tx in the block, commit_transaction would clone Fork + JournaledState, spawn a new Backend (including MultiFork), create a new FoundryEvm, transact, then apply_state_changeset. In a block with N preceding transactions, this meant N full clones. After: clone the fork's CacheDB once (SharedBackend is Arc-backed, so only the local cache layer is duplicated), create one EthEvm via EthEvmFactory, call transact_commit for each tx, then replace the fork's DB and refresh journaled states once at the end. Also: - Remove unused tx_env parameter from replay_until - Extract Fork::refresh_journaled_states helper to deduplicate the paired update_state calls in both replay_until and apply_state_changeset - Remove unused NoOpInspector import Amp-Thread-ID: https://ampcode.com/threads/T-019d2512-8074-72ac-92d8-e8f887911219 Co-authored-by: Amp * style: fix rustfmt * refactor: simplify tx collection loop and remove stale comments --------- Co-authored-by: Amp Co-authored-by: zerosnacks * fix(evm): use try_any_to_tx_env in replay_until (#13938) The merge of #13931 introduced a call to the old `try_into_tx_env` method which was renamed to `try_any_to_tx_env` in #13924. This fixes the docs CI build. Amp-Thread-ID: https://ampcode.com/threads/T-019d2952-9ec2-76f9-8f90-b7b3735d4ce3 Co-authored-by: Amp * chore(deps): bump taiki-e/install-action from 2.68.35 to 2.69.8 (#13915) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.35 to 2.69.8. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/94a7388bec5d4c8dd93e3ebf09e0ff448f3f6f4d...7bc99eee1f1b8902a125006cf790a1f4c8461e63) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.69.8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump DeterminateSystems/determinate-nix-action from 3.17.0 to 3.17.1 (#13914) chore(deps): bump DeterminateSystems/determinate-nix-action Bumps [DeterminateSystems/determinate-nix-action](https://github.com/determinatesystems/determinate-nix-action) from 3.17.0 to 3.17.1. - [Release notes](https://github.com/determinatesystems/determinate-nix-action/releases) - [Commits](https://github.com/determinatesystems/determinate-nix-action/compare/131015bad844610e5e6300f8a143bf625d3e74f4...a18f73c54ca8525de051e73c31512a67f44df919) --- updated-dependencies: - dependency-name: DeterminateSystems/determinate-nix-action dependency-version: 3.17.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor(evm): simplify `FoundryContextExt` spec handling (#13935) - Introduced `FoundryContextExt::Spec` type for convenience - Removed complex bounds on `ContextTr::Cfg` using instead the new `Spec` type - Simplified accordingly the rest of the code Co-authored-by: zerosnacks Co-authored-by: Amp * refactor(anvil): remove some intermediate `Env` wrappers, pass `EvmEnv` directly (#13941) refactor(anvil): remove some intermediate Env wrapper, pass EvmEnv directly Replace usage of the `Env` struct wrapper with direct `EvmEnv` references in `new_eth_evm_with_inspector`, `validate_pool_transaction_for`, and `validate_for`. The optimism flag is now passed explicitly as `is_optimism: bool` instead of being extracted from `env.networks`. This eliminates unnecessary `Env::new(...)` construction in several hot paths (block mining, pending block simulation, tx replay) and simplifies the function signatures. * refactor(anvil): remove Env from storage, call env, and executor APIs (#13942) Continue the cleanup of the Env wrapper by passing EvmEnv directly in remaining call sites: - BlockchainStorage::new / Blockchain::new: drop the redundant spec_id parameter, derive it from evm_env.spec_id() instead - build_call_env: return (EvmEnv, OpTransaction) instead of Env, consumers destructure and use directly - new_eth_evm_with_inspector_ref: accept &EvmEnv instead of &Env, derive is_optimism from self.is_optimism() - build_tx_env_for_pending: take is_optimism: bool instead of NetworkConfigs + &EvmEnv - inspect_tx / replay_block_transactions_with_inspector: extract only evm_env from next_env(), avoid cloning the whole Env - api.rs: use spec_id() / chain_id() helpers instead of digging into env.evm_env.cfg_env fields directly - Various minor cleanups: inline single-use env write guards, use self.chain_id() / self.spec_id() helpers consistently --------- Signed-off-by: dependabot[bot] Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Matthias Seitz Co-authored-by: figtracer <1gusredo@gmail.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Suuuuuuperrrrr fred Co-authored-by: Nikki Co-authored-by: James Niken <155266991+dizer-ti@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alexandro T. Netto <56097505+alextnetto@users.noreply.github.com> Co-authored-by: Alex Netto Co-authored-by: zerosnacks Co-authored-by: Amp Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Edgar Richards Co-authored-by: Red Swan Co-authored-by: onbjerg Co-authored-by: anim001k <140460766+anim001k@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Cleartext logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/evm/traces/src/lib.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/anvil/src/eth/backend/executor.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/cli/src/utils/suggestions.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update crates/common/src/contracts.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: dependabot[bot] Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Co-authored-by: grandizzy Co-authored-by: Rafael Quintero <32346241+rplusq@users.noreply.github.com> Co-authored-by: rplusq Co-authored-by: Claude Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: snyk-io[bot] <141718529+snyk-io[bot]@users.noreply.github.com> Co-authored-by: Gengar Co-authored-by: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Co-authored-by: cakevm Co-authored-by: Matthias Seitz Co-authored-by: Theodore Solis Co-authored-by: tefyosL-sol Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: mattsse <19890894+mattsse@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Desant pivo Co-authored-by: pistomat Co-authored-by: zark <77061323+zarkk01@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com> Co-authored-by: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Co-authored-by: Aganis Co-authored-by: tskoyo Co-authored-by: Matt D Co-authored-by: onbjerg Co-authored-by: Maxim Evtush <154841002+maximevtush@users.noreply.github.com> Co-authored-by: Amp Co-authored-by: Avory Co-authored-by: Yuya Maruyama <69783679+YuyaMaruyama21D4E@users.noreply.github.com> Co-authored-by: Georgios Konstantopoulos Co-authored-by: zerosnacks Co-authored-by: albertov19 <64150856+albertov19@users.noreply.github.com> Co-authored-by: Yero~ Co-authored-by: Philippe Dumonet Co-authored-by: Vicze Osikata Co-authored-by: Mahmoud Lababidi Co-authored-by: Tim Beiko Co-authored-by: Oliver Nordbjerg Co-authored-by: Alvarez <140459501+prestoalvarez@users.noreply.github.com> Co-authored-by: Ninja Co-authored-by: Himess <95512809+Himess@users.noreply.github.com> Co-authored-by: Ninja Co-authored-by: cui Co-authored-by: Mark Fizer Co-authored-by: marukai67 Co-authored-by: sashass1315 Co-authored-by: 0xferrous <0xferrous@proton.me> Co-authored-by: radik878 Co-authored-by: MozirDmitriy Co-authored-by: Maximilian Hubert <64627729+gap-editor@users.noreply.github.com> Co-authored-by: Tran Quang Loc Co-authored-by: James Niken <155266991+dizer-ti@users.noreply.github.com> Co-authored-by: 0xMars42 Co-authored-by: strmfos <155266597+strmfos@users.noreply.github.com> Co-authored-by: bigbear <155267841+aso20455@users.noreply.github.com> Co-authored-by: iPLAY888 <133153661+letmehateu@users.noreply.github.com> Co-authored-by: Valentin B. <703631+beeb@users.noreply.github.com> Co-authored-by: howy <132113803+howydev@users.noreply.github.com> Co-authored-by: Adrian Co-authored-by: kilavvy <140459108+kilavvy@users.noreply.github.com> Co-authored-by: fuder.eth Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: figtracer <1gusredo@gmail.com> Co-authored-by: Tomass <155266802+zeroprooff@users.noreply.github.com> Co-authored-by: Nikki Co-authored-by: Mayank Sharma <82099885+mayanksharma-eth@users.noreply.github.com> Co-authored-by: Mayank Sharma <82099885+codersharma2001@users.noreply.github.com> Co-authored-by: Snezhkko Co-authored-by: anim001k <140460766+anim001k@users.noreply.github.com> Co-authored-by: Forostovec Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: Suuuuuuperrrrr fred Co-authored-by: andrewshab <152420261+andrewshab3@users.noreply.github.com> Co-authored-by: emmmm <155267286+eeemmmmmm@users.noreply.github.com> Co-authored-by: SocksNFlops <91764028+SocksNFlops@users.noreply.github.com> Co-authored-by: Amlandeep Bhadra Co-authored-by: Edgar Richards Co-authored-by: onbjerg <8862627+onbjerg@users.noreply.github.com> Co-authored-by: googleworkspace-bot Co-authored-by: Alexandro T. Netto <56097505+alextnetto@users.noreply.github.com> Co-authored-by: Alex Netto Co-authored-by: Red Swan --- .circleci/cargo.yml | 32 + .circleci/ci-web3-gamefi.yml | 26 + .circleci/ci.yml | 31 + .circleci/ci_cargo.yml | 37 + .circleci/ci_deploy.yml | 34 + .circleci/ci_v1.yml | 31 + .circleci/config.yml | 32 + .circleci/dev_stage.yml | 70 ++ .circleci/web3_defi_gamefi.yml | 26 + .codesandbox/tasks.json | 7 + .deps/remix-tests/remix_accounts.sol | 39 + .deps/remix-tests/remix_tests.sol | 225 +++++ .github/ISSUE_TEMPLATE/bug_report.md | 41 + .github/ISSUE_TEMPLATE/custom.md | 10 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/Docker.yml | 62 ++ .github/workflows/apisec-scan.yml | 29 + .github/workflows/codeql.yml | 92 +++ .github/workflows/deploy.yml | 27 + .github/workflows/docker.yml | 62 ++ .github/workflows/google.yml | 117 +++ .github/workflows/npm.yml | 30 + .github/workflows/snyk-container.yml | 55 ++ .gitmodules | 6 + benches/src/lib.rs | 16 +- counter/.github/workflows/test.yml | 43 + counter/.gitignore | 14 + counter/README.md | 66 ++ counter/foundry.toml | 6 + counter/lib/forge-std | 1 + counter/lib/openzeppelin-contracts | 1 + counter/script/Counter.s.sol | 19 + counter/src/Counter.sol | 14 + counter/test/Counter.t.sol | 24 + crates/cast/tests/cli/main.rs | 28 + crates/cli/src/utils/suggestions.rs | 1 + crates/common/Cargo.toml | 1 + crates/doc/src/parser/comment.rs | 6 + crates/evm/evm/Cargo.toml | 1 + crates/forge/Cargo.toml | 1 + crates/forge/tests/cli/test_optimizer.rs | 1 - crates/lint/src/linter.rs | 129 +++ crates/script/Cargo.toml | 1 + crates/test-utils/src/script.rs | 29 +- crates/test-utils/src/util.rs | 96 ++- crates/wallets/src/tempo.rs | 196 +++++ deny.toml | 7 +- npm/scripts/stage-from-artifact.mjs | 28 +- npm/src/const.mjs | 30 +- sleep.json | 955 ++++++++++++++++++++++ 50 files changed, 2829 insertions(+), 26 deletions(-) create mode 100644 .circleci/cargo.yml create mode 100644 .circleci/ci-web3-gamefi.yml create mode 100644 .circleci/ci.yml create mode 100644 .circleci/ci_cargo.yml create mode 100644 .circleci/ci_deploy.yml create mode 100644 .circleci/ci_v1.yml create mode 100644 .circleci/config.yml create mode 100644 .circleci/dev_stage.yml create mode 100644 .circleci/web3_defi_gamefi.yml create mode 100644 .codesandbox/tasks.json create mode 100644 .deps/remix-tests/remix_accounts.sol create mode 100644 .deps/remix-tests/remix_tests.sol create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/Docker.yml create mode 100644 .github/workflows/apisec-scan.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/google.yml create mode 100644 .github/workflows/snyk-container.yml create mode 100644 .gitmodules create mode 100644 counter/.github/workflows/test.yml create mode 100644 counter/.gitignore create mode 100644 counter/README.md create mode 100644 counter/foundry.toml create mode 160000 counter/lib/forge-std create mode 160000 counter/lib/openzeppelin-contracts create mode 100644 counter/script/Counter.s.sol create mode 100644 counter/src/Counter.sol create mode 100644 counter/test/Counter.t.sol create mode 100644 crates/lint/src/linter.rs create mode 100644 crates/wallets/src/tempo.rs create mode 100644 sleep.json diff --git a/.circleci/cargo.yml b/.circleci/cargo.yml new file mode 100644 index 0000000000000..32b65e6a23cc5 --- /dev/null +++ b/.circleci/cargo.yml @@ -0,0 +1,32 @@ +version: 2.1 +# +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test diff --git a/.circleci/ci-web3-gamefi.yml b/.circleci/ci-web3-gamefi.yml new file mode 100644 index 0000000000000..ad53a8e498202 --- /dev/null +++ b/.circleci/ci-web3-gamefi.yml @@ -0,0 +1,26 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable + auth: + # ensure you have first added these secrets + # visit app.circleci.com/settings/project/github/Dargon789/foundry/environment-variables + username: $DOCKER_HUB_USER + password: $DOCKER_HUB_PASSWORD +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- diff --git a/.circleci/ci.yml b/.circleci/ci.yml new file mode 100644 index 0000000000000..1b5df6d6e668e --- /dev/null +++ b/.circleci/ci.yml @@ -0,0 +1,31 @@ +version: 2.1 +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test diff --git a/.circleci/ci_cargo.yml b/.circleci/ci_cargo.yml new file mode 100644 index 0000000000000..46a18d45a5fca --- /dev/null +++ b/.circleci/ci_cargo.yml @@ -0,0 +1,37 @@ +version: 2.1 + +jobs: + build-and-test: + docker: + - image: cimg/rust:1.88.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + +workflows: + ci: + jobs: + - build-and-test diff --git a/.circleci/ci_deploy.yml b/.circleci/ci_deploy.yml new file mode 100644 index 0000000000000..0c8ae5507187d --- /dev/null +++ b/.circleci/ci_deploy.yml @@ -0,0 +1,34 @@ +version: 2.1 + +jobs: + say-hello: + docker: + - image: cimg/base:current + + steps: + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +workflows: + say-hello-workflow: + jobs: + - say-hello + +- run: + name: Plan a deploy + command: | + circleci run release plan \ + --environment-name="" \ + --component-name="" \ + --target-version="" +# Your job here doing the actual deployment +- run: + name: Update a deploy to SUCCESS + command: circleci run release update --status=SUCCESS + when: on_success +- run: + name: Update planned deploy to FAILED + command: circleci run release update --status=FAILED + when: on_fail diff --git a/.circleci/ci_v1.yml b/.circleci/ci_v1.yml new file mode 100644 index 0000000000000..82c6de5b42b73 --- /dev/null +++ b/.circleci/ci_v1.yml @@ -0,0 +1,31 @@ +version: 2.1 + +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + +workflows: + ci: + jobs: + - build-and-test diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000000..4168efef0971f --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,32 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/reference/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#jobs-overview & https://circleci.com/docs/reference/configuration-reference/#jobs +jobs: + say-hello: + # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/guides/execution-managed/executor-intro/ & https://circleci.com/docs/reference/configuration-reference/#executor-job + docker: + # Specify the version you desire here + # See: https://circleci.com/developer/images/image/cimg/base + - image: cimg/base:current + + # Add steps to the job + # See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#steps-overview & https://circleci.com/docs/reference/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/guides/orchestrate/workflows/ & https://circleci.com/docs/reference/configuration-reference/#workflows +workflows: + say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - say-hello + diff --git a/.circleci/dev_stage.yml b/.circleci/dev_stage.yml new file mode 100644 index 0000000000000..5ba351727d22b --- /dev/null +++ b/.circleci/dev_stage.yml @@ -0,0 +1,70 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- + + jobs: + my-job: + steps: + - run: echo "Hello, world!" + - run: + command: echo "This step will automatically rerun up to 3 times if it fails with a 10 second delay between attempts" + max_auto_reruns: 3 + auto_rerun_delay: 10s + + workflows: + dev_stage_pre-prod: + jobs: + - test_dev: + filters: # using regex filters requires the entire branch to match + branches: + only: # only branches matching the below regex filters will run + - dev + - /user-.*/ + - test_stage: + filters: + branches: + only: stage + - test_pre-prod: + filters: + branches: + only: /pre-prod(?:-.+)?$/ + + + build-test-deploy: + jobs: + - build: + filters: # required since `test` has tag filters AND requires `build` + tags: + only: /^config-test.*/ + - test: + requires: + - build + filters: # required since `deploy` has tag filters AND requires `test` + tags: + only: /^config-test.*/ + - deploy: + requires: + - test + filters: + tags: + only: /^config-test.*/ + branches: + ignore: /.*/ diff --git a/.circleci/web3_defi_gamefi.yml b/.circleci/web3_defi_gamefi.yml new file mode 100644 index 0000000000000..edb6605e3f101 --- /dev/null +++ b/.circleci/web3_defi_gamefi.yml @@ -0,0 +1,26 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable + auth: + # ensure you have first added these secrets + # visit app.circleci.com/settings/project/github/Dargon789/foundry/environment-variables + username: $DOCKER_HUB_USER + password: $DOCKER_HUB_PASSWORD +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- diff --git a/.codesandbox/tasks.json b/.codesandbox/tasks.json new file mode 100644 index 0000000000000..b34104d5de54e --- /dev/null +++ b/.codesandbox/tasks.json @@ -0,0 +1,7 @@ +{ + // These tasks will run in order when initializing your CodeSandbox project. + "setupTasks": [], + + // These tasks can be run from CodeSandbox. Running one will open a log in the app. + "tasks": {} +} diff --git a/.deps/remix-tests/remix_accounts.sol b/.deps/remix-tests/remix_accounts.sol new file mode 100644 index 0000000000000..c1c42dc96b93e --- /dev/null +++ b/.deps/remix-tests/remix_accounts.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.4.22 <0.9.0; + +library TestsAccounts { + function getAccount(uint index) pure public returns (address) { + address[15] memory accounts; + accounts[0] = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; + + accounts[1] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; + + accounts[2] = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db; + + accounts[3] = 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB; + + accounts[4] = 0x617F2E2fD72FD9D5503197092aC168c91465E7f2; + + accounts[5] = 0x17F6AD8Ef982297579C203069C1DbfFE4348c372; + + accounts[6] = 0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678; + + accounts[7] = 0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7; + + accounts[8] = 0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C; + + accounts[9] = 0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC; + + accounts[10] = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; + + accounts[11] = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C; + + accounts[12] = 0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB; + + accounts[13] = 0x583031D1113aD414F02576BD6afaBfb302140225; + + accounts[14] = 0xdD870fA1b7C4700F2BD7f44238821C26f7392148; +return accounts[index]; + } +} diff --git a/.deps/remix-tests/remix_tests.sol b/.deps/remix-tests/remix_tests.sol new file mode 100644 index 0000000000000..b8b9960362203 --- /dev/null +++ b/.deps/remix-tests/remix_tests.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.4.22 <0.9.0; + +library Assert { + + event AssertionEvent( + bool passed, + string message, + string methodName + ); + + event AssertionEventUint( + bool passed, + string message, + string methodName, + uint256 returned, + uint256 expected + ); + + event AssertionEventInt( + bool passed, + string message, + string methodName, + int256 returned, + int256 expected + ); + + event AssertionEventBool( + bool passed, + string message, + string methodName, + bool returned, + bool expected + ); + + event AssertionEventAddress( + bool passed, + string message, + string methodName, + address returned, + address expected + ); + + event AssertionEventBytes32( + bool passed, + string message, + string methodName, + bytes32 returned, + bytes32 expected + ); + + event AssertionEventString( + bool passed, + string message, + string methodName, + string returned, + string expected + ); + + event AssertionEventUintInt( + bool passed, + string message, + string methodName, + uint256 returned, + int256 expected + ); + + event AssertionEventIntUint( + bool passed, + string message, + string methodName, + int256 returned, + uint256 expected + ); + + function ok(bool a, string memory message) public returns (bool result) { + result = a; + emit AssertionEvent(result, message, "ok"); + } + + function equal(uint256 a, uint256 b, string memory message) public returns (bool result) { + result = (a == b); + emit AssertionEventUint(result, message, "equal", a, b); + } + + function equal(int256 a, int256 b, string memory message) public returns (bool result) { + result = (a == b); + emit AssertionEventInt(result, message, "equal", a, b); + } + + function equal(bool a, bool b, string memory message) public returns (bool result) { + result = (a == b); + emit AssertionEventBool(result, message, "equal", a, b); + } + + // TODO: only for certain versions of solc + //function equal(fixed a, fixed b, string message) public returns (bool result) { + // result = (a == b); + // emit AssertionEvent(result, message); + //} + + // TODO: only for certain versions of solc + //function equal(ufixed a, ufixed b, string message) public returns (bool result) { + // result = (a == b); + // emit AssertionEvent(result, message); + //} + + function equal(address a, address b, string memory message) public returns (bool result) { + result = (a == b); + emit AssertionEventAddress(result, message, "equal", a, b); + } + + function equal(bytes32 a, bytes32 b, string memory message) public returns (bool result) { + result = (a == b); + emit AssertionEventBytes32(result, message, "equal", a, b); + } + + function equal(string memory a, string memory b, string memory message) public returns (bool result) { + result = (keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b))); + emit AssertionEventString(result, message, "equal", a, b); + } + + function notEqual(uint256 a, uint256 b, string memory message) public returns (bool result) { + result = (a != b); + emit AssertionEventUint(result, message, "notEqual", a, b); + } + + function notEqual(int256 a, int256 b, string memory message) public returns (bool result) { + result = (a != b); + emit AssertionEventInt(result, message, "notEqual", a, b); + } + + function notEqual(bool a, bool b, string memory message) public returns (bool result) { + result = (a != b); + emit AssertionEventBool(result, message, "notEqual", a, b); + } + + // TODO: only for certain versions of solc + //function notEqual(fixed a, fixed b, string message) public returns (bool result) { + // result = (a != b); + // emit AssertionEvent(result, message); + //} + + // TODO: only for certain versions of solc + //function notEqual(ufixed a, ufixed b, string message) public returns (bool result) { + // result = (a != b); + // emit AssertionEvent(result, message); + //} + + function notEqual(address a, address b, string memory message) public returns (bool result) { + result = (a != b); + emit AssertionEventAddress(result, message, "notEqual", a, b); + } + + function notEqual(bytes32 a, bytes32 b, string memory message) public returns (bool result) { + result = (a != b); + emit AssertionEventBytes32(result, message, "notEqual", a, b); + } + + function notEqual(string memory a, string memory b, string memory message) public returns (bool result) { + result = (keccak256(abi.encodePacked(a)) != keccak256(abi.encodePacked(b))); + emit AssertionEventString(result, message, "notEqual", a, b); + } + + /*----------------- Greater than --------------------*/ + function greaterThan(uint256 a, uint256 b, string memory message) public returns (bool result) { + result = (a > b); + emit AssertionEventUint(result, message, "greaterThan", a, b); + } + + function greaterThan(int256 a, int256 b, string memory message) public returns (bool result) { + result = (a > b); + emit AssertionEventInt(result, message, "greaterThan", a, b); + } + // TODO: safely compare between uint and int + function greaterThan(uint256 a, int256 b, string memory message) public returns (bool result) { + if(b < int(0)) { + // int is negative uint "a" always greater + result = true; + } else { + result = (a > uint(b)); + } + emit AssertionEventUintInt(result, message, "greaterThan", a, b); + } + function greaterThan(int256 a, uint256 b, string memory message) public returns (bool result) { + if(a < int(0)) { + // int is negative uint "b" always greater + result = false; + } else { + result = (uint(a) > b); + } + emit AssertionEventIntUint(result, message, "greaterThan", a, b); + } + /*----------------- Lesser than --------------------*/ + function lesserThan(uint256 a, uint256 b, string memory message) public returns (bool result) { + result = (a < b); + emit AssertionEventUint(result, message, "lesserThan", a, b); + } + + function lesserThan(int256 a, int256 b, string memory message) public returns (bool result) { + result = (a < b); + emit AssertionEventInt(result, message, "lesserThan", a, b); + } + // TODO: safely compare between uint and int + function lesserThan(uint256 a, int256 b, string memory message) public returns (bool result) { + if(b < int(0)) { + // int is negative int "b" always lesser + result = false; + } else { + result = (a < uint(b)); + } + emit AssertionEventUintInt(result, message, "lesserThan", a, b); + } + + function lesserThan(int256 a, uint256 b, string memory message) public returns (bool result) { + if(a < int(0)) { + // int is negative int "a" always lesser + result = true; + } else { + result = (uint(a) < b); + } + emit AssertionEventIntUint(result, message, "lesserThan", a, b); + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000..edd3e4a15ddbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. Chrome, Safari] + - Version [e.g. 22] + - Browser [e.g. Chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, Safari] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000000000..48d5f81fa4229 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000..bbcbbe7d61558 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/Docker.yml b/.github/workflows/Docker.yml new file mode 100644 index 0000000000000..5a2330e7d5d62 --- /dev/null +++ b/.github/workflows/Docker.yml @@ -0,0 +1,62 @@ +name: Docker + +on: + push: + tags: ["*"] + branches: + - "main" + pull_request: + branches: ["**"] + +env: + # Hostname of your registry + REGISTRY: docker.io + # Image repository, without hostname and tag + IMAGE_NAME: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.event.after }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + # Authenticate to the container registry + - name: Authenticate to registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.revision=${{ env.SHA }} + tags: | + type=edge,branch=$repo.default_branch + type=semver,pattern=v{{version}} + type=sha,prefix=,suffix=,format=short + + # Build and push Docker image with Buildx + # (don't push on PR, load instead) + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + sbom: ${{ github.event_name != 'pull_request' }} + provenance: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' }} + load: ${{ github.event_name == 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/apisec-scan.yml b/.github/workflows/apisec-scan.yml new file mode 100644 index 0000000000000..e716760284792 --- /dev/null +++ b/.github/workflows/apisec-scan.yml @@ -0,0 +1,29 @@ +name: APIsec +permissions: + contents: read + +on: + pull_request: + branches: + - main + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run APIsec scan + uses: apisec-inc/apisec-run-scan@025432089674a28ba8fb55f8ab06c10215e772ea + with: + apisec-username: ${{ secrets.APISEC_USERNAME }} + apisec-password: ${{ secrets.APISEC_PASSWORD }} + apisec-project: VAmPI + apisec-profile: Master + apisec-region: us-east-1 + sarif-result-file: apisec-results.sarif + apisec-email-report: true + apisec-fail-on-vuln-severity: critical + apisec-oas: false + apisec-openapi-spec-url: "https://example.com/openapi.json" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000000..5bf742c565e0f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '25 9 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000000..1ab3e63e39815 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,27 @@ +name: Foundry Build & Deploy +permissions: + contents: read +on: + push: + branches: [main, master] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Build project + run: cargo build --release + + - name: Run tests + run: cargo test + + - name: Docker build + run: docker build -t foundryg-rs diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000000..5a2330e7d5d62 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,62 @@ +name: Docker + +on: + push: + tags: ["*"] + branches: + - "main" + pull_request: + branches: ["**"] + +env: + # Hostname of your registry + REGISTRY: docker.io + # Image repository, without hostname and tag + IMAGE_NAME: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.event.after }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + # Authenticate to the container registry + - name: Authenticate to registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.revision=${{ env.SHA }} + tags: | + type=edge,branch=$repo.default_branch + type=semver,pattern=v{{version}} + type=sha,prefix=,suffix=,format=short + + # Build and push Docker image with Buildx + # (don't push on PR, load instead) + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + sbom: ${{ github.event_name != 'pull_request' }} + provenance: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' }} + load: ${{ github.event_name == 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/google.yml b/.github/workflows/google.yml new file mode 100644 index 0000000000000..1295e430ca96a --- /dev/null +++ b/.github/workflows/google.yml @@ -0,0 +1,117 @@ +# This workflow will build a docker container, publish it to Google Container +# Registry, and deploy it to GKE when there is a push to the "main" +# branch. +# +# To configure this workflow: +# +# 1. Enable the following Google Cloud APIs: +# +# - Artifact Registry (artifactregistry.googleapis.com) +# - Google Kubernetes Engine (container.googleapis.com) +# - IAM Credentials API (iamcredentials.googleapis.com) +# +# You can learn more about enabling APIs at +# https://support.google.com/googleapi/answer/6158841. +# +# 2. Ensure that your repository contains the necessary configuration for your +# Google Kubernetes Engine cluster, including deployment.yml, +# kustomization.yml, service.yml, etc. +# +# 3. Create and configure a Workload Identity Provider for GitHub: +# https://github.com/google-github-actions/auth#preferred-direct-workload-identity-federation. +# +# Depending on how you authenticate, you will need to grant an IAM principal +# permissions on Google Cloud: +# +# - Artifact Registry Administrator (roles/artifactregistry.admin) +# - Kubernetes Engine Developer (roles/container.developer) +# +# You can learn more about setting IAM permissions at +# https://cloud.google.com/iam/docs/manage-access-other-resources +# +# 5. Change the values in the "env" block to match your values. + +name: 'Build and Deploy to GKE' + +on: + push: + branches: + - '"main"' + - '"master"' + +env: + PROJECT_ID: 'my-project' # TODO: update to your Google Cloud project ID + GAR_LOCATION: 'us-central1' # TODO: update to your region + GKE_CLUSTER: 'cluster-1' # TODO: update to your cluster name + GKE_ZONE: 'us-central1-c' # TODO: update to your cluster zone + DEPLOYMENT_NAME: 'gke-test' # TODO: update to your deployment name + REPOSITORY: 'samples' # TODO: update to your Artifact Registry docker repository name + IMAGE: 'static-site' + WORKLOAD_IDENTITY_PROVIDER: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' # TODO: update to your workload identity provider + +jobs: + setup-build-publish-deploy: + name: 'Setup, Build, Publish, and Deploy' + runs-on: 'ubuntu-latest' + environment: 'production' + + permissions: + contents: 'read' + id-token: 'write' + + steps: + - name: 'Checkout' + uses: 'actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332' # actions/checkout@v4 + + # Configure Workload Identity Federation and generate an access token. + # + # See https://github.com/google-github-actions/auth for more options, + # including authenticating via a JSON credentials file. + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@f112390a2df9932162083945e46d439060d66ec2' # google-github-actions/auth@v2 + with: + workload_identity_provider: '${{ env.WORKLOAD_IDENTITY_PROVIDER }}' + + # Authenticate Docker to Google Cloud Artifact Registry + - name: 'Docker Auth' + uses: 'docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567' # docker/login-action@v3 + with: + username: 'oauth2accesstoken' + password: '${{ steps.auth.outputs.auth_token }}' + registry: '${{ env.GAR_LOCATION }}-docker.pkg.dev' + + # Get the GKE credentials so we can deploy to the cluster + - name: 'Set up GKE credentials' + uses: 'google-github-actions/get-gke-credentials@6051de21ad50fbb1767bc93c11357a49082ad116' # google-github-actions/get-gke-credentials@v2 + with: + cluster_name: '${{ env.GKE_CLUSTER }}' + location: '${{ env.GKE_ZONE }}' + + # Build the Docker image + - name: 'Build and push Docker container' + run: |- + DOCKER_TAG="${GAR_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE}:${GITHUB_SHA}" + + docker build \ + --tag "${DOCKER_TAG}" \ + --build-arg GITHUB_SHA="${GITHUB_SHA}" \ + --build-arg GITHUB_REF="${GITHUB_REF}" \ + . + + docker push "${DOCKER_TAG}" + + # Set up kustomize + - name: 'Set up Kustomize' + run: |- + curl -sfLo kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz + chmod u+x ./kustomize + + # Deploy the Docker image to the GKE cluster + - name: 'Deploy to GKE' + run: |- + # replacing the image name in the k8s template + ./kustomize edit set image LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:TAG=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:$GITHUB_SHA + ./kustomize build . | kubectl apply -f - + kubectl rollout status deployment/$DEPLOYMENT_NAME + kubectl get services -o wide diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 323059e99e6b6..e3a5b385a28a3 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -137,6 +137,36 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id || inputs.run_id }} + - name: Validate Downloaded Artifacts + env: + ARTIFACT_DIR: ${{ steps.paths.outputs.artifact_dir }} + run: | + set -euo pipefail + + echo "Validating artifacts in: $ARTIFACT_DIR" + + if [[ ! -d "$ARTIFACT_DIR" ]]; then + echo "ERROR: Artifact directory does not exist: $ARTIFACT_DIR" >&2 + exit 1 + fi + + if ! find "$ARTIFACT_DIR" -mindepth 1 -print -quit | grep -q .; then + echo "ERROR: Artifact directory is empty: $ARTIFACT_DIR" >&2 + exit 1 + fi + + # Reject files with suspicious paths (absolute paths or parent directory traversals) + # Use null-delimited paths to safely handle filenames with newlines or whitespace + while IFS= read -r -d '' path; do + rel="${path#"$ARTIFACT_DIR"/}" + if [[ "$rel" == /* ]] || [[ "$rel" == *".."* ]]; then + echo "ERROR: Suspicious artifact path detected: $rel" >&2 + exit 1 + fi + done < <(find "$ARTIFACT_DIR" -type f -print0) + + echo "Artifact validation completed successfully." + - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: diff --git a/.github/workflows/snyk-container.yml b/.github/workflows/snyk-container.yml new file mode 100644 index 0000000000000..f07df9c75c8d1 --- /dev/null +++ b/.github/workflows/snyk-container.yml @@ -0,0 +1,55 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# +# A sample workflow which checks out the code, builds a container +# image using Docker and scans that image for vulnerabilities using +# Snyk. The results are then uploaded to GitHub Security Code Scanning +# +# For more examples, including how to limit scans to only high-severity +# issues, monitor images for newly disclosed vulnerabilities in Snyk and +# fail PR checks for new vulnerabilities, see https://github.com/snyk/actions/ + +name: Snyk Container + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '30 10 * * 1' + +permissions: + contents: read + +jobs: + snyk: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Build a Docker image + run: docker build -t your/image-to-test . + - name: Run Snyk to check Docker image for vulnerabilities + # Snyk can be used to break the build when it detects vulnerabilities. + # In this case we want to upload the issues to GitHub Code Scanning + continue-on-error: true + uses: snyk/actions/docker@9adf32b1121593767fc3c057af55b55db032dc04 + env: + # In order to use the Snyk Action you will need to have a Snyk API token. + # More details in https://github.com/snyk/actions#getting-your-snyk-token + # or you can signup for free at https://snyk.io/login + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + image: your/image-to-test + args: --file=Dockerfile + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: snyk.sarif diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000..b1269653d9c6f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "counter/lib/forge-std"] + path = counter/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "counter/lib/openzeppelin-contracts"] + path = counter/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 50c7afae2ddec..7ed8807cbf0f5 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -141,10 +141,20 @@ impl BenchmarkProject { for entry in std::fs::read_dir(&root_path)? { let entry = entry?; let path = entry.path(); - if path.is_dir() { - std::fs::remove_dir_all(&path).ok(); + // Canonicalize the entry to prevent directory traversal + let canon = match path.canonicalize() { + Ok(p) => p, + Err(_) => continue, // Skip if unable to canonicalize + }; + // Ensure canonicalized path stays strictly within root_path (TempProject root) + if !canon.starts_with(&root_path) { + sh_eprintln!("⚠️ Skipping suspicious path during cleanup: {:?}", canon); + continue; + } + if canon.is_dir() { + std::fs::remove_dir_all(&canon).ok(); } else { - std::fs::remove_file(&path).ok(); + std::fs::remove_file(&canon).ok(); } } diff --git a/counter/.github/workflows/test.yml b/counter/.github/workflows/test.yml new file mode 100644 index 0000000000000..34a4a527be6f9 --- /dev/null +++ b/counter/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/counter/.gitignore b/counter/.gitignore new file mode 100644 index 0000000000000..85198aaa55b84 --- /dev/null +++ b/counter/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/counter/README.md b/counter/README.md new file mode 100644 index 0000000000000..679a7f4518035 --- /dev/null +++ b/counter/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose Solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/counter/foundry.toml b/counter/foundry.toml new file mode 100644 index 0000000000000..25b918f9c9a96 --- /dev/null +++ b/counter/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/counter/lib/forge-std b/counter/lib/forge-std new file mode 160000 index 0000000000000..3b20d60d14b34 --- /dev/null +++ b/counter/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/counter/lib/openzeppelin-contracts b/counter/lib/openzeppelin-contracts new file mode 160000 index 0000000000000..ca7a4e39de086 --- /dev/null +++ b/counter/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit ca7a4e39de0860bbaadf95824207886e6de9fa64 diff --git a/counter/script/Counter.s.sol b/counter/script/Counter.s.sol new file mode 100644 index 0000000000000..cdc1fe9a1ba25 --- /dev/null +++ b/counter/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/counter/src/Counter.sol b/counter/src/Counter.sol new file mode 100644 index 0000000000000..aded7997b0c35 --- /dev/null +++ b/counter/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/counter/test/Counter.t.sol b/counter/test/Counter.t.sol new file mode 100644 index 0000000000000..54b724f7ae766 --- /dev/null +++ b/counter/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 2f744efe4d4f0..9071614e3a7a9 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -3158,6 +3158,34 @@ Traces: "#]]); }); +// tests that displays a sample beacon block traces in Cancun +// https://github.com/foundry-rs/foundry/issues/12435 +casttest!(test_beacon_block_root_in_cancun, |prj, cmd| { + prj.clear(); + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args([ + "run", + "0xae290fe8c89c3e83dff20eeb2b8e3261bcdce0d66441c7056918dfb5fafe6d96", + "--rpc-url", + eth_rpc_url.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +Executing previous transactions from the block. +Traces: + [45054] 0xB731392c0EB5BF2092f9f7B520DA551f70Ea9131::Claim{value: 46698476594582387}() + ├─ [4320] 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02::00000000(00000000000000000000000000000000000000000000000069091d4b) [staticcall] + │ └─ ← [Return] 0x70c7855161ec07af782df915fb3e81702df40f34972da3d740cdfc132ac926f6 + ├─ emit NvStuck(param0: 0x6e6C36B970f8862bA3F148DEdAB8F98f5ed8b426, param1: 46698476594582387 [4.669e16], param2: 1762205003 [1.762e9]) + └─ ← [Stop] + + +Transaction successfully executed. +[GAS] + +"#]]); +}); + // tests that displays a sample contract artifact // casttest!(flaky_fetch_artifact_from_etherscan, |_prj, cmd| { diff --git a/crates/cli/src/utils/suggestions.rs b/crates/cli/src/utils/suggestions.rs index a675ccae963c9..82a14a3b24beb 100644 --- a/crates/cli/src/utils/suggestions.rs +++ b/crates/cli/src/utils/suggestions.rs @@ -1,4 +1,5 @@ //! Helper functions for suggesting alternative values for a possibly erroneous user input. +use std::cmp::Ordering; /// Filters multiple strings from a given list of possible values which are similar /// to the passed in value `v` within a certain confidence by least confidence. diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index a66a0fef2fe0a..6921faabcb102 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -17,6 +17,7 @@ foundry-block-explorers = { workspace = true, features = ["foundry-compilers"] } foundry-common-fmt.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true +foundry-primitives.workspace = true alloy-chains.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } diff --git a/crates/doc/src/parser/comment.rs b/crates/doc/src/parser/comment.rs index 42b91ccc366aa..e70f47e174a81 100644 --- a/crates/doc/src/parser/comment.rs +++ b/crates/doc/src/parser/comment.rs @@ -205,6 +205,12 @@ impl Comments { } } +impl From> for Comments { + fn from(value: Vec) -> Self { + Self(value) + } +} + /// The collection of references to natspec [Comment] items. #[derive(Debug, Default, PartialEq, Eq, Deref)] pub struct CommentsRef<'a>(Vec<&'a Comment>); diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 5dbf07c7a356c..dd5138f532074 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -28,6 +28,7 @@ foundry-evm-traces.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } alloy-json-abi.workspace = true +alloy-network.workspace = true alloy-primitives = { workspace = true, features = [ "serde", "getrandom", diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 667da6b442ca1..4e83eb168abca 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -62,6 +62,7 @@ alloy-primitives = { workspace = true, features = ["serde"] } alloy-provider = { workspace = true, features = ["reqwest", "ws", "ipc"] } alloy-signer.workspace = true alloy-transport.workspace = true +alloy-hardforks.workspace = true tempo-alloy.workspace = true diff --git a/crates/forge/tests/cli/test_optimizer.rs b/crates/forge/tests/cli/test_optimizer.rs index c744ff6457596..223549b08b048 100644 --- a/crates/forge/tests/cli/test_optimizer.rs +++ b/crates/forge/tests/cli/test_optimizer.rs @@ -1374,7 +1374,6 @@ Compiling 21 files with [..] }); // Test preprocessed contracts with decode internal fns. -#[cfg(not(feature = "isolate-by-default"))] forgetest_init!(preprocess_contract_with_decode_internal, |prj, cmd| { prj.initialize_default_contracts(); prj.update_config(|config| { diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs new file mode 100644 index 0000000000000..2c11e0222a286 --- /dev/null +++ b/crates/lint/src/linter.rs @@ -0,0 +1,129 @@ +use foundry_compilers::Language; +use foundry_config::lint::Severity; +use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, VariableDefinition}; +use solar_interface::{ + data_structures::Never, + diagnostics::{DiagBuilder, DiagId, MultiSpan}, + Session, Span, +}; +use std::{ops::ControlFlow, path::PathBuf}; + +/// Trait representing a generic linter for analyzing and reporting issues in smart contract source +/// code files. A linter can be implemented for any smart contract language supported by Foundry. +/// +/// # Type Parameters +/// +/// - `Language`: Represents the target programming language. Must implement the [`Language`] trait. +/// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`] +/// trait. +/// +/// # Required Methods +/// +/// - `lint`: Scans the provided source files emitting a daignostic for lints found. +pub trait Linter: Send + Sync + Clone { + type Language: Language; + type Lint: Lint; + + fn lint(&self, input: &[PathBuf]); +} + +pub trait Lint { + fn id(&self) -> &'static str; + fn severity(&self) -> Severity; + fn description(&self) -> &'static str; + fn help(&self) -> &'static str; +} + +pub struct LintContext<'s> { + sess: &'s Session, + desc: bool, +} + +impl<'s> LintContext<'s> { + pub fn new(sess: &'s Session, with_description: bool) -> Self { + Self { sess, desc: with_description } + } + + // Helper method to emit diagnostics easily from passes + pub fn emit(&self, lint: &'static L, span: Span) { + let desc = if self.desc { lint.description() } else { "" }; + let diag: DiagBuilder<'_, ()> = self + .sess + .dcx + .diag(lint.severity().into(), desc) + .code(DiagId::new_str(lint.id())) + .span(MultiSpan::from_span(span)) + .help(lint.help()); + + diag.emit(); + } +} + +/// Trait for lints that operate directly on the AST. +/// Its methods mirror `solar_ast::visit::Visit`, with the addition of `LintCotext`. +pub trait EarlyLintPass<'ast>: Send + Sync { + fn check_expr(&mut self, _ctx: &LintContext<'_>, _expr: &'ast Expr<'ast>) {} + fn check_item_struct(&mut self, _ctx: &LintContext<'_>, _struct: &'ast ItemStruct<'ast>) {} + fn check_item_function(&mut self, _ctx: &LintContext<'_>, _func: &'ast ItemFunction<'ast>) {} + fn check_variable_definition( + &mut self, + _ctx: &LintContext<'_>, + _var: &'ast VariableDefinition<'ast>, + ) { + } + + // TODO: Add methods for each required AST node type +} + +/// Visitor struct for `EarlyLintPass`es +pub struct EarlyLintVisitor<'a, 's, 'ast> { + pub ctx: &'a LintContext<'s>, + pub passes: &'a mut [Box + 's>], +} + +impl<'s, 'ast> Visit<'ast> for EarlyLintVisitor<'_, 's, 'ast> +where + 's: 'ast, +{ + type BreakValue = Never; + + fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_expr(self.ctx, expr) + } + self.walk_expr(expr) + } + + fn visit_variable_definition( + &mut self, + var: &'ast VariableDefinition<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_variable_definition(self.ctx, var) + } + self.walk_variable_definition(var) + } + + fn visit_item_struct( + &mut self, + strukt: &'ast ItemStruct<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_item_struct(self.ctx, strukt) + } + self.walk_item_struct(strukt) + } + + fn visit_item_function( + &mut self, + func: &'ast ItemFunction<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_item_function(self.ctx, func) + } + self.walk_item_function(func) + } + + // TODO: Add methods for each required AST node type, mirroring `solar_ast::visit::Visit` method + // sigs + adding `LintContext` +} diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 6de71571ac7eb..d243814c4b148 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -56,6 +56,7 @@ alloy-primitives.workspace = true alloy-eips.workspace = true alloy-consensus.workspace = true thiserror.workspace = true +tempo-alloy.workspace = true tempo-alloy.workspace = true tempo-primitives.workspace = true diff --git a/crates/test-utils/src/script.rs b/crates/test-utils/src/script.rs index 06f83886d0fd2..c1a6cb53bdbff 100644 --- a/crates/test-utils/src/script.rs +++ b/crates/test-utils/src/script.rs @@ -121,9 +121,32 @@ impl ScriptTester { let to_dir = root.join("utils"); fs::create_dir_all(&to_dir)?; for entry in fs::read_dir(&from_dir)? { - let file = &entry?.path(); - let name = file.file_name().unwrap(); - fs::copy(file, to_dir.join(name))?; + let file = entry?.path(); + // Only operate on regular files to avoid following symlinks or directories + let metadata = fs::symlink_metadata(&file)?; + let ftype = metadata.file_type(); + if !ftype.is_file() { + continue; + } + let name = match file.file_name() { + Some(name) => name, + None => continue, + }; + // Validate file name to avoid path traversal and absolute paths + let name_str = name.to_string_lossy(); + if name_str.contains("..") || name_str.contains("/") || name_str.contains("\\") { + // Skip invalid (potentially dangerous) file names + continue; + } + // Verify canonicalized file is in from_dir to avoid symlink traversal + if let Ok(canonical_file) = file.canonicalize() { + if !canonical_file.starts_with(&from_dir) { + continue; + } + } else { + continue; + } + fs::copy(&file, to_dir.join(name))?; } Ok(()) } diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 48489e43e34d9..fa065425b5281 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -3,12 +3,28 @@ use foundry_config::Config; use std::{ env, fs::{self, File}, - io::{Read, Seek, Write}, + io::{self, IsTerminal, Read, Seek, Write}, path::{Path, PathBuf}, process::Command, sync::LazyLock, }; +/// Base directory under which all test utility filesystem operations are constrained. +/// Using a fixed directory under the system temp dir avoids trusting the current +/// working directory (which may be user-controlled) as a security boundary. +static TEST_UTIL_BASE: LazyLock = LazyLock::new(|| { + // Resolve the system temp directory to an absolute, canonical path where possible. + // If canonicalization fails for any reason, fall back to the raw temp_dir value. + let tmp = env::temp_dir(); + let mut base = tmp + .canonicalize() + .unwrap_or(tmp); + base.push("foundry_test_utils"); + // Ignore errors here; they will surface when the path is actually used. + let _ = fs::create_dir_all(&base); + base +}); + /// Directories to skip when copying project directories. /// These are build artifacts and runtime-generated files that should not be copied to temp /// workspaces. @@ -19,6 +35,9 @@ pub use crate::{ext::*, prj::*}; /// The commit of forge-std to use. pub const FORGE_STD_REVISION: &str = include_str!("../../../testdata/forge-std-rev"); +/// Stores whether `stdout` is a tty / terminal. +pub static IS_TTY: LazyLock = LazyLock::new(|| std::io::stdout().is_terminal()); + /// Global default template path. Contains the global template project from which all other /// temp projects are initialized. See [`initialize()`] for more info. static TEMPLATE_PATH: LazyLock = @@ -146,7 +165,9 @@ pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput { out = project.compile().unwrap(); test_debug!("compiled {}", lock_file_path.display()); - assert!(!out.has_compiler_errors(), "Compiled with errors:\n{out}"); + if out.has_compiler_errors() { + panic!("Compiled with errors:\n{out}"); + } if let Some(write) = &mut write { write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap(); @@ -174,7 +195,7 @@ pub fn get_vyper() -> Vyper { let path = VYPER.as_path(); let mut file = File::create(path).unwrap(); if let Err(e) = file.try_lock() { - if matches!(e, fs::TryLockError::WouldBlock) { + if let fs::TryLockError::WouldBlock = e { file.lock().unwrap(); assert!(path.exists()); return Vyper::new(path).unwrap(); @@ -230,22 +251,77 @@ pub fn read_string(path: impl AsRef) -> String { /// like `out/`, `cache/`, and `broadcast/` which are build artifacts that should not be /// copied to temporary test workspaces. pub fn copy_dir_filtered(src: &Path, dst: &Path) -> std::io::Result<()> { - fs::create_dir_all(dst)?; - copy_dir_filtered_inner(src, dst, true) + let src = resolve_and_validate_under_base(src)?; + let dst = resolve_and_validate_under_base(dst)?; + + fs::create_dir_all(&dst)?; + copy_dir_filtered_inner(&src, &dst, true) +} + +/// Resolve a path against a safe base directory and ensure it does not escape that base. +/// +/// This guards against using uncontrolled paths that could traverse outside the intended +/// workspace (for example, via `..` components or absolute paths). +fn resolve_and_validate_under_base(path: &Path) -> io::Result { + // Use a fixed base directory for test utilities instead of the current working + // directory, which may be influenced by the environment. + let base = TEST_UTIL_BASE.clone(); + + // If `path` is absolute, interpret it relative to the base by stripping the + // root and joining the remaining components. This avoids treating arbitrary + // absolute paths as trustworthy. + let joined = if path.is_absolute() { + let relative_components = path.components().filter_map(|c| { + use std::path::Component; + match c { + Component::Normal(p) => Some(PathBuf::from(p)), + // Skip root and current-dir components; preserve parent-dir so that + // canonicalization below can detect and resolve them safely. + Component::RootDir | Component::CurDir => None, + Component::ParentDir => Some(PathBuf::from("..")), + Component::Prefix(_) => None, + } + }); + let mut rel = PathBuf::new(); + for c in relative_components { + rel.push(c); + } + base.join(rel) + } else { + base.join(path) + }; + + let canonical = joined.canonicalize()?; + if !canonical.starts_with(&base) { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "path escapes allowed base directory", + )); + } + + Ok(canonical) } fn copy_dir_filtered_inner(src: &Path, dst: &Path, is_root: bool) -> std::io::Result<()> { - for entry in fs::read_dir(src)? { + // Ensure that each recursion step operates on paths that are constrained to the + // configured base directory. This guarantees that any `src_path` passed to + // filesystem operations cannot escape the allowed workspace even if the initial + // input was influenced by the user. + let src = resolve_and_validate_under_base(src)?; + let dst = resolve_and_validate_under_base(dst)?; + + for entry in fs::read_dir(&src)? { let entry = entry?; let ty = entry.file_type()?; - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); + let name = entry.file_name(); + let src_path = src.join(&name); + let dst_path = dst.join(&name); if ty.is_dir() { // Skip build artifact directories at the root level if is_root - && let Some(name) = entry.file_name().to_str() - && SKIP_DIRS.contains(&name) + && let Some(name_str) = name.to_str() + && SKIP_DIRS.contains(&name_str) { continue; } diff --git a/crates/wallets/src/tempo.rs b/crates/wallets/src/tempo.rs new file mode 100644 index 0000000000000..a86b568fdea2b --- /dev/null +++ b/crates/wallets/src/tempo.rs @@ -0,0 +1,196 @@ +use alloy_eips::Encodable2718; +use alloy_primitives::{Address, hex}; +use alloy_rlp::Decodable; +use alloy_signer::Signer; +use eyre::Result; +use std::path::PathBuf; +use tempo_alloy::rpc::TempoTransactionRequest; +use tempo_primitives::transaction::{ + KeychainSignature, PrimitiveSignature, SignedKeyAuthorization, TempoSignature, +}; + +use crate::{WalletSigner, utils}; + +/// Wallet type: how this wallet was created. +#[derive(Clone, Copy, Default, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum WalletType { + #[default] + Local, + Passkey, +} + +/// Cryptographic key type. +#[derive(Clone, Copy, Default, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum KeyType { + #[default] + Secp256k1, + P256, + WebAuthn, +} + +/// A single entry from Tempo's `keys.toml`. +#[derive(serde::Deserialize)] +#[allow(dead_code)] +struct KeyEntry { + #[serde(default)] + wallet_type: WalletType, + #[serde(default)] + wallet_address: Address, + #[serde(default)] + chain_id: u64, + #[serde(default)] + key_type: KeyType, + #[serde(default)] + key_address: Option
, + #[serde(default)] + key: Option, + #[serde(default)] + key_authorization: Option, + #[serde(default)] + expiry: Option, + #[serde(default)] + limits: Vec, +} + +/// Per-token spending limit stored in `keys.toml`. +#[derive(serde::Deserialize)] +struct StoredTokenLimit { + #[allow(dead_code)] + currency: Address, + #[allow(dead_code)] + limit: String, +} + +/// The top-level structure of `~/.tempo/wallet/keys.toml`. +#[derive(serde::Deserialize)] +struct KeysFile { + #[serde(default)] + keys: Vec, +} + +/// Configuration for a Tempo access key (keychain mode). +/// +/// When a Tempo wallet entry uses keychain mode (`wallet_address != key_address`), the signer +/// is an access key that signs on behalf of the root wallet. This struct carries the metadata +/// needed to construct the correct transaction. +#[derive(Debug, Clone)] +pub struct TempoAccessKeyConfig { + /// The root wallet address (the `from` address for transactions). + pub wallet_address: Address, + /// The access key's address (derived from the private key that actually signs). + pub key_address: Address, + /// Decoded key authorization for on-chain provisioning. + /// + /// When present, callers should check whether the key is already provisioned on-chain + /// (via the AccountKeychain precompile) before including this in a transaction. + pub key_authorization: Option, +} + +/// Result of looking up an address in Tempo's key store. +pub enum TempoLookup { + /// A direct (EOA) signer was found — `wallet_address == key_address`. + Direct(WalletSigner), + /// A keychain (access key) signer was found — `wallet_address != key_address`. + Keychain(WalletSigner, Box), + /// No matching entry was found. + NotFound, +} + +/// Returns the path to Tempo's keys file. +/// +/// Respects `TEMPO_HOME` env var, defaulting to `~/.tempo`. +fn keys_path() -> Option { + let base = std::env::var_os("TEMPO_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|h| h.join(".tempo")))?; + Some(base.join("wallet").join("keys.toml")) +} + +/// Decodes a hex-encoded, RLP-encoded [`SignedKeyAuthorization`]. +fn decode_key_authorization(hex_str: &str) -> Result { + let bytes = hex::decode(hex_str)?; + let auth = SignedKeyAuthorization::decode(&mut bytes.as_slice())?; + Ok(auth) +} + +/// Looks up a signer for the given address in Tempo's `keys.toml`. +/// +/// Returns [`TempoLookup::Direct`] if a direct-mode (EOA) key is found, +/// [`TempoLookup::Keychain`] if a keychain-mode access key is found, +/// or [`TempoLookup::NotFound`] if no entry matches. +pub fn lookup_signer(from: Address) -> Result { + let path = match keys_path() { + Some(p) if p.is_file() => p, + _ => return Ok(TempoLookup::NotFound), + }; + + let contents = std::fs::read_to_string(&path)?; + let file: KeysFile = toml::from_str(&contents)?; + + for entry in &file.keys { + if entry.wallet_address != from { + continue; + } + + let Some(key) = &entry.key else { + continue; + }; + + // Direct mode: wallet_address == key_address (or key_address is absent). + let is_direct = + entry.key_address.is_none() || entry.key_address == Some(entry.wallet_address); + + let signer = utils::create_private_key_signer(key)?; + + if is_direct { + return Ok(TempoLookup::Direct(signer)); + } + + // Keychain mode: the key is an access key signing on behalf of wallet_address. + let key_authorization = + entry.key_authorization.as_deref().map(decode_key_authorization).transpose()?; + + let config = TempoAccessKeyConfig { + wallet_address: entry.wallet_address, + // SAFETY: `is_direct` was false, so `key_address` is `Some` and != wallet_address + key_address: entry.key_address.unwrap(), + key_authorization, + }; + return Ok(TempoLookup::Keychain(signer, Box::new(config))); + } + + Ok(TempoLookup::NotFound) +} + +/// Signs a Tempo transaction request using an access key (keychain V2 mode). +/// +/// Bypasses the standard `EthereumWallet` signing path and instead: +/// 1. Builds the `TempoTransaction` from the request +/// 2. Computes the V2 keychain signing hash +/// 3. Signs with the access key +/// 4. Wraps in a `KeychainSignature` and encodes to EIP-2718 wire format +pub async fn sign_with_access_key( + tx_request: impl Into, + signer: &impl Signer, + wallet_address: Address, +) -> Result> { + let tx_request: TempoTransactionRequest = tx_request.into(); + let tempo_tx = tx_request + .build_aa() + .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?; + + let sig_hash = tempo_tx.signature_hash(); + let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address); + let raw_sig = signer.sign_hash(&signing_hash).await?; + + let keychain_sig = + KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig)); + let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig)); + + let mut buf = Vec::new(); + aa_signed.encode_2718(&mut buf); + + Ok(buf) +} diff --git a/deny.toml b/deny.toml index 1a0e1e8e53005..0f891df4bfbfe 100644 --- a/deny.toml +++ b/deny.toml @@ -100,7 +100,10 @@ unknown-git = "deny" allow-git = [ "https://github.com/alloy-rs/alloy", "https://github.com/alloy-rs/evm", + "https://github.com/foundry-rs/compilers", + "https://github.com/foundry-rs/foundry-fork-db", "https://github.com/foundry-rs/foundry-core", + "https://github.com/foundry-rs/optimism", "https://github.com/paradigmxyz/revm-inspectors", "https://github.com/paradigmxyz/solar", "https://github.com/bluealloy/revm", @@ -111,7 +114,5 @@ allow-git = [ "https://github.com/tempoxyz/mpp-rs", # Transitive dependency of Tempo "https://github.com/paradigmxyz/reth", - "https://github.com/paradigmxyz/reth-core", - # Temporary: upstream OP crates until release is published. - "https://github.com/ethereum-optimism/optimism", + "https://github.com/stevencartavia/reth", ] diff --git a/npm/scripts/stage-from-artifact.mjs b/npm/scripts/stage-from-artifact.mjs index c1ca22c8bb2ed..1d39fdc82e84f 100755 --- a/npm/scripts/stage-from-artifact.mjs +++ b/npm/scripts/stage-from-artifact.mjs @@ -64,10 +64,10 @@ function resolveArgs() { strict: true }) - const tool = requireValue(values.tool || process.env.TARGET_TOOL, 'tool') - const platform = requireValue(values.platform || process.env.PLATFORM_NAME, 'platform') - const arch = requireValue(values.arch || process.env.ARCH, 'arch') - const releaseVersion = requireValue( + const tool = requireSafeIdentifier(values.tool || process.env.TARGET_TOOL, 'tool') + const platform = requireSafeIdentifier(values.platform || process.env.PLATFORM_NAME, 'platform') + const arch = requireSafeIdentifier(values.arch || process.env.ARCH, 'arch') + const releaseVersion = requireSafeIdentifier( values.release || values['release-version'] || process.env.RELEASE_VERSION, 'release version' ) @@ -95,6 +95,26 @@ function requireValue(value, name) { throw new Error(`Missing required ${name}`) } +/** + * Ensure a required value is present and consists only of safe identifier + * characters suitable for use in file and directory names. + * + * Allowed characters: letters, digits, dot, underscore, and hyphen. + * + * @param {string | undefined} value + * @param {string} name + * @returns {string} + */ +function requireSafeIdentifier(value, name) { + const trimmed = requireValue(value, name) + if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) { + throw new Error( + `Invalid ${name}: "${trimmed}". Only letters, digits, ".", "_", and "-" are allowed.` + ) + } + return trimmed +} + /** * Determine which archive variant exists for the given artifact prefix. * @param {string} prefix diff --git a/npm/src/const.mjs b/npm/src/const.mjs index 6b3dcf3f9fbed..e606759888acb 100644 --- a/npm/src/const.mjs +++ b/npm/src/const.mjs @@ -1,4 +1,5 @@ import * as NodePath from 'node:path' +import { URL } from 'node:url' /** * @typedef {'amd64' | 'arm64'} Arch @@ -33,11 +34,36 @@ export function resolveTargetTool(raw = process.env.TARGET_TOOL || process.argv[ export function getRegistryUrl() { // Prefer npm's configured registry (works with Verdaccio and custom registries) // Fallback to REGISTRY_URL for tests/dev, then npmjs - return ( + const raw = process.env.npm_config_registry || process.env.REGISTRY_URL || 'https://registry.npmjs.org' - ) + + let parsed + try { + parsed = new URL(raw) + } catch { + throw new Error(`Invalid registry URL: "${raw}"`) + } + + // Enforce secure scheme + if (parsed.protocol !== 'https:') { + throw new Error(`Insecure registry URL scheme "${parsed.protocol}". Only "https:" is allowed.`) + } + + // Basic SSRF mitigation: disallow obvious loopback hosts + const hostname = parsed.hostname.toLowerCase() + if ( + hostname === 'localhost' + || hostname === '127.0.0.1' + || hostname === '::1' + ) { + throw new Error(`Registry URL host "${parsed.hostname}" is not allowed.`) + } + + // Normalize to a consistent base URL without trailing slash + const base = parsed.origin + parsed.pathname + return base.replace(/\/+$/, '') } /** diff --git a/sleep.json b/sleep.json new file mode 100644 index 0000000000000..5b430e1e663f6 --- /dev/null +++ b/sleep.json @@ -0,0 +1,955 @@ +{ + "results": [ + { + "command": "sleep 0.020", + "mean": 0.023726515413333333, + "stddev": 0.004602014051751124, + "median": 0.02267755758, + "user": 0.0013185473333333334, + "system": 0.0020899164444444446, + "min": 0.02109890308, + "max": 0.05602819808, + "times": [ + 0.02856005608, + 0.02346135008, + 0.02202502208, + 0.02139558708, + 0.02265920408, + 0.02121691608, + 0.02272505608, + 0.02114247908, + 0.02157142808, + 0.021514666079999998, + 0.02161920108, + 0.02335035008, + 0.02224331408, + 0.02228639708, + 0.02152537208, + 0.021732302079999998, + 0.02273370308, + 0.02115513608, + 0.02268494308, + 0.02244547308, + 0.023943647079999998, + 0.02324528508, + 0.02152617908, + 0.023991903079999998, + 0.02250884108, + 0.02342551708, + 0.02113216608, + 0.02168223108, + 0.02222267508, + 0.02273532108, + 0.02273995308, + 0.05602819808, + 0.02501500608, + 0.03121396008, + 0.02424400108, + 0.02459129108, + 0.02633760708, + 0.02377406808, + 0.02365474708, + 0.02406064008, + 0.02300910408, + 0.02437339208, + 0.02317403908, + 0.02257532008, + 0.02267017208, + 0.02356714508, + 0.02367204808, + 0.02258227108, + 0.02330384008, + 0.02225645108, + 0.02478414908, + 0.02484724308, + 0.02270765708, + 0.02339114708, + 0.02450795908, + 0.02348840008, + 0.044674490080000004, + 0.028041754080000002, + 0.022940745079999998, + 0.02259975308, + 0.022112378079999998, + 0.02271348408, + 0.02320266708, + 0.02284982108, + 0.02244050908, + 0.02238655808, + 0.022084648079999998, + 0.02241669808, + 0.02523103408, + 0.02256237908, + 0.03532525108, + 0.02232798408, + 0.02173793008, + 0.021903001079999998, + 0.02288046308, + 0.02368652508, + 0.02211418708, + 0.02265551308, + 0.02187778308, + 0.02191395108, + 0.02182523808, + 0.02185612208, + 0.02109890308, + 0.02294132008, + 0.02191512608, + 0.02264461208, + 0.02227651108, + 0.02307147508, + 0.02227169708, + 0.02177434208 + ], + "memory_usage_byte": [ + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "command": "sleep 0.021", + "mean": 0.022889189941111117, + "stddev": 0.0007161191938371117, + "median": 0.02280623708, + "user": 0.0009166992592592593, + "system": 0.0016941181481481477, + "min": 0.02132554808, + "max": 0.02453766808, + "times": [ + 0.02311599608, + 0.02274468508, + 0.02193879008, + 0.02158843608, + 0.02329398008, + 0.02379494508, + 0.02260801308, + 0.02439507908, + 0.02448522508, + 0.02403379508, + 0.02298143008, + 0.02263027308, + 0.02229235308, + 0.02335063508, + 0.02377098008, + 0.02269184108, + 0.023631199079999998, + 0.02338021508, + 0.02198521708, + 0.02251586208, + 0.022295963079999998, + 0.02226397608, + 0.02453766808, + 0.02184453408, + 0.02289659908, + 0.02382663208, + 0.02347397108, + 0.02225926308, + 0.02207640608, + 0.02243237108, + 0.02278192608, + 0.02270514808, + 0.02245069008, + 0.023018867079999998, + 0.02399866208, + 0.02236840708, + 0.02366382208, + 0.02294188908, + 0.02155127708, + 0.02294999808, + 0.02132554808, + 0.02242025908, + 0.02202766108, + 0.02182175108, + 0.02272186608, + 0.02211805308, + 0.02319764908, + 0.022308045079999998, + 0.02345400908, + 0.022437877079999998, + 0.02273417808, + 0.02217370908, + 0.02254318408, + 0.023269922079999998, + 0.02384951108, + 0.02419476108, + 0.02439866908, + 0.02354840508, + 0.02304219108, + 0.02354960608, + 0.02382648708, + 0.02345751208, + 0.02367913708, + 0.02253067208, + 0.02215132608, + 0.022603942079999998, + 0.02284062808, + 0.02252907808, + 0.02220393508, + 0.023291509079999998, + 0.02399456908, + 0.02407123208, + 0.02279175108, + 0.02300624708, + 0.02309500408, + 0.023036532079999998, + 0.02303833108, + 0.02316846908, + 0.02228349608, + 0.02247140608, + 0.022482600079999998, + 0.02370720808, + 0.02220123708, + 0.02230588608, + 0.02333678708, + 0.02153336008, + 0.02203071908, + 0.02279195108, + 0.02353659108, + 0.02267460708, + 0.022536274079999998, + 0.022769262079999998, + 0.02314857808, + 0.02194885908, + 0.02355038408, + 0.02320035308, + 0.02307451408, + 0.02379926408, + 0.02330480208, + 0.02257055708, + 0.02330320308, + 0.02303003208, + 0.02327859908, + 0.02171311608, + 0.02282052308, + 0.02170123708, + 0.02254831308, + 0.02235855408 + ], + "memory_usage_byte": [ + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "command": "sleep 0.022", + "mean": 0.02415569324504855, + "stddev": 0.0009830972994273135, + "median": 0.02409406108, + "user": 0.001165289514563107, + "system": 0.001767603883495146, + "min": 0.02243173808, + "max": 0.02755932908, + "times": [ + 0.02456728108, + 0.02650439708, + 0.02480475408, + 0.02452974808, + 0.02300978308, + 0.02521451608, + 0.02543841408, + 0.02538411108, + 0.02475773908, + 0.02403843308, + 0.02426362708, + 0.02326921708, + 0.02447185308, + 0.02361749008, + 0.02410661008, + 0.02371481508, + 0.02327300908, + 0.02430165908, + 0.02328269108, + 0.02315262608, + 0.02380195808, + 0.02283639508, + 0.02491355808, + 0.02401717008, + 0.02556049408, + 0.02350359508, + 0.02400529208, + 0.02533555808, + 0.02467923308, + 0.02478442308, + 0.02422068708, + 0.02352175108, + 0.02481882108, + 0.02456148108, + 0.02314905108, + 0.024188183079999998, + 0.02483985908, + 0.02289141308, + 0.02364977308, + 0.02354907008, + 0.02379135508, + 0.026812933079999997, + 0.023360627079999998, + 0.02331436308, + 0.02504176308, + 0.02358805508, + 0.02409406108, + 0.02350689508, + 0.02303628508, + 0.02430972408, + 0.02516170908, + 0.02352843108, + 0.02274564308, + 0.02345165808, + 0.02429327308, + 0.02252948108, + 0.02445868508, + 0.02755932908, + 0.02522621808, + 0.02491753008, + 0.022858510079999998, + 0.02401968108, + 0.02409596908, + 0.02390450108, + 0.02373108808, + 0.027211489079999998, + 0.02537487108, + 0.02319182608, + 0.02390569508, + 0.02490164708, + 0.02384732708, + 0.02243173808, + 0.02367003008, + 0.02494288308, + 0.02436298308, + 0.02390639308, + 0.02423030808, + 0.02430082908, + 0.02320845908, + 0.02421546708, + 0.02530823508, + 0.02368935308, + 0.02306283708, + 0.023536658079999998, + 0.02359881208, + 0.02438320308, + 0.02477724008, + 0.02362231908, + 0.02419465008, + 0.02596891608, + 0.02307578608, + 0.02459456508, + 0.02384055408, + 0.02421387408, + 0.02510733208, + 0.02473580508, + 0.02243970708, + 0.02253156008, + 0.02550018108, + 0.02440877608, + 0.02281331608, + 0.02354148408, + 0.02352098308 + ], + "memory_usage_byte": [ + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +} From 1175872ab3ae417ede7dce3d6d4c1cc160a492be Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Thu, 7 May 2026 08:42:20 +0700 Subject: [PATCH 12/14] ci: sign release archives, docker images, and publish SBOMs --- .circleci/cargo.yml | 32 + .circleci/ci-web3-gamefi.yml | 26 + .circleci/ci.yml | 31 + .circleci/ci_cargo.yml | 37 + .circleci/ci_v1.yml | 31 + .circleci/dev_stage.yml | 70 + .circleci/web3_defi_gamefi.yml | 26 + .github/CODEOWNERS | 2 +- .github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md | 10 + .github/scripts/commit-and-read-benchmarks.sh | 114 -- .github/scripts/commit-benchmark-results.sh | 75 + .github/scripts/compare-nightly.sh | 56 + .github/scripts/read-benchmark-results.sh | 37 + .github/scripts/tempo-check.sh | 86 +- .github/workflows/benchmarks-nightly.yml | 217 +++ .github/workflows/benchmarks.yml | 73 +- .github/workflows/ci-tempo.yml | 91 +- .github/workflows/ci.yml | 18 + .github/workflows/crate-checks.yml | 2 +- .github/workflows/docker-publish.yml | 30 + .github/workflows/nix.yml | 4 +- .github/workflows/npm.yml | 34 +- .github/workflows/release.yml | 78 +- .github/workflows/test-flaky.yml | 2 +- .github/workflows/test-isolate.yml | 2 +- .github/workflows/test.yml | 4 +- Cargo.lock | 1024 +++++------ README.md | 2 + SECURITY.md | 109 ++ benches/LATEST.md | 134 +- benches/src/main.rs | 40 +- benches/src/results.rs | 19 + benchmark.sh | 60 +- crates/anvil/Cargo.toml | 34 +- crates/anvil/core/Cargo.toml | 10 +- crates/anvil/src/cmd.rs | 27 +- crates/anvil/src/config.rs | 6 +- crates/anvil/src/eth/api.rs | 44 +- crates/anvil/src/eth/backend/executor.rs | 23 +- crates/anvil/src/eth/backend/mem/mod.rs | 267 ++- crates/anvil/src/eth/backend/mem/optimism.rs | 61 + .../anvil/src/eth/{error.rs => error/mod.rs} | 69 +- crates/anvil/src/eth/error/optimism.rs | 62 + crates/anvil/src/eth/otterscan/api.rs | 19 +- crates/anvil/src/eth/pool/transactions.rs | 4 +- crates/anvil/src/eth/sign.rs | 8 +- crates/anvil/src/{evm.rs => evm/mod.rs} | 84 +- crates/anvil/src/evm/optimism.rs | 87 + crates/anvil/src/lib.rs | 3 + crates/anvil/tests/it/main.rs | 2 + crates/anvil/tests/it/revert.rs | 50 + crates/cast/Cargo.toml | 17 +- crates/cast/src/args.rs | 7 + crates/cast/src/cmd/batch_mktx.rs | 27 +- crates/cast/src/cmd/batch_send.rs | 32 +- crates/cast/src/cmd/call.rs | 25 +- crates/cast/src/cmd/keychain.rs | 1022 ++++++++++- crates/cast/src/cmd/mktx.rs | 44 +- crates/cast/src/cmd/mod.rs | 3 + crates/cast/src/cmd/run.rs | 17 +- crates/cast/src/cmd/send.rs | 72 +- crates/cast/src/cmd/tempo.rs | 45 + crates/cast/src/cmd/tip20/mine.rs | 23 +- crates/cast/src/cmd/tip20/mod.rs | 2 +- crates/cast/src/cmd/vaddr/create.rs | 181 ++ crates/cast/src/cmd/vaddr/mod.rs | 131 ++ crates/cast/src/cmd/vaddr/resolve.rs | 52 + crates/cast/src/cmd/vaddr/watch.rs | 108 ++ crates/cast/src/cmd/wallet/mod.rs | 13 +- crates/cast/src/lib.rs | 4 +- crates/cast/src/opts.rs | 25 +- crates/cast/src/tx.rs | 30 +- crates/cast/tests/cli/keychain.rs | 76 + crates/cast/tests/cli/main.rs | 119 ++ crates/cheatcodes/Cargo.toml | 10 + crates/cheatcodes/assets/cheatcodes.json | 120 +- crates/cheatcodes/spec/src/vm.rs | 122 +- crates/cheatcodes/src/inspector.rs | 145 +- crates/cheatcodes/src/test/assert.rs | 14 +- crates/cheatcodes/src/version.rs | 67 +- crates/chisel/Cargo.toml | 9 +- crates/chisel/src/executor.rs | 1617 ++++++++--------- crates/chisel/src/source.rs | 562 ++---- crates/chisel/tests/it/repl/mod.rs | 20 + crates/cli/Cargo.toml | 7 + crates/cli/src/opts/evm.rs | 11 + crates/cli/src/opts/rpc.rs | 56 +- crates/cli/src/opts/rpc_common.rs | 7 +- crates/cli/src/opts/tempo.rs | 320 +++- crates/cli/src/utils/tempo.rs | 193 +- crates/common/Cargo.toml | 20 +- crates/common/build.rs | 20 +- crates/common/fmt/Cargo.toml | 8 +- crates/common/fmt/src/ui.rs | 8 + crates/common/src/contracts.rs | 14 +- crates/common/src/provider/mpp/keys.rs | 73 +- crates/common/src/provider/mpp/session.rs | 10 + crates/common/src/provider/mpp/transport.rs | 922 +++++++++- crates/common/src/provider/mpp/ws.rs | 4 + .../common/src/provider/runtime_transport.rs | 6 +- crates/common/src/tempo/auth.rs | 494 +++++ crates/common/src/tempo/mod.rs | 186 ++ crates/common/src/transactions/builder.rs | 57 +- crates/common/src/transactions/receipt.rs | 2 + crates/config/src/fuzz.rs | 6 + crates/config/src/inline/mod.rs | 39 + crates/config/src/lib.rs | 83 +- crates/config/src/providers/warnings.rs | 5 +- crates/debugger/Cargo.toml | 8 + crates/doc/Cargo.toml | 6 +- crates/doc/src/builder.rs | 40 +- crates/doc/src/helpers.rs | 91 - crates/doc/src/lib.rs | 7 +- crates/doc/src/parser/comment.rs | 74 +- crates/doc/src/parser/item.rs | 136 +- crates/doc/src/parser/mod.rs | 407 +++-- crates/doc/src/parser/source.rs | 172 ++ .../src/preprocessor/contract_inheritance.rs | 12 +- .../doc/src/preprocessor/infer_hyperlinks.rs | 17 +- crates/doc/src/preprocessor/inheritdoc.rs | 3 +- crates/doc/src/solang_ext/ast_eq.rs | 708 -------- crates/doc/src/solang_ext/loc.rs | 168 -- crates/doc/src/solang_ext/mod.rs | 31 - crates/doc/src/solang_ext/safe_unwrap.rs | 52 - crates/doc/src/solang_ext/visit.rs | 621 ------- crates/doc/src/writer/as_doc.rs | 69 +- crates/doc/src/writer/traits.rs | 51 +- crates/evm/core/Cargo.toml | 24 +- crates/evm/core/src/decode.rs | 4 +- crates/evm/core/src/env.rs | 608 ++++--- crates/evm/core/src/evm/mod.rs | 21 +- crates/evm/core/src/evm/op.rs | 22 +- crates/evm/core/src/fork/database.rs | 53 +- crates/evm/core/src/lib.rs | 3 + crates/evm/core/src/opts.rs | 7 +- crates/evm/coverage/Cargo.toml | 4 + crates/evm/evm/Cargo.toml | 13 + crates/evm/evm/src/executors/fuzz/mod.rs | 125 +- crates/evm/evm/src/executors/invariant/mod.rs | 2 +- crates/evm/fuzz/Cargo.toml | 9 + crates/evm/fuzz/src/lib.rs | 35 +- crates/evm/hardforks/Cargo.toml | 8 +- crates/evm/hardforks/src/lib.rs | 74 +- crates/evm/networks/Cargo.toml | 8 +- crates/evm/networks/src/lib.rs | 273 ++- crates/evm/networks/src/optimism.rs | 25 + crates/evm/traces/Cargo.toml | 4 + crates/fmt/Cargo.toml | 4 + crates/fmt/src/state/mod.rs | 2 +- .../bracket-spacing.fmt.sol | 6 + .../contract-new-lines.fmt.sol | 9 + .../fmt/testdata/ContractDefinition/fmt.sol | 9 + .../testdata/ContractDefinition/original.sol | 3 + crates/forge/Cargo.toml | 15 +- crates/forge/assets/tempo/MailTemplate.s.sol | 2 +- crates/forge/assets/tempo/MailTemplate.t.sol | 2 +- crates/forge/src/cmd/coverage.rs | 7 +- crates/forge/src/cmd/create.rs | 49 +- crates/forge/src/cmd/snapshot.rs | 7 +- crates/forge/src/cmd/test/mod.rs | 238 ++- crates/forge/src/cmd/test/summary.rs | 4 +- crates/forge/src/gas_report.rs | 2 +- crates/forge/src/multi_runner.rs | 32 + crates/forge/src/runner.rs | 24 +- crates/forge/tests/cli/cmd.rs | 127 +- crates/forge/tests/cli/config.rs | 31 +- crates/forge/tests/cli/ext_integration.rs | 14 +- crates/forge/tests/cli/failure_assertions.rs | 7 +- crates/forge/tests/cli/inline_config.rs | 104 ++ crates/forge/tests/cli/lint.rs | 289 ++- crates/forge/tests/cli/lint/geiger.rs | 10 +- crates/forge/tests/cli/script.rs | 2 +- crates/forge/tests/cli/test_cmd/fuzz.rs | 145 ++ .../tests/cli/test_cmd/invariant/common.rs | 2 +- .../forge/tests/cli/test_cmd/invariant/mod.rs | 12 +- crates/forge/tests/cli/test_cmd/repros.rs | 60 + .../tests/fixtures/ExpectRevertFailures.t.sol | 57 + crates/lint/Cargo.toml | 4 + crates/lint/README.md | 12 + crates/lint/docs/README.md | 52 + crates/lint/docs/_template.md | 28 + crates/lint/docs/asm-keccak256.md | 42 + crates/lint/docs/block-timestamp.md | 44 + crates/lint/docs/boolean-cst.md | 37 + crates/lint/docs/boolean-equal.md | 34 + crates/lint/docs/could-be-immutable.md | 42 + crates/lint/docs/custom-errors.md | 45 + crates/lint/docs/divide-before-multiply.md | 32 + crates/lint/docs/erc20-unchecked-transfer.md | 43 + crates/lint/docs/incorrect-erc20-interface.md | 42 + .../lint/docs/incorrect-erc721-interface.md | 48 + crates/lint/docs/incorrect-shift.md | 37 + crates/lint/docs/inline-assembly.md | 69 + crates/lint/docs/interface-file-naming.md | 31 + crates/lint/docs/interface-naming.md | 31 + crates/lint/docs/missing-zero-check.md | 39 + crates/lint/docs/mixed-case-function.md | 32 + crates/lint/docs/mixed-case-variable.md | 36 + crates/lint/docs/multi-contract-file.md | 37 + crates/lint/docs/named-struct-fields.md | 31 + crates/lint/docs/pascal-case-struct.md | 31 + crates/lint/docs/pragma-inconsistent.md | 41 + crates/lint/docs/rtlo.md | 32 + .../lint/docs/screaming-snake-case-const.md | 30 + .../docs/screaming-snake-case-immutable.md | 31 + crates/lint/docs/too-many-digits.md | 32 + crates/lint/docs/tx-origin.md | 34 + crates/lint/docs/unaliased-plain-import.md | 34 + crates/lint/docs/unchecked-call.md | 34 + crates/lint/docs/unsafe-cheatcode.md | 35 + crates/lint/docs/unsafe-typecast.md | 40 + crates/lint/docs/unused-import.md | 40 + crates/lint/docs/unused-state-variables.md | 39 + crates/lint/docs/unwrapped-modifier-logic.md | 51 + crates/lint/src/linter/late.rs | 1 + crates/lint/src/linter/mod.rs | 34 +- crates/lint/src/linter/project.rs | 92 + crates/lint/src/sol/gas/immutable.rs | 406 +++++ crates/lint/src/sol/gas/mod.rs | 11 +- .../src/sol/gas/unused_state_variables.rs | 90 + crates/lint/src/sol/high/mod.rs | 5 +- crates/lint/src/sol/high/rtlo.rs | 58 + crates/lint/src/sol/info/boolean_cst.rs | 116 ++ crates/lint/src/sol/info/boolean_equal.rs | 108 ++ crates/lint/src/sol/info/inline_assembly.rs | 71 + crates/lint/src/sol/info/mod.rs | 20 + crates/lint/src/sol/info/pragma_directive.rs | 71 + crates/lint/src/sol/info/too_many_digits.rs | 50 + crates/lint/src/sol/macros.rs | 42 +- crates/lint/src/sol/med/mod.rs | 4 + crates/lint/src/sol/med/tx_origin.rs | 101 + crates/lint/src/sol/mod.rs | 156 +- crates/lint/testdata/BlockTimestamp.stderr | 24 +- crates/lint/testdata/BooleanCst.sol | 25 + crates/lint/testdata/BooleanCst.stderr | 40 + crates/lint/testdata/BooleanEqual.sol | 24 + crates/lint/testdata/BooleanEqual.stderr | 56 + crates/lint/testdata/CouldBeImmutable.sol | 85 + crates/lint/testdata/CouldBeImmutable.stderr | 56 + crates/lint/testdata/CustomErrors.stderr | 10 +- .../lint/testdata/DivideBeforeMultiply.stderr | 12 +- crates/lint/testdata/Imports.stderr | 26 +- .../testdata/IncorrectERC20Interface.stderr | 30 +- .../testdata/IncorrectERC721Interface.stderr | 38 +- crates/lint/testdata/IncorrectShift.stderr | 10 +- crates/lint/testdata/InlineAssembly.sol | 110 ++ crates/lint/testdata/InlineAssembly.stderr | 96 + crates/lint/testdata/Keccak256.sol | 1 + crates/lint/testdata/Keccak256.stderr | 42 +- crates/lint/testdata/MissingZeroCheck.stderr | 46 +- crates/lint/testdata/MixedCase.stderr | 38 +- crates/lint/testdata/MultiContractFile.stderr | 10 +- .../MultiContractFile_InterfaceLibrary.stderr | 6 +- crates/lint/testdata/NamedStructFields.stderr | 2 +- .../PragmaInconsistentCaretAboveExact.sol | 7 + .../PragmaInconsistentCaretAboveExact.stderr | 16 + .../PragmaInconsistentCaretMatchesExact.sol | 7 + ...PragmaInconsistentCaretMatchesExact.stderr | 16 + .../PragmaInconsistentCaretVsTilde.sol | 7 + .../PragmaInconsistentCaretVsTilde.stderr | 16 + .../testdata/PragmaInconsistentOrVsExact.sol | 7 + .../PragmaInconsistentOrVsExact.stderr | 16 + .../PragmaInconsistentRangeVsExact.sol | 7 + .../PragmaInconsistentRangeVsExact.stderr | 16 + .../PragmaInconsistentThreeDistinct.sol | 8 + .../PragmaInconsistentThreeDistinct.stderr | 24 + crates/lint/testdata/Rtlo.sol | 81 + crates/lint/testdata/Rtlo.stderr | 192 ++ crates/lint/testdata/RtloCommentsOnly.sol | 15 + crates/lint/testdata/RtloCommentsOnly.stderr | 32 + .../lint/testdata/ScreamingSnakeCase.stderr | 16 +- crates/lint/testdata/StructPascalCase.stderr | 12 +- crates/lint/testdata/TooManyDigits.sol | 73 + crates/lint/testdata/TooManyDigits.stderr | 72 + crates/lint/testdata/TxOrigin.sol | 65 + crates/lint/testdata/TxOrigin.stderr | 72 + crates/lint/testdata/UncheckedCall.stderr | 16 +- .../testdata/UncheckedTransferERC20.stderr | 22 +- crates/lint/testdata/UnsafeCheatcodes.stderr | 26 +- crates/lint/testdata/UnsafeTypecast.stderr | 330 ++-- crates/lint/testdata/UnusedStateVariables.sol | 52 + .../lint/testdata/UnusedStateVariables.stderr | 40 + .../testdata/UnwrappedModifierLogic.stderr | 22 +- crates/primitives/Cargo.toml | 17 +- crates/primitives/src/network/mod.rs | 10 +- crates/primitives/src/network/optimism.rs | 47 + crates/primitives/src/network/receipt.rs | 40 +- crates/primitives/src/transaction/envelope.rs | 281 +-- crates/primitives/src/transaction/mod.rs | 6 +- crates/primitives/src/transaction/optimism.rs | 300 +++ crates/primitives/src/transaction/receipt.rs | 114 +- crates/primitives/src/transaction/request.rs | 110 +- crates/script-sequence/Cargo.toml | 4 + crates/script/Cargo.toml | 12 + crates/script/src/broadcast.rs | 143 +- crates/script/src/lib.rs | 131 +- crates/script/src/runner.rs | 10 +- crates/script/src/verify.rs | 2 +- crates/sol-macro-gen/Cargo.toml | 4 + crates/test-utils/Cargo.toml | 4 + crates/test-utils/src/util.rs | 2 +- crates/verify/Cargo.toml | 9 + crates/wallets/src/tempo.rs | 196 ++ deny.toml | 7 +- docs/dev/lintrules.md | 2 + flake.lock | 18 +- foundryup/README.md | 4 +- foundryup/foundryup | 127 +- sleep.json | 955 ++++++++++ testdata/default/cheats/ExpectRevert.t.sol | 85 + testdata/default/cheats/Fork2.t.sol | 1 + .../default/cheats/GetFoundryVersion.t.sol | 51 + testdata/default/cheats/MockCall.t.sol | 41 +- testdata/default/cheats/MockCalls.t.sol | 4 + testdata/forge-std-rev | 2 +- testdata/utils/Vm.sol | 116 +- 316 files changed, 17238 insertions(+), 6495 deletions(-) create mode 100644 .circleci/cargo.yml create mode 100644 .circleci/ci-web3-gamefi.yml create mode 100644 .circleci/ci.yml create mode 100644 .circleci/ci_cargo.yml create mode 100644 .circleci/ci_v1.yml create mode 100644 .circleci/dev_stage.yml create mode 100644 .circleci/web3_defi_gamefi.yml create mode 100644 .github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md delete mode 100755 .github/scripts/commit-and-read-benchmarks.sh create mode 100644 .github/scripts/commit-benchmark-results.sh create mode 100644 .github/scripts/compare-nightly.sh create mode 100644 .github/scripts/read-benchmark-results.sh create mode 100644 .github/workflows/benchmarks-nightly.yml create mode 100644 crates/anvil/src/eth/backend/mem/optimism.rs rename crates/anvil/src/eth/{error.rs => error/mod.rs} (91%) create mode 100644 crates/anvil/src/eth/error/optimism.rs rename crates/anvil/src/{evm.rs => evm/mod.rs} (64%) create mode 100644 crates/anvil/src/evm/optimism.rs create mode 100644 crates/cast/src/cmd/tempo.rs create mode 100644 crates/cast/src/cmd/vaddr/create.rs create mode 100644 crates/cast/src/cmd/vaddr/mod.rs create mode 100644 crates/cast/src/cmd/vaddr/resolve.rs create mode 100644 crates/cast/src/cmd/vaddr/watch.rs create mode 100644 crates/cast/tests/cli/keychain.rs create mode 100644 crates/common/src/tempo/auth.rs create mode 100644 crates/doc/src/parser/source.rs delete mode 100644 crates/doc/src/solang_ext/ast_eq.rs delete mode 100644 crates/doc/src/solang_ext/loc.rs delete mode 100644 crates/doc/src/solang_ext/mod.rs delete mode 100644 crates/doc/src/solang_ext/safe_unwrap.rs delete mode 100644 crates/doc/src/solang_ext/visit.rs create mode 100644 crates/evm/networks/src/optimism.rs create mode 100644 crates/lint/docs/README.md create mode 100644 crates/lint/docs/_template.md create mode 100644 crates/lint/docs/asm-keccak256.md create mode 100644 crates/lint/docs/block-timestamp.md create mode 100644 crates/lint/docs/boolean-cst.md create mode 100644 crates/lint/docs/boolean-equal.md create mode 100644 crates/lint/docs/could-be-immutable.md create mode 100644 crates/lint/docs/custom-errors.md create mode 100644 crates/lint/docs/divide-before-multiply.md create mode 100644 crates/lint/docs/erc20-unchecked-transfer.md create mode 100644 crates/lint/docs/incorrect-erc20-interface.md create mode 100644 crates/lint/docs/incorrect-erc721-interface.md create mode 100644 crates/lint/docs/incorrect-shift.md create mode 100644 crates/lint/docs/inline-assembly.md create mode 100644 crates/lint/docs/interface-file-naming.md create mode 100644 crates/lint/docs/interface-naming.md create mode 100644 crates/lint/docs/missing-zero-check.md create mode 100644 crates/lint/docs/mixed-case-function.md create mode 100644 crates/lint/docs/mixed-case-variable.md create mode 100644 crates/lint/docs/multi-contract-file.md create mode 100644 crates/lint/docs/named-struct-fields.md create mode 100644 crates/lint/docs/pascal-case-struct.md create mode 100644 crates/lint/docs/pragma-inconsistent.md create mode 100644 crates/lint/docs/rtlo.md create mode 100644 crates/lint/docs/screaming-snake-case-const.md create mode 100644 crates/lint/docs/screaming-snake-case-immutable.md create mode 100644 crates/lint/docs/too-many-digits.md create mode 100644 crates/lint/docs/tx-origin.md create mode 100644 crates/lint/docs/unaliased-plain-import.md create mode 100644 crates/lint/docs/unchecked-call.md create mode 100644 crates/lint/docs/unsafe-cheatcode.md create mode 100644 crates/lint/docs/unsafe-typecast.md create mode 100644 crates/lint/docs/unused-import.md create mode 100644 crates/lint/docs/unused-state-variables.md create mode 100644 crates/lint/docs/unwrapped-modifier-logic.md create mode 100644 crates/lint/src/linter/project.rs create mode 100644 crates/lint/src/sol/gas/immutable.rs create mode 100644 crates/lint/src/sol/gas/unused_state_variables.rs create mode 100644 crates/lint/src/sol/high/rtlo.rs create mode 100644 crates/lint/src/sol/info/boolean_cst.rs create mode 100644 crates/lint/src/sol/info/boolean_equal.rs create mode 100644 crates/lint/src/sol/info/inline_assembly.rs create mode 100644 crates/lint/src/sol/info/pragma_directive.rs create mode 100644 crates/lint/src/sol/info/too_many_digits.rs create mode 100644 crates/lint/src/sol/med/tx_origin.rs create mode 100644 crates/lint/testdata/BooleanCst.sol create mode 100644 crates/lint/testdata/BooleanCst.stderr create mode 100644 crates/lint/testdata/BooleanEqual.sol create mode 100644 crates/lint/testdata/BooleanEqual.stderr create mode 100644 crates/lint/testdata/CouldBeImmutable.sol create mode 100644 crates/lint/testdata/CouldBeImmutable.stderr create mode 100644 crates/lint/testdata/InlineAssembly.sol create mode 100644 crates/lint/testdata/InlineAssembly.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol create mode 100644 crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentOrVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.sol create mode 100644 crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.sol create mode 100644 crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr create mode 100644 crates/lint/testdata/Rtlo.sol create mode 100644 crates/lint/testdata/Rtlo.stderr create mode 100644 crates/lint/testdata/RtloCommentsOnly.sol create mode 100644 crates/lint/testdata/RtloCommentsOnly.stderr create mode 100644 crates/lint/testdata/TooManyDigits.sol create mode 100644 crates/lint/testdata/TooManyDigits.stderr create mode 100644 crates/lint/testdata/TxOrigin.sol create mode 100644 crates/lint/testdata/TxOrigin.stderr create mode 100644 crates/lint/testdata/UnusedStateVariables.sol create mode 100644 crates/lint/testdata/UnusedStateVariables.stderr create mode 100644 crates/primitives/src/network/optimism.rs create mode 100644 crates/primitives/src/transaction/optimism.rs create mode 100644 crates/wallets/src/tempo.rs create mode 100644 sleep.json diff --git a/.circleci/cargo.yml b/.circleci/cargo.yml new file mode 100644 index 0000000000000..32b65e6a23cc5 --- /dev/null +++ b/.circleci/cargo.yml @@ -0,0 +1,32 @@ +version: 2.1 +# +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test diff --git a/.circleci/ci-web3-gamefi.yml b/.circleci/ci-web3-gamefi.yml new file mode 100644 index 0000000000000..ad53a8e498202 --- /dev/null +++ b/.circleci/ci-web3-gamefi.yml @@ -0,0 +1,26 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable + auth: + # ensure you have first added these secrets + # visit app.circleci.com/settings/project/github/Dargon789/foundry/environment-variables + username: $DOCKER_HUB_USER + password: $DOCKER_HUB_PASSWORD +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- diff --git a/.circleci/ci.yml b/.circleci/ci.yml new file mode 100644 index 0000000000000..1b5df6d6e668e --- /dev/null +++ b/.circleci/ci.yml @@ -0,0 +1,31 @@ +version: 2.1 +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test diff --git a/.circleci/ci_cargo.yml b/.circleci/ci_cargo.yml new file mode 100644 index 0000000000000..46a18d45a5fca --- /dev/null +++ b/.circleci/ci_cargo.yml @@ -0,0 +1,37 @@ +version: 2.1 + +jobs: + build-and-test: + docker: + - image: cimg/rust:1.88.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + +workflows: + ci: + jobs: + - build-and-test diff --git a/.circleci/ci_v1.yml b/.circleci/ci_v1.yml new file mode 100644 index 0000000000000..82c6de5b42b73 --- /dev/null +++ b/.circleci/ci_v1.yml @@ -0,0 +1,31 @@ +version: 2.1 + +jobs: + build-and-test: + docker: + - image: cimg/rust:1.89.0 + steps: + - checkout + - restore_cache: + keys: + - v1-cargo-{{ checksum "Cargo.lock" }} + - v1-cargo- + - run: + name: "Check formatting" + command: cargo fmt -- --check + - run: + name: "Run tests" + command: cargo test + - save_cache: + key: v1-cargo-{{ checksum "Cargo.lock" }} + paths: + - "~/.cargo/bin" + - "~/.cargo/registry/index" + - "~/.cargo/registry/cache" + - "~/.cargo/git/db" + - "target" + +workflows: + ci: + jobs: + - build-and-test diff --git a/.circleci/dev_stage.yml b/.circleci/dev_stage.yml new file mode 100644 index 0000000000000..5ba351727d22b --- /dev/null +++ b/.circleci/dev_stage.yml @@ -0,0 +1,70 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- + + jobs: + my-job: + steps: + - run: echo "Hello, world!" + - run: + command: echo "This step will automatically rerun up to 3 times if it fails with a 10 second delay between attempts" + max_auto_reruns: 3 + auto_rerun_delay: 10s + + workflows: + dev_stage_pre-prod: + jobs: + - test_dev: + filters: # using regex filters requires the entire branch to match + branches: + only: # only branches matching the below regex filters will run + - dev + - /user-.*/ + - test_stage: + filters: + branches: + only: stage + - test_pre-prod: + filters: + branches: + only: /pre-prod(?:-.+)?$/ + + + build-test-deploy: + jobs: + - build: + filters: # required since `test` has tag filters AND requires `build` + tags: + only: /^config-test.*/ + - test: + requires: + - build + filters: # required since `deploy` has tag filters AND requires `test` + tags: + only: /^config-test.*/ + - deploy: + requires: + - test + filters: + tags: + only: /^config-test.*/ + branches: + ignore: /.*/ diff --git a/.circleci/web3_defi_gamefi.yml b/.circleci/web3_defi_gamefi.yml new file mode 100644 index 0000000000000..edb6605e3f101 --- /dev/null +++ b/.circleci/web3_defi_gamefi.yml @@ -0,0 +1,26 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference + +version: 2.1 +executors: + my-custom-executor: + docker: + - image: cimg/base:stable + auth: + # ensure you have first added these secrets + # visit app.circleci.com/settings/project/github/Dargon789/foundry/environment-variables + username: $DOCKER_HUB_USER + password: $DOCKER_HUB_PASSWORD +jobs: + web3-defi-game-project-: + + executor: my-custom-executor + steps: + - checkout + - run: | + # echo Hello, World! + +workflows: + my-custom-workflow: + jobs: + - web3-defi-game-project- diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 83ff83e43c870..a6726847fd70b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @danipopes @mattsse @grandizzy @zerosnacks @onbjerg @0xrusowsky @mablr @figtracer @stevencartavia +* @danipopes @mattsse @grandizzy @zerosnacks @0xrusowsky @mablr @figtracer @stevencartavia diff --git a/.github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md b/.github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md new file mode 100644 index 0000000000000..8f72899c63611 --- /dev/null +++ b/.github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md @@ -0,0 +1,10 @@ +--- +title: "bug: tempo nightly workflow failed" +labels: P-high, T-bug +--- + +The nightly Tempo workflow (mainnet and testnet checks) has failed. This indicates a regression in Foundry's Tempo support or an issue with the Tempo mainnet/testnet endpoints. + +Check the [tempo nightly workflow page]({{ env.WORKFLOW_URL }}) for details. + +This issue was raised by the workflow at `.github/workflows/ci-tempo.yml`. diff --git a/.github/scripts/commit-and-read-benchmarks.sh b/.github/scripts/commit-and-read-benchmarks.sh deleted file mode 100755 index 358b53a73155a..0000000000000 --- a/.github/scripts/commit-and-read-benchmarks.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Script to commit benchmark results and read them for GitHub Actions output -# Usage: ./commit-and-read-benchmarks.sh - -OUTPUT_DIR="${1:-benches}" -GITHUB_EVENT_NAME="${2:-pull_request}" -GITHUB_REPOSITORY="${3:-}" - -# Global variable for branch name -BRANCH_NAME="" - -# Function to commit benchmark results -commit_results() { - echo "Configuring git..." - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # For PR runs, fetch and checkout the PR branch to ensure we're up to date - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" - git fetch origin "$GITHUB_HEAD_REF" - git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" - fi - - echo "Adding benchmark file..." - git add "$OUTPUT_DIR/LATEST.md" - - if git diff --staged --quiet; then - echo "No changes to commit" - else - echo "Committing benchmark results..." - git commit -m "chore(\`benches\`): update benchmark results - -🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) - -Co-Authored-By: github-actions " - - echo "Pushing to repository..." - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - # For manual runs, we're on a new branch - git push origin "$BRANCH_NAME" - elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - # For PR runs, push to the PR branch - if [ -n "${GITHUB_HEAD_REF:-}" ]; then - echo "Pushing to PR branch: $GITHUB_HEAD_REF" - git push origin "$GITHUB_HEAD_REF" - else - echo "Error: GITHUB_HEAD_REF not set for pull_request event" - exit 1 - fi - else - # This workflow should only run on workflow_dispatch or pull_request - echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" - echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" - exit 1 - fi - echo "Successfully pushed benchmark results" - fi -} - -# Function to read benchmark results and output for GitHub Actions -read_results() { - if [ -f "$OUTPUT_DIR/LATEST.md" ]; then - echo "Reading benchmark results..." - - # Output full results - { - echo 'results<> "$GITHUB_OUTPUT" - - # Format results for PR comment - echo "Formatting results for PR comment..." - FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") - - { - echo 'pr_comment<> "$GITHUB_OUTPUT" - - echo "Successfully read and formatted benchmark results" - else - echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" - echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" - fi -} - -# Main execution -echo "Starting benchmark results processing..." - -# Create new branch for manual runs -if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - echo "Manual workflow run detected, creating new branch..." - BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$BRANCH_NAME" - echo "Created branch: $BRANCH_NAME" - - # Output branch name for later use - echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" -fi - -# Always commit benchmark results -echo "Committing benchmark results..." -commit_results - -# Always read results for output -read_results - -echo "Benchmark results processing complete" \ No newline at end of file diff --git a/.github/scripts/commit-benchmark-results.sh b/.github/scripts/commit-benchmark-results.sh new file mode 100644 index 0000000000000..f7dba8980fd64 --- /dev/null +++ b/.github/scripts/commit-benchmark-results.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -euo pipefail + +# Script to commit and push benchmark results. +# +# This script is intended to run from the lightweight `publish-results` job, +# which checks out the repo with credentials and only operates on the +# trusted artifact produced by the benchmark job. Keeping the write-scoped +# token away from the bench job (which runs untrusted third-party builds) +# limits the blast radius of a compromised dependency. +# +# Usage: ./commit-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" +GITHUB_EVENT_NAME="${2:-workflow_dispatch}" +GITHUB_REPOSITORY="${3:-}" + +if [ ! -f "$OUTPUT_DIR/LATEST.md" ]; then + echo "Error: $OUTPUT_DIR/LATEST.md not found, nothing to commit" + exit 1 +fi + +echo "Configuring git..." +git config --local user.email "action@github.com" +git config --local user.name "GitHub Action" + +# Decide which branch to commit to based on the event. +BRANCH_NAME="" +case "$GITHUB_EVENT_NAME" in + workflow_dispatch) + echo "Manual workflow run detected, creating new branch..." + BRANCH_NAME="benchmarks/results-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH_NAME" + echo "Created branch: $BRANCH_NAME" + ;; + pull_request) + if [ -z "${GITHUB_HEAD_REF:-}" ]; then + echo "Error: GITHUB_HEAD_REF not set for pull_request event" + exit 1 + fi + echo "Fetching latest changes for PR branch: $GITHUB_HEAD_REF" + git fetch origin "$GITHUB_HEAD_REF" + git checkout -B "$GITHUB_HEAD_REF" "origin/$GITHUB_HEAD_REF" + BRANCH_NAME="$GITHUB_HEAD_REF" + ;; + *) + echo "Error: Unexpected event type: $GITHUB_EVENT_NAME" + echo "This workflow only supports 'workflow_dispatch' and 'pull_request' events" + exit 1 + ;; +esac + +# Always emit the branch name so downstream steps (e.g. PR creation) can use it. +echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" + +echo "Adding benchmark file..." +git add "$OUTPUT_DIR/LATEST.md" + +if git diff --staged --quiet; then + echo "No changes to commit" + echo "committed=false" >> "$GITHUB_OUTPUT" + exit 0 +fi + +echo "Committing benchmark results..." +git commit -m "chore(\`benches\`): update benchmark results + +🤖 Generated with [Foundry Benchmarks](https://github.com/${GITHUB_REPOSITORY}/actions) + +Co-Authored-By: github-actions " + +echo "Pushing to repository..." +git push origin "$BRANCH_NAME" +echo "Successfully pushed benchmark results to $BRANCH_NAME" +echo "committed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/compare-nightly.sh b/.github/scripts/compare-nightly.sh new file mode 100644 index 0000000000000..674cc0fe01754 --- /dev/null +++ b/.github/scripts/compare-nightly.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Compare two nightly benchmark JSON summaries and report regressions. +# +# Usage: compare-nightly.sh [warn_pct] [fail_pct] +# Exits 0 if no regressions, 1 if any metric exceeds fail_pct. +# Exits 0 gracefully when prev.json is missing (first run / gap > 7 days). +set -euo pipefail + +PREV_JSON="${1:-}" +TODAY_JSON="${2:-}" +WARN="${3:-1}" +FAIL="${4:-3}" + +PREV_JSON="$PREV_JSON" TODAY_JSON="$TODAY_JSON" WARN="$WARN" FAIL="$FAIL" \ +python3 - <<'EOF' +import json, os, sys + +warn = float(os.environ["WARN"]) +fail = float(os.environ["FAIL"]) + +prev_path = os.environ.get("PREV_JSON", "") +prev = json.load(open(prev_path)) if prev_path and os.path.isfile(prev_path) else {} +with open(os.environ["TODAY_JSON"]) as f: + today = json.load(f) + +print("## Nightly Benchmark Regression Report\n") +print("| Benchmark | Previous | Today | Δ | Status |") +print("|-----------|----------|-------|---|--------|") + +has_regression = False +all_keys = sorted(prev.keys() | today.keys()) +for key in all_keys: + t = today.get(key) + p = prev.get(key) + if t is None: + print(f"| `{key}` | {p:.5f}s | N/A | — | ⚠️ Missing |") + has_regression = True + continue + if p is None: + print(f"| `{key}` | N/A | {t:.5f}s | — | 🆕 New |") + continue + delta = (t - p) / p * 100 + if delta >= fail: + status = "🔴 Regression" + has_regression = True + elif delta >= warn: + status = "🟡 Warning" + elif delta <= -warn: + status = "🟢 Improvement" + else: + status = "➡️ OK" + sign = "+" if delta > 0 else "" + print(f"| `{key}` | {p}s | {t}s | {sign}{delta:.1f}% | {status} |") + +sys.exit(1 if has_regression else 0) +EOF diff --git a/.github/scripts/read-benchmark-results.sh b/.github/scripts/read-benchmark-results.sh new file mode 100644 index 0000000000000..548611a7d204a --- /dev/null +++ b/.github/scripts/read-benchmark-results.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# Script to read benchmark results and emit them as GitHub Actions outputs. +# This script performs no git operations — it only reads the combined +# benchmark file and writes outputs for the workflow to consume. +# +# Usage: ./read-benchmark-results.sh + +OUTPUT_DIR="${1:-benches}" + +echo "Reading benchmark results from $OUTPUT_DIR..." + +if [ -f "$OUTPUT_DIR/LATEST.md" ]; then + # Output full results + { + echo 'results<> "$GITHUB_OUTPUT" + + # Format results for PR comment + echo "Formatting results for PR comment..." + FORMATTED_COMMENT=$("$(dirname "$0")/format-pr-comment.sh" "$OUTPUT_DIR/LATEST.md") + + { + echo 'pr_comment<> "$GITHUB_OUTPUT" + + echo "Successfully read and formatted benchmark results" +else + echo 'results=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo 'pr_comment=No benchmark results found.' >> "$GITHUB_OUTPUT" + echo "Warning: No benchmark results found at $OUTPUT_DIR/LATEST.md" +fi diff --git a/.github/scripts/tempo-check.sh b/.github/scripts/tempo-check.sh index b730c466bde55..3caea992cfe7e 100644 --- a/.github/scripts/tempo-check.sh +++ b/.github/scripts/tempo-check.sh @@ -445,7 +445,7 @@ echo -e "\n=== CAST SEND WITH SPONSOR (--tempo.sponsor-signature) ===" # Test sponsored transactions using pre-signed signature. # Step 1: Get the fee_payer_signature_hash using --tempo.print-sponsor-hash # Step 2: Sign it with the sponsor's private key -# Step 3: Send with --tempo.sponsor-signature +# Step 3: Send with --tempo.sponsor and --tempo.sponsor-signature # Step 1: Get the hash that the sponsor needs to sign FEE_PAYER_HASH=$(cast mktx ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ @@ -460,7 +460,7 @@ printf "Sponsor signature: %s\n" "$SPONSOR_SIG" # Step 3: Send the sponsored transaction with the signature RECEIPT=$(cast send ${FEE_TOKEN_ARG[@]+"${FEE_TOKEN_ARG[@]}"} --rpc-url "$ETH_RPC_URL" \ 0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D 'increment()' --private-key "$PK" \ - --tempo.sponsor-signature "$SPONSOR_SIG" --json) + --tempo.sponsor "$SPONSOR_ADDR" --tempo.sponsor-signature "$SPONSOR_SIG" --json) # Verify the fee_payer in the receipt matches the sponsor address RECEIPT_FEE_PAYER=$(echo "$RECEIPT" | jq -r '.feePayer // .fee_payer // empty') @@ -897,3 +897,85 @@ check_has_code "Nonce" "0x4e4F4E4345000000000000000000000000000000" check_has_code "AccountKeychain" "0xaAAAaaAA00000000000000000000000000000000" echo -e "\n=== CHISEL FORK TESTS COMPLETE ===" + +# --- cast virtual-address (TIP-1022) tests --- + +echo -e "\n=== CAST VIRTUAL-ADDRESS: SETUP MASTER WALLET ===" +vaddr_master_json="$(cast wallet new --json)" +VADDR_MASTER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_master_json")" +VADDR_MASTER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_master_json")" +printf "Master address: %s\n" "$VADDR_MASTER_ADDR" +fund_and_wait "$VADDR_MASTER_ADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: CREATE (mine + register) ===" +# Use the `vaddr` alias to also exercise it. +VADDR_CREATE_OUT=$(cast vaddr create \ +--owner "$VADDR_MASTER_ADDR" \ +--private-key "$VADDR_MASTER_PK" \ +--rpc-url "$ETH_RPC_URL") +echo "$VADDR_CREATE_OUT" +VADDR=$(echo "$VADDR_CREATE_OUT" | sed -n 's/^ tag=0x000000000000 \(0x[a-fA-F0-9]\{40\}\).*/\1/p' | head -1) +if [[ -z "$VADDR" ]]; then +echo "ERROR: failed to parse virtual address from create output" +exit 1 +fi +echo "Virtual address: $VADDR" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: RESOLVE ===" +VADDR_RESOLVE_OUT=$(cast virtual-address resolve "$VADDR" --rpc-url "$ETH_RPC_URL") +echo "$VADDR_RESOLVE_OUT" +RESOLVED_MASTER=$(echo "$VADDR_RESOLVE_OUT" | sed -n 's/^Master address: \(0x[a-fA-F0-9]\{40\}\).*/\1/p') +RESOLVED_LOWER=$(echo "$RESOLVED_MASTER" | tr '[:upper:]' '[:lower:]') +EXPECTED_LOWER=$(echo "$VADDR_MASTER_ADDR" | tr '[:upper:]' '[:lower:]') +if [[ "$RESOLVED_LOWER" != "$EXPECTED_LOWER" ]]; then +echo "ERROR: resolve returned master $RESOLVED_MASTER, expected $VADDR_MASTER_ADDR" +exit 1 +fi +echo "OK: resolve returned the registered master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: AUTO-FORWARD TO MASTER ===" +# Create a separate sender, fund it, and transfer the fee token to the +# virtual address. The protocol must auto-forward to the master wallet. +vaddr_sender_json="$(cast wallet new --json)" +VADDR_SENDER_ADDR="$(jq -r '.[0].address' <<<"$vaddr_sender_json")" +VADDR_SENDER_PK="$(jq -r '.[0].private_key' <<<"$vaddr_sender_json")" +fund_and_wait "$VADDR_SENDER_ADDR" + +BAL_BEFORE=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance before: $BAL_BEFORE" + +# Capture the current block before the transfer so `cast vaddr watch` can +# replay the Transfer log via --from-block. +BLOCK_BEFORE_TRANSFER=$(cast block-number --rpc-url "$ETH_RPC_URL") + +AMOUNT=1000000 +cast send "$FEE_TOKEN" 'transfer(address,uint256)' "$VADDR" "$AMOUNT" \ +--rpc-url "$ETH_RPC_URL" --private-key "$VADDR_SENDER_PK" + +BAL_AFTER=$(cast call --rpc-url "$ETH_RPC_URL" "$FEE_TOKEN" 'balanceOf(address)(uint256)' "$VADDR_MASTER_ADDR" | awk '{print $1}') +echo "Master balance after: $BAL_AFTER" + +EXPECTED=$((BAL_BEFORE + AMOUNT)) +if [[ "$BAL_AFTER" != "$EXPECTED" ]]; then +echo "ERROR: master balance grew by $((BAL_AFTER - BAL_BEFORE)), expected $AMOUNT" +exit 1 +fi +echo "OK: transfer to virtual address auto-forwarded to master" + +echo -e "\n=== CAST VIRTUAL-ADDRESS: WATCH ===" +# Tail incoming TIP-20 transfers to the virtual address. `cast vaddr watch` +# polls indefinitely, so we cap it with `timeout`; the historical fetch via +# --from-block emits the prior Transfer log immediately at startup. +WATCH_OUT=$(timeout 5 cast vaddr watch "$VADDR" \ + --token "$FEE_TOKEN" \ + --from-block "$BLOCK_BEFORE_TRANSFER" \ + --rpc-url "$ETH_RPC_URL" 2>&1 || true) +echo "$WATCH_OUT" + +EXPECTED_PATTERN="token=$FEE_TOKEN from=$VADDR_SENDER_ADDR amount=$AMOUNT" +echo "Expected pattern: $EXPECTED_PATTERN" +if ! echo "$WATCH_OUT" | grep -iqF "$EXPECTED_PATTERN"; then + echo "ERROR: cast vaddr watch output did not contain expected '$EXPECTED_PATTERN'" + exit 1 +fi +echo "OK: cast vaddr watch reported correct token/from/amount" diff --git a/.github/workflows/benchmarks-nightly.yml b/.github/workflows/benchmarks-nightly.yml new file mode 100644 index 0000000000000..8569f52ce3b93 --- /dev/null +++ b/.github/workflows/benchmarks-nightly.yml @@ -0,0 +1,217 @@ +name: Nightly Benchmarks (AAVE v4) + +permissions: {} + +on: + schedule: + - cron: "0 2 * * *" # 2am UTC nightly + workflow_dispatch: # allow manual triggering for testing + +env: + AAVE_V4_REPO: "aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" + RUSTC_WRAPPER: "sccache" + +jobs: + run-benchmarks: + name: Run Nightly Benchmarks + runs-on: depot-ubuntu-24.04-32 + permissions: + contents: read + actions: read # needed to download artifacts from previous runs + outputs: + has_regression: ${{ steps.compare.outputs.has_regression }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + + - name: Setup Foundry + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + ./.github/scripts/setup-foundryup.sh + printf '%s\n' "$GITHUB_WORKSPACE/.foundry/bin" >> "$GITHUB_PATH" + + - name: Build benchmark binary + run: cargo build --locked --release --bin foundry-bench + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24" + + - name: Install hyperfine + run: | + curl -L https://github.com/sharkdp/hyperfine/releases/download/v1.19.0/hyperfine-v1.19.0-x86_64-unknown-linux-gnu.tar.gz | tar xz + sudo mv hyperfine-v1.19.0-x86_64-unknown-linux-gnu/hyperfine /usr/local/bin/ + rm -rf hyperfine-v1.19.0-x86_64-unknown-linux-gnu + + - name: Download previous benchmark results + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p prev-results + PREV_RUN_ID=$(gh run list \ + --workflow=benchmarks-nightly.yml \ + --status=success \ + --limit=1 \ + --json databaseId \ + -q '.[0].databaseId // empty' 2>/dev/null || true) + if [[ -n "$PREV_RUN_ID" ]]; then + echo "Downloading results from previous run $PREV_RUN_ID" + gh run download "$PREV_RUN_ID" \ + --name nightly-bench-results \ + --dir prev-results/ || true + else + echo "No previous successful run found, skipping download." + fi + + - name: Run forge test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches --force-install \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_test \ + --json-output "nightly-${DATE}-forge_test.json" \ + --verbose + + - name: Run forge fuzz test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_fuzz_test \ + --json-output "nightly-${DATE}-forge_fuzz_test.json" \ + --verbose + + - name: Run forge isolate test benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_isolate_test \ + --json-output "nightly-${DATE}-forge_isolate_test.json" \ + --verbose + + - name: Run forge build benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --json-output "nightly-${DATE}-forge_build.json" \ + --verbose + + - name: Run forge coverage benchmarks + continue-on-error: true + env: + FOUNDRY_DIR: ${{ github.workspace }}/.foundry + run: | + DATE=$(date -u +%Y-%m-%d) + ./target/release/foundry-bench --output-dir ./benches \ + --versions nightly \ + --repos "$AAVE_V4_REPO" \ + --benchmarks forge_coverage \ + --json-output "nightly-${DATE}-forge_coverage.json" \ + --verbose + + - name: Merge benchmark JSON results + run: | + DATE=$(date -u +%Y-%m-%d) + shopt -s nullglob + parts=( benches/nightly-${DATE}-*.json ) + if [[ ${#parts[@]} -eq 0 ]]; then + echo "No benchmark results produced — all steps failed." + exit 1 + fi + jq -s 'add' "${parts[@]}" > "benches/nightly-${DATE}.json" + echo "Merged ${#parts[@]} result file(s) into nightly-${DATE}.json" + + - name: Compare with previous results + id: compare + run: | + DATE=$(date -u +%Y-%m-%d) + PREV_JSON=$(ls prev-results/nightly-*.json 2>/dev/null | head -1 || true) + TODAY_JSON="benches/nightly-${DATE}.json" + if ./.github/scripts/compare-nightly.sh "$PREV_JSON" "$TODAY_JSON" > regression.md 2>&1; then + echo "has_regression=false" >> "$GITHUB_OUTPUT" + else + echo "has_regression=true" >> "$GITHUB_OUTPUT" + fi + cat regression.md + + - name: Upload benchmark results + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nightly-bench-results + retention-days: 7 + path: | + benches/nightly-*.json + regression.md + + report-regression: + name: Report Regression + needs: run-benchmarks + if: needs.run-benchmarks.outputs.has_regression == 'true' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Download benchmark results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nightly-bench-results + path: results/ + + - name: Open regression issue + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + DATE=$(date -u +%Y-%m-%d) + BODY="$(cat results/regression.md) + + --- + + **Run**: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + **Date**: ${DATE} + **Repo benchmarked**: \`aave/aave-v4\` (pinned commit) + **Threshold**: 🔴 >=3% regression, 🟡 >=1% warning" + + gh issue create \ + --title "[Nightly Regression] ${DATE}" \ + --body "$BODY" \ + --label "regression" \ + --repo "$GH_REPO" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5d4767ad3b554..a136703abc294 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -10,17 +10,17 @@ on: required: false type: string versions: - description: "Comma-separated list of Foundry versions to benchmark (e.g., stable,nightly,v1.0.0)" + description: "Comma-separated list of Foundry versions to benchmark (optional, defaults to 'v1.5.1,v1.7.0')" required: false type: string - default: "stable,nightly" repos: - description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev][ ] (e.g. vectorized/solady:v0.1.26 --nmc BrokenTest). Leave empty to use the per-benchmark default repo lists." + description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev] (e.g. aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35). Leave empty to use the per-benchmark default repo lists." required: false type: string - default: "" env: + DEFAULT_VERSIONS: "v1.5.1,v1.7.0" + # Repos to benchmark per step. Each comma-separated entry has the form # org/repo[:rev][ ] # where anything after the first whitespace is appended to every benchmark @@ -29,27 +29,23 @@ env: TEST_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting ISOLATE_TEST_REPOS: >- ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest, - vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test', - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, + vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest', uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting BUILD_REPOS: >- ithacaxyz/account:v0.5.7, vectorized/solady:v0.1.26, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 COVERAGE_REPOS: >- ithacaxyz/account:v0.5.7, - aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35, uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, sparkdotfi/spark-psm:v1.0.0 @@ -60,7 +56,7 @@ jobs: name: Run All Benchmarks runs-on: depot-ubuntu-24.04-32 permissions: - contents: write + contents: read steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -93,7 +89,7 @@ jobs: run: cargo build --locked --release --bin foundry-bench - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" @@ -106,59 +102,61 @@ jobs: - name: Run forge test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_test,forge_fuzz_test \ - --output-file forge_test_bench.md + --output-file forge_test_bench.md \ + --verbose - name: Run forge isolate test benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.ISOLATE_TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_isolate_test \ - --output-file forge_isolate_test_bench.md + --output-file forge_isolate_test_bench.md \ + --verbose - name: Run forge build benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.BUILD_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_build_no_cache,forge_build_with_cache \ - --output-file forge_build_bench.md + --output-file forge_build_bench.md \ + --verbose - name: Run forge coverage benchmarks env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry - VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + VERSIONS: ${{ github.event.inputs.versions || env.DEFAULT_VERSIONS }} REPOS: ${{ github.event.inputs.repos || env.COVERAGE_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ --repos "$REPOS" \ --benchmarks forge_coverage \ - --output-file forge_coverage_bench.md + --output-file forge_coverage_bench.md \ + --verbose - name: Combine benchmark results run: ./.github/scripts/combine-benchmarks.sh benches - - name: Commit and read benchmark results + - name: Read benchmark results id: benchmark_results - env: - GITHUB_HEAD_REF: ${{ github.head_ref }} - run: ./.github/scripts/commit-and-read-benchmarks.sh benches "${{ github.event_name }}" "${{ github.repository }}" + run: ./.github/scripts/read-benchmark-results.sh benches - name: Upload benchmark results as artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -172,21 +170,21 @@ jobs: benches/LATEST.md outputs: - branch_name: ${{ steps.benchmark_results.outputs.branch_name }} pr_comment: ${{ steps.benchmark_results.outputs.pr_comment }} publish-results: name: Publish Results needs: run-benchmarks runs-on: ubuntu-latest + # All git writes happen here, on a clean ubuntu-latest runner that has + # never executed third-party benchmark code. permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + # persist-credentials defaults to true so we can push. - name: Download benchmark results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -194,19 +192,22 @@ jobs: name: benchmark-results path: benches/ - - name: Push branch for manual runs - if: github.event_name == 'workflow_dispatch' - run: | - git push origin "${{ needs.run-benchmarks.outputs.branch_name }}" - echo "Pushed branch: ${{ needs.run-benchmarks.outputs.branch_name }}" + - name: Commit benchmark results + id: commit_results + env: + GITHUB_HEAD_REF: ${{ github.head_ref }} + run: ./.github/scripts/commit-benchmark-results.sh benches "${{ github.event_name }}" "${{ github.repository }}" - name: Create PR for manual runs - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.commit_results.outputs.committed == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + BRANCH_NAME: ${{ steps.commit_results.outputs.branch_name }} + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | - const branchName = '${{ needs.run-benchmarks.outputs.branch_name }}'; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const branchName = process.env.BRANCH_NAME; + const prComment = process.env.PR_COMMENT; // Create the pull request const { data: pr } = await github.rest.pulls.create({ @@ -231,10 +232,12 @@ jobs: - name: Comment on PR if: github.event.inputs.pr_number != '' || github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_COMMENT: ${{ needs.run-benchmarks.outputs.pr_comment }} with: script: | const prNumber = ${{ github.event.inputs.pr_number || github.event.pull_request.number }}; - const prComment = `${{ needs.run-benchmarks.outputs.pr_comment }}`; + const prComment = process.env.PR_COMMENT; const comment = `${prComment} diff --git a/.github/workflows/ci-tempo.yml b/.github/workflows/ci-tempo.yml index b2be5a5312a10..1ef4c760f324e 100644 --- a/.github/workflows/ci-tempo.yml +++ b/.github/workflows/ci-tempo.yml @@ -6,6 +6,8 @@ on: push: branches: [master] pull_request: + schedule: + - cron: "0 2 * * *" # Run daily at 2 AM UTC (offset from other nightlies) workflow_dispatch: inputs: network: @@ -67,36 +69,28 @@ jobs: run: | cargo test --locked -p foundry-common --lib tempo::tests::test_fork_schedule_parses_configured_rpcs -- --exact --nocapture - # TODO: re-enable once devnet is up and stable - # - name: Run Tempo check on devnet - # if: | - # github.event_name == 'pull_request' || - # github.event.inputs.network == 'devnet' || - # github.event.inputs.network == 'all' - # env: - # ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} - # SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} - # run: | - # if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then - # ./.github/scripts/tempo-check.sh - # fi - # if [ "$SCRIPTS" = "deploy" ] || [ "$SCRIPTS" = "both" ]; then - # ./.github/scripts/tempo-deploy.sh - # fi - - # TODO: re-enable once devnet is up and stable - # - name: Run Tempo wallet tests on devnet - # if: | - # github.event_name == 'pull_request' || - # github.event.inputs.network == 'devnet' || - # github.event.inputs.network == 'all' - # env: - # ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} - # run: ./.github/scripts/tempo-wallet.sh + - name: Run Tempo check on mainnet + if: | + github.event_name == 'schedule' || + github.event.inputs.network == 'mainnet' || + github.event.inputs.network == 'all' + env: + ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} + TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" + VERIFIER_URL: ${{ secrets.VERIFIER_URL }} + PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} + SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} + run: | + if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then + ./.github/scripts/tempo-check.sh + fi + if [ "$SCRIPTS" = "deploy" ] || [ "$SCRIPTS" = "both" ]; then + ./.github/scripts/tempo-deploy.sh + fi - name: Run Tempo check on testnet if: | - github.event_name == 'pull_request' || + github.event_name == 'schedule' || github.event.inputs.network == 'testnet' || github.event.inputs.network == 'all' env: @@ -111,15 +105,14 @@ jobs: ./.github/scripts/tempo-deploy.sh fi - - name: Run Tempo check on mainnet + - name: Run Tempo check on devnet if: | - github.event.inputs.network == 'mainnet' || + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || github.event.inputs.network == 'all' env: - ETH_RPC_URL: ${{ secrets.TEMPO_MAINNET_RPC_URL }} - TEMPO_FEE_TOKEN: "0x20c0000000000000000000000000000000000000" - VERIFIER_URL: ${{ secrets.VERIFIER_URL }} - PRIVATE_KEY: ${{ secrets.THROW_AWAY_MAINNET_PKEY }} + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} SCRIPTS: ${{ github.event.inputs.scripts || 'both' }} run: | if [ "$SCRIPTS" = "check" ] || [ "$SCRIPTS" = "both" ]; then @@ -128,3 +121,35 @@ jobs: if [ "$SCRIPTS" = "deploy" ] || [ "$SCRIPTS" = "both" ]; then ./.github/scripts/tempo-deploy.sh fi + + - name: Run Tempo wallet tests on devnet + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' || + github.event.inputs.network == 'devnet' || + github.event.inputs.network == 'all' + env: + ETH_RPC_URL: ${{ secrets.TEMPO_DEVNET_RPC_URL }} + run: ./.github/scripts/tempo-wallet.sh + + # If the nightly run fails, this will create an issue to signal so. + issue: + name: Open an issue + runs-on: ubuntu-latest + needs: [tempo-check] + if: failure() && github.event_name == 'schedule' + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_URL: | + ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + update_existing: true + filename: .github/TEMPO_NIGHTLY_FAILURE_TEMPLATE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eb90a76cdbfd..9b63b3e01b4b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,23 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - run: cargo test --workspace --doc --locked + no-default-features: + runs-on: depot-ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - run: cargo build --workspace --no-default-features --locked + typos: runs-on: depot-ubuntu-latest timeout-minutes: 30 @@ -171,6 +188,7 @@ jobs: - test - docs - doctest + - no-default-features - typos - clippy - rustfmt diff --git a/.github/workflows/crate-checks.yml b/.github/workflows/crate-checks.yml index eb865bddc10e3..f0d460da6fbb1 100644 --- a/.github/workflows/crate-checks.yml +++ b/.github/workflows/crate-checks.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: cargo-hack - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b97a99d5310a4..6120735657ee6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -32,6 +32,8 @@ jobs: name: build and push runs-on: depot-ubuntu-latest permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -92,6 +94,7 @@ jobs: uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1 - name: Build and push Foundry image + id: build uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0 with: build-args: | @@ -106,3 +109,30 @@ jobs: platforms: linux/amd64,linux/arm64 push: true no-cache: true + sbom: true + provenance: mode=max + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign image with cosign (keyless) + env: + DOCKER_TAGS: ${{ steps.docker_tagging.outputs.docker_tags }} + DIGEST: ${{ steps.build.outputs.digest }} + shell: bash + run: | + set -euo pipefail + IFS=',' read -r -a tags <<< "$DOCKER_TAGS" + for tag in "${tags[@]}"; do + # Strip any tag suffix and pin to immutable digest, then sign. + ref="${tag%%:*}@${DIGEST}" + printf 'Signing %s\n' "$ref" + cosign sign --yes "$ref" + done + + - name: Image build provenance attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index f6e068eeec62a..258c86e9e72c2 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -19,7 +19,7 @@ jobs: contents: write pull-requests: write steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -38,7 +38,7 @@ jobs: permissions: contents: read steps: - - uses: DeterminateSystems/determinate-nix-action@32cb6a5ae30bb0dfc996fa7baf8bf1ed28442fa4 # v3.17.3 + - uses: DeterminateSystems/determinate-nix-action@2be1df9ed6cfd12d52bfbba7af79472420fa5299 # v3.18.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 2f990e2f9da92..7e849f31c34fa 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -153,13 +153,43 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id || inputs.run_id }} + - name: Validate Downloaded Artifacts + env: + ARTIFACT_DIR: ${{ steps.paths.outputs.artifact_dir }} + run: | + set -euo pipefail + + echo "Validating artifacts in: $ARTIFACT_DIR" + + if [[ ! -d "$ARTIFACT_DIR" ]]; then + echo "ERROR: Artifact directory does not exist: $ARTIFACT_DIR" >&2 + exit 1 + fi + + if ! find "$ARTIFACT_DIR" -mindepth 1 -print -quit | grep -q .; then + echo "ERROR: Artifact directory is empty: $ARTIFACT_DIR" >&2 + exit 1 + fi + + # Reject files with suspicious paths (absolute paths or parent directory traversals) + # Use null-delimited paths to safely handle filenames with newlines or whitespace + while IFS= read -r -d '' path; do + rel="${path#"$ARTIFACT_DIR"/}" + if [[ "$rel" == /* ]] || [[ "$rel" == *".."* ]]; then + echo "ERROR: Suspicious artifact path detected: $rel" >&2 + exit 1 + fi + done < <(find "$ARTIFACT_DIR" -type f -print0) + + echo "Artifact validation completed successfully." + - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -275,7 +305,7 @@ jobs: bun-version: latest - name: Setup Node (for npm publish auth) - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 682c9214284f6..38fa791fb655f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Build changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2 + uses: mikepenz/release-changelog-builder-action@bcae7115752d4ed746ff92feb666574428a79415 # v6.2.1 with: configuration: "./.github/changelog.json" fromTag: ${{ steps.release_info.outputs.from_tag || '' }} @@ -117,6 +117,8 @@ jobs: needs: prepare uses: ./.github/workflows/docker-publish.yml permissions: + attestations: write + artifact-metadata: write contents: read id-token: write packages: write @@ -129,9 +131,10 @@ jobs: # way, GitHub's immutable-releases setting seals the release at publish. release: permissions: - id-token: write - contents: write attestations: write + artifact-metadata: write + contents: write + id-token: write name: release ${{ matrix.target }} (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} timeout-minutes: 240 @@ -264,6 +267,38 @@ jobs: printf "file_name=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.zip" >> "$GITHUB_OUTPUT" fi printf "foundry_attestation=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.attestation.txt" >> "$GITHUB_OUTPUT" + printf "foundry_sbom=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.spdx.json" >> "$GITHUB_OUTPUT" + printf "foundry_checksum=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sha256" >> "$GITHUB_OUTPUT" + printf "foundry_signature=%s\n" "foundry_${VERSION_NAME}_${PLATFORM_NAME}_${ARCH}.sigstore.json" >> "$GITHUB_OUTPUT" + + - name: Generate archive checksum + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + shell: bash + run: | + set -euo pipefail + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + else + shasum -a 256 "$FILE_NAME" > "$FOUNDRY_CHECKSUM" + fi + cat "$FOUNDRY_CHECKSUM" + + - name: Install Syft + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + + - name: Generate SBOM (SPDX) + env: + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + VERSION_NAME: ${{ (env.IS_NIGHTLY == 'true' && 'nightly') || needs.prepare.outputs.tag_name }} + shell: bash + run: | + set -euo pipefail + syft scan dir:. \ + --source-name foundry \ + --source-version "$VERSION_NAME" \ + -o spdx-json="$FOUNDRY_SBOM" - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -292,15 +327,37 @@ jobs: tar -czvf "foundry_man_${VERSION_NAME}.tar.gz" forge.1.gz cast.1.gz anvil.1.gz chisel.1.gz printf 'foundry_man=%s\n' "foundry_man_${VERSION_NAME}.tar.gz" >> "$GITHUB_OUTPUT" - - name: Binaries attestation + - name: Binaries and archive provenance attestation id: attestation - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: | ${{ env.anvil_bin_path }} ${{ env.cast_bin_path }} ${{ env.chisel_bin_path }} ${{ env.forge_bin_path }} + ${{ steps.artifacts.outputs.file_name }} + + - name: Archive SBOM attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: ${{ steps.artifacts.outputs.file_name }} + sbom-path: ${{ steps.artifacts.outputs.foundry_sbom }} + + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Sign archive with cosign (keyless) + env: + FILE_NAME: ${{ steps.artifacts.outputs.file_name }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} + shell: bash + run: | + set -euo pipefail + cosign sign-blob \ + --yes \ + --bundle "$FOUNDRY_SIGNATURE" \ + "$FILE_NAME" - name: Record attestation URL env: @@ -321,11 +378,20 @@ jobs: TAG_NAME: ${{ needs.prepare.outputs.tag_name }} FILE_NAME: ${{ steps.artifacts.outputs.file_name }} FOUNDRY_ATTESTATION: ${{ steps.artifacts.outputs.foundry_attestation }} + FOUNDRY_SBOM: ${{ steps.artifacts.outputs.foundry_sbom }} + FOUNDRY_CHECKSUM: ${{ steps.artifacts.outputs.foundry_checksum }} + FOUNDRY_SIGNATURE: ${{ steps.artifacts.outputs.foundry_signature }} FOUNDRY_MAN: ${{ steps.man.outputs.foundry_man }} shell: bash run: | set -euo pipefail - files=("$FILE_NAME" "$FOUNDRY_ATTESTATION") + files=( + "$FILE_NAME" + "$FOUNDRY_ATTESTATION" + "$FOUNDRY_SBOM" + "$FOUNDRY_CHECKSUM" + "$FOUNDRY_SIGNATURE" + ) if [[ -n "${FOUNDRY_MAN:-}" ]]; then files+=("$FOUNDRY_MAN") fi diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index d6244f826887e..9caa254f05c10 100644 --- a/.github/workflows/test-flaky.yml +++ b/.github/workflows/test-flaky.yml @@ -33,7 +33,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index 141e3a049a73b..6763f1f80bde3 100644 --- a/.github/workflows/test-isolate.yml +++ b/.github/workflows/test-isolate.yml @@ -37,7 +37,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaa46c8e79f7c..daa4822b6e395 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,14 +73,14 @@ jobs: toolchain: stable target: ${{ matrix.target }} - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + - uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458 # v2.75.19 with: tool: nextest # External tests dependencies - name: Setup Node.js if: contains(matrix.name, 'external') - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 - name: Install Bun diff --git a/Cargo.lock b/Cargo.lock index 6996d21b881d9..9db38f1967f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,21 +77,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a547705d5c1b42575a0542bae2ba45bc62a6154be86611afaef1c0ab5c38598e" +checksum = "d8010fc7e9e8643ef4e758cdccf3eef26734594aedf88a9d5ed35e51837d42ef" dependencies = [ "alloy-consensus", "alloy-contract", "alloy-core", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-network", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -116,14 +116,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8c24c95e90c1608c2d91cff1b451d796474168d3310ccc8b7cd12502ca8169" +checksum = "e3d64da86c616b5092ea64eea648f311bbd58630a0b384c42d699175d6f9122b" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "alloy-tx-macros", "auto_impl", @@ -143,23 +143,23 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d211ad0ef468a70a7a829e49683ff59ad25f02b4ab3764344c4c2663329a52c" +checksum = "8fd98696ca3617d3a9ba1a6f2011880cbfd5618228dab6400c9f8bca457859a8" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-contract" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59d55233ac14aa7fa6bcdcad45ba305e90c556065e0947cd9f243c4469e7c2d" +checksum = "de3df0aadc569a8b277808a7d0ad0e421180654ea36a3c59e9ed2bb968c9a1cd" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -238,12 +238,12 @@ dependencies = [ [[package]] name = "alloy-eip5792" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250ba1168b8a049185a68c4dfa7f2a6a4046bd26fcc8c68632caeb216a5e12dc" +checksum = "1ceb16e7fe5a95825305f218ccd356665f848831f94ce2bbf55339bf5d21e88a" dependencies = [ "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] @@ -265,13 +265,14 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +checksum = "ec6ae911a2fc304a7cb80a79fb7bed6d1474aed4e7c203df1f8ff538f64fc78d" dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "once_cell", "serde", ] @@ -300,9 +301,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae69eaa5096b47ffe97e6a5d6bde7e7fa2dec106af22a9315621d11039c3de3c" +checksum = "64c0456f5f7a4497e9342d20f528e30f5288ddfa0d6a012bd5044afee46cd8a0" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -310,7 +311,7 @@ dependencies = [ "alloy-eip7928", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "auto_impl", "borsh", "c-kzg", @@ -325,9 +326,9 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a8c1330ad33c95b5958573bca9a1ad0b419a51d76bb4c521556fbba8539b8d" +checksum = "d5638cbbffb318d440fdb009de019090d8d117dae40de9d10cdb29891ea59eb9" dependencies = [ "alloy-contract", "alloy-primitives", @@ -339,12 +340,12 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.33.2" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc4b83cb672156663e6094d098beb509965b7fe684bb3d6e44bb9ca2e9ae714" +checksum = "c1ceeea6dcbbcd4e546b27700763a6f6c3b3fee30054209884f521078b6fda4f" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-hardforks", "alloy-primitives", "alloy-rpc-types-engine", @@ -359,13 +360,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39789db0b3f3bbef0e6549c87bc6842b73886ebabee1405b6941685b1cc34083" +checksum = "a71ff8b55d2b8aa05259f474cae7dea0e4991724dc18936b81cb23ec492a0c2a" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "borsh", "serde", @@ -400,9 +401,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662b525af73e86b2167dae923261c8edf440ba7e1426b30a8b993177bc214c02" +checksum = "19e352478b756bad5d7203148e4b461861282ea2ded3da406ba24868b52cd098" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -415,19 +416,19 @@ dependencies = [ [[package]] name = "alloy-network" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c657c2d9751d3c7d94990554b231e5372c3c2e4bad842806280b6151a0d6a05d" +checksum = "ed08ae169869e08370ed121612e0d3dadac33d1a256e9f2465926b23f0bd7d95" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-any", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-sol-types", "async-trait", @@ -441,24 +442,24 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e7c4bb0ebbd6d7406d2808968f43c0d5186c69c5e58cedcbee7380f4cd1fcf" +checksum = "02e6c7ad28afe348a9a9c5624b67ee5b3607b8de98d5816b3056ecdfa6fa2697" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-op-evm" version = "0.31.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -472,7 +473,7 @@ dependencies = [ [[package]] name = "alloy-op-hardforks" version = "0.4.7" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -513,13 +514,13 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fea0fc2628cdbc851aaa333124f9d8ab9f567ab8d4c20202819db13aa1a534" +checksum = "93a7c17472b55482d4734154c2f5ed13f72e03f6752cebb927f6a2d8b52e646c" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-rpc", "alloy-network", "alloy-network-primitives", @@ -547,7 +548,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "thiserror 2.0.18", @@ -559,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc7b42e514613c717887dc77bb58d35e845557ebd63a18c3f92a77094e4891f" +checksum = "a8d86958b02bca85103d64fa60d7b364a8b017c6e40f2b02c3f50ca22964a738" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -603,9 +604,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ee7b51752c68fb95f21705e402700750e692b1d21ccc294ac48fadc8655d53" +checksum = "5beb5c2fe6b960c8e8b038e69fd502a90a2e930afa4770efb748b163b0767729" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -616,7 +617,7 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "tokio", @@ -629,9 +630,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa76988f54105ad4398828e8aaf1a39b3f07f91fb79091529056689514ee8c2" +checksum = "4ee1257a278f6d293e05c5162c5940a1561b1aa85ded0028b464c81de37ebfa5" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -640,44 +641,44 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-rpc-types-txpool", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-anvil" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d276bea4e92e4991269d31b9abd3e722eed2565b82036478a4416adb8dd4992" +checksum = "df32156f085e74eac942b6103744be49b817c302341aaa8cb0c1c88dc29228d9" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] [[package]] name = "alloy-rpc-types-any" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1a9a3bda9be7f6515316eb792710532411878bbfc88934973f4b371376b00d" +checksum = "6a234bfbdf7a76c3d13808f729af5321852de3dedcaa6fc6d5f54787aaf54c6a" dependencies = [ "alloy-consensus-any", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", ] [[package]] name = "alloy-rpc-types-beacon" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5d68ddca890854fb78291cbde06115473ded00b2337d0f815e92c0c1f8003" +checksum = "296450f5e76bece0116c939b9437b0421a5da9c5d40031bf4cf9b38d3d94e475" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "derive_more", @@ -689,9 +690,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea21739e232c221779741eba7e7b9bc19ad8ff777b72736647ae519f5c9f6f33" +checksum = "0ab075ac1c25bcf697f133b7cd92e2fb26afe213e872ef79fdf77f0d7bcb3793" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -702,15 +703,15 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f05338cfb4ee5508ff76f01c88142cab8a4579db74b7d9432936c26e4f11374" +checksum = "73b12366c96f4013e1aeebc96c6b56e5f33f07853c42ea2f485045c0c157a4a1" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "ethereum_ssz", "ethereum_ssz_derive", @@ -722,17 +723,17 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda4ece0050154ab278241aeffade58916b04f38254832e8cb6e4671c6e72ed2" +checksum = "56a282daf869eeb7383d3d5c2deb35b0b3fb45ecb329513af4090fc61245ee18" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "itertools 0.14.0", "serde", @@ -743,13 +744,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5905ac3663b0859d67b82d912acce20887d20682a0cadde79c8a763b133a515" +checksum = "6184b5d14152b68b0bb8beb621339d94f0b761a37958bb365fbf7c00922125c2" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", "serde_json", "thiserror 2.0.18", @@ -757,13 +758,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fbf71892d4df9cae8d35dc96f15d522384bb93806205465e2c8c012b7f0a34" +checksum = "f00b631c361e7c7baaf4f1f5a9877730f3507fed2acb9d4b34841b8184b2ec28" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "serde", ] @@ -780,9 +781,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beaa5c581a67e2743d95b4849eb9cfeb90866429cdaa6d8f6b75eb988b2d0cd9" +checksum = "a0eada2558e921b39dfcead33c487364df9b31374f5733c1c9d2c891c4529933" dependencies = [ "alloy-primitives", "serde", @@ -791,9 +792,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5da9ae50f9b48d7b4e2e5cde87175257be7e5e56909a7794720597c1d9806f6" +checksum = "41eb29f7a8adcd8941fbb8e134022a133e6f8dfd345f2e3b7109599f8a7dca08" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -808,9 +809,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a57d1e72b1f9b11e5e71ebdab0569cb02277a462bbea6793fcaebfcd794ae9" +checksum = "1258987fbc82716b5153ec7bb95a8a295e7640871b8f03d8ec7c4000dc80c215" dependencies = [ "alloy-consensus", "alloy-network", @@ -827,9 +828,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b27f20b5298b76a5a3b7cdbe6bdb184ab1ebd6e120e00dad748867673f5c90" +checksum = "7ffc2a49bca5b73c6964711b57452f6c36a6bcb7f845ab7e9ad05b5a828d0161" dependencies = [ "alloy-consensus", "alloy-network", @@ -845,9 +846,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c7acc40ffbfd37d4113eb619863099f3235d78d044006a1eecb94d8b0b2f1a" +checksum = "94e11ddaddfb98c1ddce737dc440225565b0ae0987ac9ad5e59a85db5904878c" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -865,9 +866,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b794002d57fd2f71b4c87298a41ca24dfc0f2cf6630d95106a477e451747ba" +checksum = "bef839e7ce9b59aa60fa9a175e97986c6145c888d643b0f1fb0a3e7b8e56a2e2" dependencies = [ "alloy-consensus", "alloy-network", @@ -885,9 +886,9 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a09a865ae9e1f05478429ef0d935b16467f35c6e0b02cb10f23f66a3b33fc3" +checksum = "44eb341d0013784da6a39e5bbdc11b95d6744993b12a1c3fd55df795a850dd42" dependencies = [ "alloy-consensus", "alloy-network", @@ -902,9 +903,9 @@ dependencies = [ [[package]] name = "alloy-signer-turnkey" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bb8218544ab635281f1be180a1cfd9b5d549db686faa7e85b3b2c10969819e" +checksum = "82ff16b4166fb90bbe79bd1e49244824fb3cadc6b8cd11e9c8a002c1f8c07492" dependencies = [ "alloy-consensus", "alloy-network", @@ -991,9 +992,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19dec9bfb59647254afdecbb5ddcddd7ba02edcd48ffa40510bddfbed0be1634" +checksum = "3ac7a80c0bac3e44559d53d002e34c461dc2f23262b42cafec019bc70551abbe" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -1014,14 +1015,14 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2035f3c4d6bee20624da2dcf765d469b292398e48d766ffade61b0fcf8b4d45d" +checksum = "eed3ed3300a998f88639ed619fdbbd88bd82865e00c6a8ecb796c99eb12358f6" dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "tower", "tracing", @@ -1030,9 +1031,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfad7aa9206fcb831ae401b6a1c893a402b8eed74f9c8ffbb7a7323afb0d9a4c" +checksum = "1075d9d30fd4d71e50000fd4afb19ed2664ceab20c2a29f3889a6e988329e02d" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -1050,9 +1051,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aa8ff49386df3e008b73c7fb0a5479410e8493fdb86a8b916877a16e8aead9" +checksum = "0e3bff84b2b2a46eb34cc522dc3f889a2867c70be90a377421429b662b3ec4ce" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1085,9 +1086,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "2.0.1" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520337f3d3d063a7fe20f47aaa62d695e3dc0372b34f601560dee24e76988b9" +checksum = "99fce0350197dcd4ba4e9a7dd43915d908c0eb0e7352755791709a705e1c76b6" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1194,7 +1195,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1218,18 +1219,18 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anvil" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-network", @@ -1241,7 +1242,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-beacon", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -1276,7 +1277,7 @@ dependencies = [ "parking_lot", "rand 0.8.6", "rand 0.9.4", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -1297,17 +1298,17 @@ dependencies = [ [[package]] name = "anvil-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-eip5792", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "foundry-common", "foundry-evm", @@ -1321,7 +1322,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "serde", "serde_json", @@ -1329,7 +1330,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "1.6.0" +version = "1.7.1" dependencies = [ "anvil-rpc", "async-trait", @@ -1669,20 +1670,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ascii-canvas" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" -dependencies = [ - "term", -] - [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -1955,9 +1947,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.101.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -2172,9 +2164,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.14" +version = "1.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -2361,9 +2353,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -2579,7 +2571,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -2745,13 +2737,13 @@ dependencies = [ [[package]] name = "cast" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-evm", "alloy-hardforks", @@ -2763,7 +2755,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-beacon", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -2822,9 +2814,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -2832,12 +2824,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -2876,7 +2862,7 @@ dependencies = [ [[package]] name = "chisel" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -2884,17 +2870,15 @@ dependencies = [ "clap", "dirs", "eyre", - "forge-doc", "forge-fmt", "foundry-cli", "foundry-common", "foundry-compilers", "foundry-config", "foundry-evm", - "foundry-solang-parser", "foundry-test-utils", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "rexpect", "rustyline", "semver 1.0.28", @@ -2998,9 +2982,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" dependencies = [ "clap", ] @@ -3043,7 +3027,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3196,7 +3180,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3365,9 +3349,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -3376,9 +3360,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -3426,9 +3410,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -3539,9 +3523,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" @@ -3822,9 +3806,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -3974,9 +3958,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", @@ -4000,7 +3984,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4188,15 +4172,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -4305,7 +4280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4503,15 +4478,15 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment2" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4380ce44915a6227efbb61e3885bc1c8e99fb9820f5db612abfac2c5cfc46871" +checksum = "87d63dee16df12076c7770919713c0b92f4e1c85eac828dc2ade0b6c998f016b" dependencies = [ "atomic", "parking_lot", "serde", "tempfile", - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.11+spec-1.1.0", "uncased", "version_check", ] @@ -4608,7 +4583,7 @@ checksum = "932dcfbd51320af5f27f1ba02d2e567dec332cac7d2c221ba45d8e767264c4dc" [[package]] name = "forge" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4662,7 +4637,7 @@ dependencies = [ "rand 0.9.4", "rayon", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4690,16 +4665,14 @@ dependencies = [ [[package]] name = "forge-doc" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "derive_more", "eyre", - "forge-fmt", "foundry-common", "foundry-compilers", "foundry-config", - "foundry-solang-parser", "itertools 0.14.0", "mdbook-driver", "rayon", @@ -4714,7 +4687,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "foundry-common", "foundry-config", @@ -4728,7 +4701,7 @@ dependencies = [ [[package]] name = "forge-lint" -version = "1.6.0" +version = "1.7.1" dependencies = [ "eyre", "foundry-common", @@ -4742,12 +4715,12 @@ dependencies = [ [[package]] name = "forge-script" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-json-abi", "alloy-network", @@ -4790,7 +4763,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-network", "alloy-primitives", @@ -4806,7 +4779,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -4821,7 +4794,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4844,7 +4817,7 @@ dependencies = [ "futures", "itertools 0.14.0", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "semver 1.0.28", "serde", @@ -4867,7 +4840,7 @@ dependencies = [ [[package]] name = "foundry-bench" -version = "1.6.0" +version = "1.7.1" dependencies = [ "chrono", "clap", @@ -4892,7 +4865,7 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "foundry-compilers", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -4902,7 +4875,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4955,7 +4928,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4966,11 +4939,11 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-ens", "alloy-json-abi", "alloy-network", @@ -5010,6 +4983,7 @@ dependencies = [ "tempo-primitives", "tikv-jemallocator", "tokio", + "toml", "tracing", "tracing-subscriber 0.3.23", "tracing-tracy", @@ -5018,7 +4992,7 @@ dependencies = [ [[package]] name = "foundry-cli-markdown" -version = "1.6.0" +version = "1.7.1" dependencies = [ "clap", "pretty_assertions", @@ -5026,12 +5000,12 @@ dependencies = [ [[package]] name = "foundry-common" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-json-abi", "alloy-json-rpc", "alloy-network", @@ -5043,6 +5017,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-engine", "alloy-signer", + "alloy-signer-local", "alloy-sol-types", "alloy-transport", "alloy-transport-ipc", @@ -5050,6 +5025,7 @@ dependencies = [ "anstream 0.6.21", "anstyle", "axum", + "base64 0.22.1", "chrono", "ciborium", "clap", @@ -5067,18 +5043,20 @@ dependencies = [ "futures", "itertools 0.14.0", "jiff", + "k256", "mpp", "num-format", "op-alloy-network", "op-alloy-rpc-types", "path-slash", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "rustls", "semver 1.0.28", "serde", "serde_json", + "sha2 0.10.9", "solar-compiler", "tempfile", "tempo-alloy", @@ -5097,14 +5075,14 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-network", "alloy-primitives", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "chrono", "comfy-table", "eyre", @@ -5220,7 +5198,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5260,7 +5238,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "crossterm", @@ -5278,7 +5256,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5315,7 +5293,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -5327,7 +5305,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -5342,7 +5320,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-sol-types", "anvil", "auto_impl", @@ -5379,7 +5357,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "eyre", @@ -5395,7 +5373,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5420,7 +5398,7 @@ dependencies = [ [[package]] name = "foundry-evm-hardforks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -5435,10 +5413,10 @@ dependencies = [ [[package]] name = "foundry-evm-networks" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -5451,11 +5429,11 @@ dependencies = [ [[package]] name = "foundry-evm-sancov" -version = "1.6.0" +version = "1.7.1" [[package]] name = "foundry-evm-traces" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5473,7 +5451,7 @@ dependencies = [ "itertools 0.14.0", "memchr", "rayon", - "reqwest 0.13.2", + "reqwest 0.13.3", "revm", "revm-inspectors", "serde", @@ -5512,7 +5490,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -5523,7 +5501,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "1.6.0" +version = "1.7.1" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -5533,7 +5511,7 @@ dependencies = [ [[package]] name = "foundry-primitives" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-consensus", "alloy-evm", @@ -5544,7 +5522,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer", "derive_more", "op-alloy-consensus", @@ -5558,23 +5536,9 @@ dependencies = [ "tempo-revm", ] -[[package]] -name = "foundry-solang-parser" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9645e75b89f977423690f3b4bfd8d84825e5fdabd7803cbce6d4a2c4d54972b4" -dependencies = [ - "itertools 0.14.0", - "lalrpop", - "lalrpop-util", - "phf 0.11.3", - "thiserror 2.0.18", - "unicode-xid", -] - [[package]] name = "foundry-test-utils" -version = "1.6.0" +version = "1.7.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5589,7 +5553,7 @@ dependencies = [ "parking_lot", "rand 0.9.4", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "snapbox", "svm-rs", @@ -5817,7 +5781,7 @@ dependencies = [ "once_cell", "prost 0.14.3", "prost-types 0.14.3", - "reqwest 0.13.2", + "reqwest 0.13.3", "secret-vault-value", "serde", "serde_json", @@ -6191,9 +6155,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -6585,9 +6549,9 @@ dependencies = [ [[package]] name = "interprocess" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be5e5c847dbdb44564bd85294740d031f4f8aeb3464e5375ef7141f7538db69" +checksum = "069323743400cb7ab06a8fe5c1ed911d36b6919ec531661d034c89083629595b" dependencies = [ "doctest-file", "futures-core", @@ -6595,7 +6559,7 @@ dependencies = [ "recvmsg", "tokio", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6644,7 +6608,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6697,9 +6661,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -6710,31 +6674,15 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -6744,7 +6692,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -6765,15 +6713,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -6805,9 +6744,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -6844,6 +6783,7 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", "getrandom 0.2.17", "js-sys", @@ -6926,45 +6866,14 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] -[[package]] -name = "lalrpop" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.14.0", - "lalrpop-util", - "petgraph", - "regex", - "regex-syntax", - "sha3", - "string_cache 0.8.9", - "term", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" -dependencies = [ - "regex-automata", - "rustversion", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -6985,9 +6894,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -6997,12 +6906,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" dependencies = [ "cc", - "libc", ] [[package]] @@ -7302,12 +7210,12 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] @@ -7334,9 +7242,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" dependencies = [ "libmimalloc-sys", ] @@ -7591,7 +7499,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7820,7 +7728,7 @@ dependencies = [ [[package]] name = "op-alloy" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "op-alloy-consensus", "op-alloy-network", @@ -7832,15 +7740,15 @@ dependencies = [ [[package]] name = "op-alloy-consensus" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "bytes", "derive_more", "reth-codecs", @@ -7859,7 +7767,7 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", "alloy-network", @@ -7872,7 +7780,7 @@ dependencies = [ [[package]] name = "op-alloy-provider" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-network", "alloy-primitives", @@ -7886,15 +7794,15 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "derive_more", "op-alloy-consensus", "reth-rpc-traits", @@ -7906,15 +7814,14 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" version = "0.24.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-engine", - "alloy-serde 2.0.1", - "derive_more", + "alloy-serde 2.0.4", "ethereum_ssz", "ethereum_ssz_derive", "op-alloy-consensus", @@ -7927,7 +7834,7 @@ dependencies = [ [[package]] name = "op-revm" version = "19.0.0" -source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" +source = "git+https://github.com/ethereum-optimism/optimism?rev=e3b59e76588f99db17205f5601e45a5b00f0bfbb#e3b59e76588f99db17205f5601e45a5b00f0bfbb" dependencies = [ "auto_impl", "revm", @@ -8145,16 +8052,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.14.0", -] - [[package]] name = "pharos" version = "0.5.3" @@ -8171,7 +8068,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -8181,7 +8077,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", + "phf_macros", "phf_shared 0.13.1", "serde", ] @@ -8226,19 +8122,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "phf_macros" version = "0.13.1" @@ -8574,7 +8457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -9094,9 +8977,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -9138,12 +9021,12 @@ dependencies = [ [[package]] name = "reth-chainspec" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-primitives", @@ -9158,11 +9041,12 @@ dependencies = [ [[package]] name = "reth-codecs" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce542a96bf888f31854803e80b3340bc233927743aa580838014e8a88fe0d66" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-trie", @@ -9176,8 +9060,9 @@ dependencies = [ [[package]] name = "reth-codecs-derive" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634c90f1cc0f9887680ca785b0b21aa961070b9465917bf65afaec56a6d005bb" dependencies = [ "proc-macro2", "quote", @@ -9186,8 +9071,8 @@ dependencies = [ [[package]] name = "reth-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9199,11 +9084,11 @@ dependencies = [ [[package]] name = "reth-consensus-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9212,8 +9097,8 @@ dependencies = [ [[package]] name = "reth-db-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9236,10 +9121,10 @@ dependencies = [ [[package]] name = "reth-db-models" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "bytes", "modular-bitfield", @@ -9250,11 +9135,11 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "reth-chainspec", "reth-consensus", @@ -9266,8 +9151,8 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -9279,11 +9164,11 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-eth", "reth-codecs", @@ -9293,11 +9178,11 @@ dependencies = [ [[package]] name = "reth-evm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "auto_impl", @@ -9315,11 +9200,11 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rpc-types-engine", @@ -9335,8 +9220,8 @@ dependencies = [ [[package]] name = "reth-execution-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-evm", "alloy-primitives", @@ -9348,11 +9233,11 @@ dependencies = [ [[package]] name = "reth-execution-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-primitives", "alloy-rlp", @@ -9367,8 +9252,8 @@ dependencies = [ [[package]] name = "reth-network-peers" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9380,11 +9265,12 @@ dependencies = [ [[package]] name = "reth-primitives-traits" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee12e304adbacbb32248c9806ebafbe1e2811fbfefe53c5e5b710a8438b7ec0" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-genesis", "alloy-primitives", "alloy-rlp", @@ -9408,8 +9294,8 @@ dependencies = [ [[package]] name = "reth-prune-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9423,8 +9309,8 @@ dependencies = [ [[package]] name = "reth-revm" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9436,8 +9322,8 @@ dependencies = [ [[package]] name = "reth-rpc-convert" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9456,9 +9342,9 @@ dependencies = [ [[package]] name = "reth-rpc-traits" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b766da61ec7c46596386b4bc88d9b57d1939d3da2bc9e927567a8a23650e5ce9" +checksum = "860fe223501a76ff14aa3bf164f739f31008c2a2905ac85708bfd88f042e6151" dependencies = [ "alloy-consensus", "alloy-network", @@ -9471,8 +9357,8 @@ dependencies = [ [[package]] name = "reth-stages-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "bytes", @@ -9484,8 +9370,8 @@ dependencies = [ [[package]] name = "reth-static-file-types" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-primitives", "derive_more", @@ -9498,11 +9384,11 @@ dependencies = [ [[package]] name = "reth-storage-api" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rpc-types-engine", "auto_impl", @@ -9521,10 +9407,10 @@ dependencies = [ [[package]] name = "reth-storage-errors" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-primitives", "alloy-rlp", "derive_more", @@ -9539,14 +9425,14 @@ dependencies = [ [[package]] name = "reth-trie-common" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth?rev=38c627c#38c627ce8f1a3bb82bed8a6beb3016f62c50016d" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-trie", "arrayvec", "bytes", @@ -9562,8 +9448,9 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" -version = "0.3.0" -source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12fafa33d2f420a9d39249a3e0357b1928d09429f30758b85280409092873b2" dependencies = [ "zstd", ] @@ -9846,9 +9733,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -9856,20 +9743,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.4.0" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rtoolbox" -version = "0.0.4" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327b72899159dfae8060c51a1f6aebe955245bcd9cc4997eed0f623caea022e4" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", "windows-sys 0.59.0", @@ -9877,9 +9764,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" dependencies = [ "alloy-rlp", "arbitrary", @@ -10004,14 +9891,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -10037,9 +9924,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -10047,13 +9934,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -10063,7 +9950,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10481,9 +10368,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" dependencies = [ "base64 0.22.1", "chrono", @@ -10500,9 +10387,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -10563,14 +10450,14 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -10704,9 +10591,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -10777,7 +10664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10845,7 +10732,7 @@ dependencies = [ "derive_more", "dunce", "inturn", - "itertools 0.12.1", + "itertools 0.14.0", "itoa", "normalize-path", "once_map", @@ -10880,7 +10767,7 @@ dependencies = [ "alloy-primitives", "bitflags 2.11.1", "bumpalo", - "itertools 0.12.1", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", @@ -11014,18 +10901,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -11178,13 +11053,13 @@ dependencies = [ [[package]] name = "svm-rs" -version = "0.5.24" +version = "0.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230df06b463c7251e4d1b39b1b3e6f25a9b3a42630179053a1e5f919e6e15534" +checksum = "4572dd9845e37ca0293acb5fe591a7f61b51f1b7b62d3dc6fb8e99e2664f3755" dependencies = [ "const-hex", "dirs", - "reqwest 0.13.2", + "reqwest 0.13.3", "semver 1.0.28", "serde", "serde_json", @@ -11197,9 +11072,9 @@ dependencies = [ [[package]] name = "svm-rs-builds" -version = "0.5.24" +version = "0.5.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b271921143e5b12947a526de464db02b00363919d582a7ea712374840f928328" +checksum = "74224f62f19c1309caa071de7c1c9c1ad1d7551d2f881af4046f3d71880c820a" dependencies = [ "const-hex", "semver 1.0.28", @@ -11304,22 +11179,22 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "tempo-alloy" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-contract", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "alloy-signer-local", "alloy-sol-types", "alloy-transport", @@ -11337,9 +11212,9 @@ dependencies = [ [[package]] name = "tempo-chainspec" version = "1.5.3" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-evm", "alloy-genesis", "alloy-hardforks", @@ -11356,7 +11231,7 @@ dependencies = [ [[package]] name = "tempo-consensus" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11374,7 +11249,7 @@ dependencies = [ [[package]] name = "tempo-contracts" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-contract", "alloy-primitives", @@ -11385,7 +11260,7 @@ dependencies = [ [[package]] name = "tempo-evm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11412,7 +11287,7 @@ dependencies = [ [[package]] name = "tempo-precompiles" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "alloy-evm", @@ -11432,7 +11307,7 @@ dependencies = [ [[package]] name = "tempo-precompiles-macros" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy", "proc-macro2", @@ -11443,15 +11318,15 @@ dependencies = [ [[package]] name = "tempo-primitives" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.1", + "alloy-eips 2.0.4", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.1", + "alloy-serde 2.0.4", "aws-lc-rs", "base64 0.22.1", "derive_more", @@ -11474,7 +11349,7 @@ dependencies = [ [[package]] name = "tempo-revm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=8bd4d01d37e3cc324030baacbce2da0862d7735a#8bd4d01d37e3cc324030baacbce2da0862d7735a" +source = "git+https://github.com/tempoxyz/tempo?rev=6bf9903d6a75cc264029dcf54183adea38d3cfaa#6bf9903d6a75cc264029dcf54183adea38d3cfaa" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11505,15 +11380,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "term" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "terminal_size" version = "0.4.4" @@ -11521,7 +11387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11555,9 +11421,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" [[package]] name = "thiserror" @@ -11698,9 +11564,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -11867,9 +11733,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap 2.14.0", + "serde_core", + "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -11878,7 +11746,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -12223,9 +12091,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -12509,9 +12377,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174a690eb3293a5666442b0738d080df9ea6b9e03782bbe78875c89ff914a77c" +checksum = "2d7cb4a83971db3f6ae36f0aa41eaf5985d2e2b469581fa755c132f9c2a1ec89" dependencies = [ "anyhow", "bon", @@ -12522,9 +12390,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f628f4acc90a5c1a8136495eaf5f9ef94e03c174d6fb2e6de691bc58fc721ee" +checksum = "bba14c9676943b2899cea2ed7ea194b89b3d13564a3c93a61882a978b123a41c" dependencies = [ "anyhow", "bon", @@ -12536,9 +12404,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "10.0.0-beta.6" +version = "10.0.0-beta.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390d0442b660baedd7a6f60d2af01bd8967e0d7fe69cd15e13c82c811d82b709" +checksum = "fb684e6d170ef15a9b3c20561779a50ba8c806f8acdaff47c0a2c5c4c6cadd43" dependencies = [ "anyhow", "bon", @@ -12603,11 +12471,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -12616,14 +12484,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -12634,9 +12502,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -12644,9 +12512,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12654,9 +12522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -12667,9 +12535,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -12767,7 +12635,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12807,9 +12675,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -12827,13 +12695,13 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", - "string_cache 0.9.0", + "string_cache", "string_cache_codegen", ] @@ -12844,7 +12712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni 0.22.4", + "jni", "log", "ndk-context", "objc2", @@ -12917,7 +12785,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -13038,15 +12906,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -13083,21 +12942,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -13140,12 +12984,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -13158,12 +12996,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -13176,12 +13008,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -13206,12 +13032,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -13224,12 +13044,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -13242,12 +13056,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -13260,12 +13068,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -13289,9 +13091,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -13305,6 +13107,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/README.md b/README.md index c9f0a45c57b0a..90cd1d8a7865e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ foundryup See the [installation guide](https://getfoundry.sh/getting-started/installation) for more details. +To verify a downloaded release archive or container image, see [Verifying Releases](./SECURITY.md#verifying-releases). + ## Getting Started Initialize a new project, build and test: diff --git a/SECURITY.md b/SECURITY.md index d84327cc18e91..6296066db5e73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,3 +3,112 @@ ## Reporting a Vulnerability Contact [security@tempo.xyz](mailto:security@tempo.xyz). + +## Verifying Releases + +Every official Foundry release ships with multiple, independent integrity +artifacts. All signing is keyless via [Sigstore](https://www.sigstore.dev/) — +no Foundry-managed key material is involved, and every signature is recorded +in the public [Rekor](https://docs.sigstore.dev/logging/overview/) transparency +log. The signing identity is the GitHub Actions OIDC token of this repository's +`release.yml` / `docker-publish.yml` workflows. + +### Per-release artifacts + +For each `foundry___.{tar.gz,zip}` archive on the +[releases page](https://github.com/foundry-rs/foundry/releases), the same +release also publishes: + +| Suffix | Purpose | +| --- | --- | +| `.sha256` | SHA-256 checksum of the archive (`sha256sum` format) | +| `.sigstore.json` | Cosign keyless signature bundle (cert + signature + Rekor proof) over the archive | +| `.spdx.json` | SPDX 2.3 SBOM of the source workspace used for the build | +| `.attestation.txt` | URL of the GitHub artifact-attestation summary | + +In addition, GitHub stores SLSA build-provenance and SBOM attestations against +the archive's digest; these are queryable via `gh attestation` without +downloading anything else. + +### Verifying an archive + +Pick whichever toolchain you have available — they verify the same signatures. + +#### Option 1: GitHub CLI (simplest) + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry +``` + +This computes the file's digest, fetches the matching attestation from GitHub, +and verifies the Sigstore signature plus the SLSA provenance predicate. Add +`--signer-workflow foundry-rs/foundry/.github/workflows/release.yml` to also +require the workflow identity. + +To verify the SBOM attestation specifically: + +```bash +gh attestation verify foundry_v1.4.0_linux_amd64.tar.gz \ + --repo foundry-rs/foundry \ + --predicate-type 'https://spdx.dev/Document/v2.3' +``` + +#### Option 2: Cosign (offline-friendly) + +Download the archive and its `.sigstore.json` bundle from the release page, +then: + +```bash +cosign verify-blob \ + --bundle foundry_v1.4.0_linux_amd64.sigstore.json \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/release\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + foundry_v1.4.0_linux_amd64.tar.gz +``` + +For nightly builds the certificate identity points at `refs/heads/master` +instead of a tag; the regex above matches both. + +#### Option 3: Plain checksum (integrity only) + +```bash +sha256sum -c foundry_v1.4.0_linux_amd64.sha256 # GNU coreutils +shasum -a 256 -c foundry_v1.4.0_linux_amd64.sha256 # macOS +``` + +This proves the bytes match what was uploaded, but says nothing about who +uploaded them. Combine with one of the verifications above for end-to-end +trust. + +### Verifying the Docker image + +Container signatures and attestations are pushed as OCI referrers to GHCR, so +no separate files need to be downloaded. + +```bash +# Cosign keyless signature on the image +cosign verify ghcr.io/foundry-rs/foundry:v1.4.0 \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' + +# SLSA build-provenance attestation +gh attestation verify oci://ghcr.io/foundry-rs/foundry:v1.4.0 \ + --repo foundry-rs/foundry + +# Inspect the buildx-attached SBOM and provenance +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .SBOM }}' +docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 \ + --format '{{ json .Provenance }}' +``` + +To pin to an immutable digest (recommended for reproducible deployments): + +```bash +docker pull ghcr.io/foundry-rs/foundry:v1.4.0 +DIGEST=$(docker buildx imagetools inspect ghcr.io/foundry-rs/foundry:v1.4.0 --format '{{ .Manifest.Digest }}') +cosign verify "ghcr.io/foundry-rs/foundry@${DIGEST}" \ + --certificate-identity-regexp '^https://github.com/foundry-rs/foundry/\.github/workflows/(release|docker-publish)\.yml@.*' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' +``` diff --git a/benches/LATEST.md b/benches/LATEST.md index 238a691229389..cb75f8d68780b 100644 --- a/benches/LATEST.md +++ b/benches/LATEST.md @@ -1,74 +1,108 @@ -# Foundry Benchmark Results +# 📊 Foundry Benchmark Results -**Date**: 2026-04-24 23:10:24 +**Generated at**: 2026-05-02 21:53:46 UTC -## Repositories Tested +## Forge Test + +### Repositories Tested 1. [ithacaxyz/account](https://github.com/ithacaxyz/account) -2. [Vectorized/solady](https://github.com/Vectorized/solady) -3. [Uniswap/v4-core](https://github.com/Uniswap/v4-core) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) 4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) -5. [aave/aave-v4](https://github.com/aave/aave-v4) - -## Foundry Versions +### Foundry Versions - **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) -- **nightly**: forge Version: 1.6.0-nightly (a249f5c 2026-04-24) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) -## Forge Test - -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| vectorized-solady | 1.46 s | 1.38 s | -| aave-aave-v4 | 4m 14.2s | 3m 29.1s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.78 s | 0.965 s | +| vectorized-solady | 0.995 s | 0.645 s | +| uniswap-v4-core | 5.97 s | 1.51 s | +| sparkdotfi-spark-psm | 19.98 s | 10.20 s | ## Forge Fuzz Test -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | -------- | -| ithacaxyz-account | 2.81 s | 1.59 s | -| vectorized-solady | 1.40 s | 1.34 s | -| Uniswap-v4-core | 3.01 s | 2.87 s | -| sparkdotfi-spark-psm | 2.04 s | 1.87 s | -| aave-aave-v4 | 3m 46.0s | 3m 17.3s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 2.54 s | 0.923 s | +| vectorized-solady | 0.929 s | 0.617 s | +| uniswap-v4-core | 6.44 s | 1.40 s | +| sparkdotfi-spark-psm | 2.25 s | 2.03 s | ## Forge Test (Isolated) -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| Uniswap-v4-core | 3.50 s | 3.48 s. | -| aave-aave-v4 | 4m 14.0s | 3m 53.4s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 3.05 s | 1.02 s | +| vectorized-solady | 0.871 s | 0.741 s | +| uniswap-v4-core | 6.81 s | 1.68 s | +| sparkdotfi-spark-psm | 21.96 s | 11.26 s | + +## Forge Build -## Forge Build (No Cache) +### Repositories Tested -| Repository | v1.5.1 | nightly | -| -------------------- | -------- | -------- | -| ithacaxyz-account | 26.06 s | 26.61 s | -| vectorized-solady | 14.20 s | 14.26 s | -| Uniswap-v4-core | 2m 1.3s | 2m 5.0s | -| sparkdotfi-spark-psm | 15.16 s | 15.30 s | -| aave-aave-v4 | 3m 37.0s | 3m 35.1s | +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [vectorized/solady](https://github.com/vectorized/solady) +3. [uniswap/v4-core](https://github.com/uniswap/v4-core) +4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +### No Cache + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 34.58 s | 33.29 s | +| vectorized-solady | 14.40 s | 14.41 s | +| uniswap-v4-core | 2m 17.6s | 2m 17.7s | +| sparkdotfi-spark-psm | 12.62 s | 12.61 s | -## Forge Build (With Cache) +### With Cache -| Repository | v1.5.1 | nightly | -| -------------------- | ------- | ------- | -| ithacaxyz-account | 0.167 s | 0.201 s | -| vectorized-solady | 0.099 s | 0.098 s | -| Uniswap-v4-core | 0.139 s | 0.140 s | -| sparkdotfi-spark-psm | 0.168 s | 0.173 s | -| aave-aave-v4 | 0.370 s | 0.357 s | +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 0.083 s | 0.089 s | +| vectorized-solady | 0.062 s | 0.064 s | +| uniswap-v4-core | 0.071 s | 0.074 s | +| sparkdotfi-spark-psm | 0.066 s | 0.068 s | ## Forge Coverage -| Repository | v1.5.1 | nightly | -| -------------------- | --------- | ---------- | -| Uniswap-v4-core | 1m 13.9s | 1m 10.3s | -| sparkdotfi-spark-psm | 2m 54.7s | 2m 50.0s | -| aave-aave-v4 | 11m 20.8s | 10m 58.7s | +### Repositories Tested + +1. [ithacaxyz/account](https://github.com/ithacaxyz/account) +2. [uniswap/v4-core](https://github.com/uniswap/v4-core) +3. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +### Foundry Versions + +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **v1.7.0**: forge Version: 1.6.0-v1.7.0 (f83bad9 2026-04-28) + +| Repository | v1.5.1 | v1.7.0 | +|------------|----------|----------| +| ithacaxyz-account | 29.35 s | 18.69 s | +| uniswap-v4-core | 1m 26.8s | 1m 4.1s | +| sparkdotfi-spark-psm | 2m 1.6s | 1m 28.4s | ## System Information -- **OS**: macos -- **CPU**: 12 -- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) \ No newline at end of file + +- **OS**: linux +- **CPU**: 32 +- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) diff --git a/benches/src/main.rs b/benches/src/main.rs index 60e815cecb0ec..8d7134b1c25bc 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -39,9 +39,15 @@ struct Cli { #[clap(long, default_value = ".")] output_dir: PathBuf, - /// Name of the output file (default: LATEST.md) - #[clap(long, default_value = "LATEST.md")] - output_file: String, + /// Name of the output file. Defaults to LATEST.md unless --json-output is set + /// without this flag, in which case no Markdown is written. + #[clap(long)] + output_file: Option, + + /// Filename for a flat JSON summary (benchmark/repo -> mean_seconds). + /// Resolved relative to --output-dir. Used by the nightly regression comparison script. + #[clap(long)] + json_output: Option, /// Run only specific benchmarks (comma-separated: /// forge_test,forge_build_no_cache,forge_build_with_cache,forge_fuzz_test,forge_coverage) @@ -216,12 +222,28 @@ fn main() -> Result<()> { } } - // Generate markdown report - sh_println!("📝 Generating report..."); - let markdown = results.generate_markdown(&versions, &repos); - let output_path = cli.output_dir.join(cli.output_file); - fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; - sh_println!("✅ Report written to: {}", output_path.display()); + // Write Markdown report unless --json-output is set without an explicit --output-file. + let md_filename = match cli.output_file { + Some(f) => Some(f), + None if cli.json_output.is_none() => Some("LATEST.md".to_string()), + None => None, + }; + if let Some(filename) = md_filename { + sh_println!("📝 Generating report..."); + let markdown = results.generate_markdown(&versions, &repos); + let output_path = cli.output_dir.join(filename); + fs::write(&output_path, markdown).wrap_err("Failed to write output file")?; + sh_println!("✅ Report written to: {}", output_path.display()); + } + + if let Some(json_filename) = cli.json_output { + let summary = results.generate_json_summary(&versions); + let json = + serde_json::to_string_pretty(&summary).wrap_err("Failed to serialize JSON summary")?; + let json_path = cli.output_dir.join(json_filename); + fs::write(&json_path, json).wrap_err("Failed to write JSON summary")?; + sh_println!("✅ JSON summary written to: {}", json_path.display()); + } Ok(()) } diff --git a/benches/src/results.rs b/benches/src/results.rs index 447e8ed2766b4..e7d57250fc9a1 100644 --- a/benches/src/results.rs +++ b/benches/src/results.rs @@ -66,6 +66,25 @@ impl BenchmarkResults { self.version_details.insert(version.to_string(), details); } + /// Generate a flat JSON summary mapping `"benchmark/repo" -> mean_seconds`. + /// + /// Used by the nightly regression comparison script. + pub fn generate_json_summary(&self, versions: &[String]) -> HashMap { + let mut summary = HashMap::new(); + for (benchmark_name, version_data) in &self.data { + for version in versions { + if let Some(repo_data) = version_data.get(version) { + for (repo_name, result) in repo_data { + let key = format!("{benchmark_name}/{repo_name}"); + let rounded = (result.mean * 10_000.0).round() / 10_000.0; + summary.insert(key, rounded); + } + } + } + } + summary + } + pub fn generate_markdown(&self, versions: &[String], repos: &[RepoConfig]) -> String { let mut output = String::new(); diff --git a/benchmark.sh b/benchmark.sh index 2faffa93dfa1d..ac6159099069b 100755 --- a/benchmark.sh +++ b/benchmark.sh @@ -1,36 +1,52 @@ #!/bin/bash -versions="v1.3.6,v1.4.0-rc1" +versions="v1.5.1,v1.7.0" # Repositories -export ITHACA_ACCOUNT="ithacaxyz/account:v0.3.2" -export SOLADY_REPO="Vectorized/solady:v0.1.22" -export UNISWAP_V4_CORE="Uniswap/v4-core:59d3ecf" -export SPARK_PSM="sparkdotfi/spark-psm:v1.0.0" +ITHACA_ACCOUNT="ithacaxyz/account:v0.5.7" +SOLADY_REPO="vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test'" +AAVE_V4="aave/aave-v4:af1f0f2ba323ac6fbaaee3abf6be060c78e22d35" +UNISWAP_V4_CORE="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc TickMathTestTest" +SPARK_PSM="sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting" -# Benches -echo "===========FORGE TEST AND BUILD BENCHMARKS===========" +SOLADY_ISOLATE="vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test|LibStringTest'" +ITHACA_ISOLATE="ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_test,forge_fuzz_test,forge_build_no_cache,forge_build_with_cache \ - --output-dir ./benches/results \ - --output-file TEST_BUILD.md +SOLADY_BUILD="vectorized/solady:v0.1.26" +UNISWAP_BUILD="uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75" +SPARK_PSM_BUILD="sparkdotfi/spark-psm:v1.0.0" -echo "===========FORGE COVERAGE BENCHMARKS===========" +# Benches +echo "===========FORGE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $ITHACA_ACCOUNT,$UNISWAP_V4_CORE,$SPARK_PSM \ - --benchmarks forge_coverage \ - --output-dir ./benches/results \ - --output-file COVERAGE.md +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_REPO,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ + --benchmarks forge_test,forge_fuzz_test \ + --output-dir ./benches \ + --output-file forge_test_bench.md echo "===========FORGE ISOLATE TEST BENCHMARKS===========" -foundry-bench --versions $versions \ - --repos $SOLADY_REPO,$UNISWAP_V4_CORE,$SPARK_PSM \ +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ISOLATE,$SOLADY_ISOLATE,$AAVE_V4,$UNISWAP_V4_CORE,$SPARK_PSM" \ --benchmarks forge_isolate_test \ - --output-dir ./benches/results \ - --output-file ISOLATE_TEST.md + --output-dir ./benches \ + --output-file forge_isolate_test_bench.md + +echo "===========FORGE BUILD BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$SOLADY_BUILD,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_build_no_cache,forge_build_with_cache \ + --output-dir ./benches \ + --output-file forge_build_bench.md + +echo "===========FORGE COVERAGE BENCHMARKS===========" + +foundry-bench --versions "$versions" \ + --repos "$ITHACA_ACCOUNT,$AAVE_V4,$UNISWAP_BUILD,$SPARK_PSM_BUILD" \ + --benchmarks forge_coverage \ + --output-dir ./benches \ + --output-file forge_coverage_bench.md echo "===========BENCHMARKS COMPLETED===========" diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index b664266450d07..d6404b21e2e8e 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -20,15 +20,15 @@ required-features = ["cli"] [dependencies] # foundry internal -anvil-core = { path = "core" } +anvil-core = { path = "core", default-features = false } anvil-rpc = { path = "rpc" } anvil-server = { path = "server" } -foundry-cli.workspace = true +foundry-cli = { workspace = true, optional = true } foundry-common.workspace = true foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-networks.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } tempo-chainspec.workspace = true tempo-primitives.workspace = true tempo-precompiles.workspace = true @@ -37,7 +37,7 @@ tempo-revm.workspace = true # alloy alloy-evm = { workspace = true, features = ["call-util"] } -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = ["serde"] } alloy-consensus = { workspace = true, features = ["k256", "kzg"] } alloy-contract = { workspace = true, features = ["pubsub"] } @@ -63,7 +63,8 @@ alloy-transport.workspace = true alloy-chains.workspace = true alloy-genesis.workspace = true alloy-trie.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde"] } +op-alloy-consensus = { workspace = true, features = ["serde"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } # revm revm = { workspace = true, features = [ @@ -73,7 +74,7 @@ revm = { workspace = true, features = [ "c-kzg", ] } revm-inspectors.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } # axum related axum.workspace = true @@ -120,17 +121,28 @@ reqwest.workspace = true foundry-test-utils.workspace = true tokio = { workspace = true, features = ["full"] } -op-alloy-rpc-types.workspace = true tempo-alloy.workspace = true [features] -default = ["cli", "jemalloc", "asm-keccak"] +default = ["cli", "jemalloc", "asm-keccak", "optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "anvil-core/optimism", + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli?/optimism", +] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] -jemalloc = ["foundry-cli/jemalloc"] -mimalloc = ["foundry-cli/mimalloc"] -tracy-allocator = ["foundry-cli/tracy-allocator"] +jemalloc = ["foundry-cli?/jemalloc"] +mimalloc = ["foundry-cli?/mimalloc"] +tracy-allocator = ["foundry-cli?/tracy-allocator"] cli = ["tokio/full", "cmd"] cmd = [ + "dep:foundry-cli", "clap", "clap_complete", "dep:fdlimit", diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index cf4b952ecfaa3..8456413a78b1f 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [dependencies] foundry-common.workspace = true foundry-evm.workspace = true -foundry-primitives.workspace = true +foundry-primitives = { workspace = true, default-features = false } revm = { workspace = true, default-features = false, features = [ "std", "serde", @@ -39,3 +39,11 @@ bytes.workspace = true # misc rand.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-primitives/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index fb75529b82908..4fd2aabb82a8b 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -12,7 +12,9 @@ use clap::Parser; use core::fmt; use foundry_common::shell; use foundry_config::{Chain, Config, FigmentProviders}; -use foundry_evm::hardfork::{EthereumHardfork, OpHardfork}; +#[cfg(feature = "optimism")] +use foundry_evm::hardfork::OpHardfork; +use foundry_evm::hardfork::{EthereumHardfork, FoundryHardfork}; use foundry_evm_networks::NetworkConfigs; use foundry_primitives::FoundryReceiptEnvelope; use futures::FutureExt; @@ -240,15 +242,7 @@ impl NodeArgs { } let hardfork = match &self.hardfork { - Some(hf) => { - if self.evm.networks.is_optimism() { - Some(OpHardfork::from_str(hf)?.into()) - } else if self.evm.networks.is_tempo() { - Some(TempoHardfork::from_str(hf)?.into()) - } else { - Some(EthereumHardfork::from_str(hf)?.into()) - } - } + Some(hf) => Some(parse_hardfork(hf, &self.evm.networks)?), None => None, }; @@ -849,6 +843,19 @@ impl FromStr for ForkUrl { } } +/// Parses a hardfork string against the active network configuration. +fn parse_hardfork(hf: &str, networks: &NetworkConfigs) -> eyre::Result { + #[cfg(feature = "optimism")] + if networks.is_optimism() { + return Ok(OpHardfork::from_str(hf)?.into()); + } + if networks.is_tempo() { + Ok(TempoHardfork::from_str(hf)?.into()) + } else { + Ok(EthereumHardfork::from_str(hf)?.into()) + } +} + /// Clap's value parser for genesis. Loads a genesis.json file. fn read_genesis_file(path: &str) -> Result { foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string()) diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 23cd6e61bc076..7c91590c071fa 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -37,7 +37,7 @@ use foundry_config::Config; use foundry_evm::{ backend::{BlockchainDb, BlockchainDbMeta, SharedBackend}, constants::DEFAULT_CREATE2_DEPLOYER, - hardfork::{FoundryHardfork, OpHardfork}, + hardfork::FoundryHardfork, utils::{ apply_chain_and_block_specific_env_changes, block_env_from_header, get_blob_base_fee_update_fraction, @@ -577,8 +577,9 @@ impl NodeConfig { if let Some(hardfork) = self.hardfork { return hardfork; } + #[cfg(feature = "optimism")] if self.networks.is_optimism() { - return OpHardfork::default().into(); + return foundry_evm::hardforks::OpHardfork::default().into(); } if self.networks.is_tempo() { return TempoHardfork::default().into(); @@ -1079,6 +1080,7 @@ impl NodeConfig { } /// Enable Optimism network features. + #[cfg(feature = "optimism")] #[must_use] pub fn with_optimism(mut self) -> Self { self.networks = NetworkConfigs::with_optimism(); diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 78cce938318c1..4900c18eebd82 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -1882,6 +1882,7 @@ impl EthApi { fn sign_request(&self, from: &Address, typed_tx: FoundryTypedTx) -> Result { match typed_tx { + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => return Ok(build_impersonated(typed_tx)), _ => { for signer in self.signers.iter() { @@ -2210,9 +2211,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; - debug_assert!(requires != provides); + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -2288,11 +2293,10 @@ impl EthApi { let priority = self.transaction_priority(&pending_transaction.transaction); // Tempo txs use a 2D nonce system — no sequential ordering by account nonce. - let (requires, provides) = if let FoundryTxEnvelope::Tempo(aa_tx) = - pending_transaction.transaction.as_ref() - && !aa_tx.tx().nonce_key.is_zero() + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) { - (vec![], vec![pending_transaction.hash().to_vec()]) + (requires, provides) } else { let on_chain_nonce = self.backend.current_nonce(from).await?; let nonce = pending_transaction.transaction.nonce(); @@ -3192,8 +3196,13 @@ impl EthApi { // pre-validate self.backend.validate_pool_transaction(&pending_transaction).await?; - let requires = required_marker(nonce, on_chain_nonce, from); - let provides = vec![to_marker(nonce, from)]; + let (requires, provides) = if let Some((requires, provides)) = + tempo_parallel_nonce_markers(&pending_transaction) + { + (requires, provides) + } else { + (required_marker(nonce, on_chain_nonce, from), vec![to_marker(nonce, from)]) + }; self.add_pending_transaction(pending_transaction, requires, provides) } @@ -3549,6 +3558,7 @@ impl EthApi { requires: Vec, provides: Vec, ) -> Result { + debug_assert!(requires != provides); let from = *pending_transaction.sender(); let priority = self.transaction_priority(&pending_transaction.transaction); let pool_transaction = @@ -3565,7 +3575,9 @@ impl EthApi { FoundryTxEnvelope::Eip1559(_) => self.backend.ensure_eip1559_active(), FoundryTxEnvelope::Eip4844(_) => self.backend.ensure_eip4844_active(), FoundryTxEnvelope::Eip7702(_) => self.backend.ensure_eip7702_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => Err(BlockchainError::InvalidTransactionRequest( "not implemented for post-exec tx".to_string(), )), @@ -3634,6 +3646,20 @@ fn required_marker(provided_nonce: u64, on_chain_nonce: u64, from: Address) -> V if on_chain_nonce <= prev_nonce { vec![to_marker(prev_nonce, from)] } else { Vec::new() } } +fn tempo_parallel_nonce_markers( + pending_transaction: &PendingTransaction, +) -> Option<(Vec, Vec)> { + // Tempo txs with non-zero nonce_key use a 2D nonce system and should not + // be sequenced by account nonce markers. + if let FoundryTxEnvelope::Tempo(aa_tx) = pending_transaction.transaction.as_ref() + && !aa_tx.tx().nonce_key.is_zero() + { + Some((vec![], vec![pending_transaction.hash().to_vec()])) + } else { + None + } +} + fn convert_transact_out(out: &Option) -> Bytes { match out { None => Default::default(), diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 614f409c3eb65..c41937055fdd4 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -67,9 +67,11 @@ impl ReceiptBuilder for FoundryReceiptBuilder { FoundryTxType::Eip1559 => FoundryReceiptEnvelope::Eip1559(receipt), FoundryTxType::Eip4844 => FoundryReceiptEnvelope::Eip4844(receipt), FoundryTxType::Eip7702 => FoundryReceiptEnvelope::Eip7702(receipt), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { unreachable!("deposit receipts are built in commit_transaction") } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt), FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt), } @@ -85,7 +87,7 @@ pub struct AnvilTxResult { pub sender: Address, } -impl TxResult for AnvilTxResult { +impl TxResult for AnvilTxResult { type HaltReason = H; fn result(&self) -> &ResultAndState { @@ -217,12 +219,10 @@ where }) } - fn commit_transaction( - &mut self, - output: Self::Result, - ) -> Result { + fn commit_transaction(&mut self, output: Self::Result) -> GasOutput { let AnvilTxResult { inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type }, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] sender, } = output; @@ -237,6 +237,7 @@ where self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used); } + #[cfg(feature = "optimism")] let receipt = if tx_type == FoundryTxType::Deposit { let deposit_nonce = state.get(&sender).map(|acc| acc.info.nonce); let receipt = alloy_consensus::Receipt { @@ -262,11 +263,19 @@ where cumulative_gas_used: self.gas_used, }) }; + #[cfg(not(feature = "optimism"))] + let receipt = self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx_type, + evm: &self.evm, + result, + state: &state, + cumulative_gas_used: self.gas_used, + }); self.receipts.push(receipt); self.evm.db_mut().commit(state); - Ok(GasOutput::new(gas_used)) + GasOutput::new(gas_used) } fn finish( @@ -429,7 +438,7 @@ where let exec_result = result.result().result.clone(); let gas_used = result.result().result.tx_gas_used(); - executor.commit_transaction(result).expect("commit failed"); + executor.commit_transaction(result); let traces = executor.evm_mut().inspector_mut().finish_transaction(inspector_config); diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index f404031445611..19f39f3d368de 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -57,6 +57,7 @@ use alloy_network::{ AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, Network, NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, UnknownTypedTransaction, }; +#[cfg(feature = "optimism")] use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; use alloy_primitives::{ Address, B256, Bloom, Bytes, TxHash, TxKind, U64, U256, hex, keccak256, logs_bloom, @@ -108,20 +109,60 @@ use foundry_evm::{ }, }; use foundry_evm_networks::NetworkConfigs; +#[cfg(feature = "optimism")] +use foundry_primitives::get_deposit_tx_parts; use foundry_primitives::{ FoundryNetwork, FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope, - FoundryTxReceipt, get_deposit_tx_parts, + FoundryTxReceipt, }; use futures::channel::mpsc::{UnboundedSender, unbounded}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait}; -use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +#[cfg(feature = "optimism")] +use op_revm::{OpSpecId, OpTransaction, transaction::deposit::DepositTransactionParts}; + +/// Side-channel container for OP-specific deposit info produced by +/// [`Backend::build_call_env`] and consumed by the OP transact path. +/// +/// When the `optimism` feature is enabled, this is an alias for +/// `op_revm::DepositTransactionParts`. When disabled, it is a zero-sized +/// stand-in so the eth/tempo dispatch chain still type-checks. +#[cfg(feature = "optimism")] +type OpCallDepositInfo = DepositTransactionParts; +#[cfg(not(feature = "optimism"))] +#[derive(Default, Clone, Debug)] +struct OpCallDepositInfo; + +/// Marker trait that abstracts over the per-network inspector trait bounds +/// required by the in-memory backend. The OP bound is only included when the +/// `optimism` feature is enabled. +#[cfg(feature = "optimism")] +pub trait BackendInspector: + Inspector> + Inspector> + Inspector> +{ +} +#[cfg(feature = "optimism")] +impl BackendInspector for T where + T: Inspector> + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +pub trait BackendInspector: + Inspector> + Inspector> +{ +} +#[cfg(not(feature = "optimism"))] +impl BackendInspector for T where + T: Inspector> + Inspector> +{ +} use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ DatabaseCommit, Inspector, context::{Block as RevmBlock, BlockEnv, Cfg, TxEnv}, context_interface::{ block::BlobExcessGasAndPrice, - result::{EVMError, ExecutionResult, HaltReason, Output, ResultAndState}, + result::{ExecutionResult, HaltReason, Output, ResultAndState}, }, database::{CacheDB, DbAccount, WrapDatabaseRef}, interpreter::InstructionResult, @@ -157,6 +198,8 @@ pub mod cache; pub mod fork_db; pub mod in_memory_db; pub mod inspector; +#[cfg(feature = "optimism")] +pub mod optimism; pub mod state; pub mod storage; @@ -419,6 +462,11 @@ impl Backend { self.genesis.timestamp } + /// Returns the configured genesis block number. + pub const fn genesis_number(&self) -> u64 { + self.genesis.number + } + /// Returns balance of the given account. pub async fn current_balance(&self, address: Address) -> DatabaseResult { Ok(self.get_account(address).await?.balance) @@ -490,12 +538,21 @@ impl Backend { } /// Returns true if op-stack deposits are active - pub fn is_optimism(&self) -> bool { + #[cfg(feature = "optimism")] + pub const fn is_optimism(&self) -> bool { self.networks.is_optimism() } + /// Returns true if op-stack deposits are active. + /// + /// Always `false` when built without the `optimism` feature. + #[cfg(not(feature = "optimism"))] + pub const fn is_optimism(&self) -> bool { + false + } + /// Returns true if Tempo network mode is active - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { self.networks.is_tempo() } @@ -589,7 +646,8 @@ impl Backend { } /// Returns an error if op-stack deposits are not active - pub fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { + #[cfg(feature = "optimism")] + pub const fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { if self.is_optimism() { return Ok(()); } @@ -597,7 +655,7 @@ impl Backend { } /// Returns an error if Tempo transactions are not active - pub fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { + pub const fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { if self.is_tempo() { return Ok(()); } @@ -1139,58 +1197,53 @@ impl Backend { db: &'db DB, evm_env: &EvmEnv, inspector: &mut I, - tx_env: OpTransaction, + tx_env: TxEnv, + op_deposit: OpCallDepositInfo, ) -> Result, BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_env = EvmEnv::new( - evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), - evm_env.block_env.clone(), - ); - let mut evm = OpEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - op_env, - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { - EVMError::Database(db) => EVMError::Database(db), - EVMError::Header(h) => EVMError::Header(h), - EVMError::Custom(s) => EVMError::Custom(s), - EVMError::CustomAny(err) => EVMError::CustomAny(err), - EVMError::Transaction(t) => EVMError::Transaction(t), - })?; - Ok(ResultAndState { - result: result.result.map_haltreason(|h| match h { - OpHaltReason::Base(eth) => eth, - _ => HaltReason::PrecompileError, - }), - state: result.state, - }) - } else if self.is_tempo() { - self.transact_tempo_with_inspector_ref( - db, - evm_env, - inspector, - TempoTxEnv::from(tx_env.base), - ) + let op_tx = OpTransaction { base: tx_env, deposit: op_deposit, ..Default::default() }; + return self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx); + } + // `op_deposit` only matters on the OP path; eth/tempo ignore it. + let _ = op_deposit; + if self.is_tempo() { + self.transact_tempo_with_inspector_ref(db, evm_env, inspector, TempoTxEnv::from(tx_env)) } else { - let mut evm = EthEvmFactory::default().create_evm_with_inspector( - WrapDatabaseRef(db), - evm_env.clone(), - inspector, - ); - self.inject_precompiles(evm.precompiles_mut()); - Ok(evm.transact(tx_env.base)?) + self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env) } } + /// Eth path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an Ethereum EVM, injects precompiles, and transacts with a + /// plain [`TxEnv`]. + fn transact_eth_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: TxEnv, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let mut evm = EthEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + evm_env.clone(), + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + Ok(evm.transact(tx_env)?) + } + /// Builds the appropriate tx env from a [`FoundryTxEnvelope`], executes via the correct /// EVM backend (Op/Tempo/Eth), and returns both the result and the base [`TxEnv`]. fn transact_envelope_with_inspector_ref<'db, I, DB>( @@ -1203,9 +1256,7 @@ impl Backend { ) -> Result<(ResultAndState, TxEnv), BlockchainError> where DB: DatabaseRef + ?Sized, - I: Inspector>> - + Inspector>> - + Inspector>>, + I: BackendInspector>, WrapDatabaseRef<&'db DB>: Database, { if tx.is_tempo() { @@ -1213,14 +1264,21 @@ impl Backend { FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); let base = tx_env.inner.clone(); let result = self.transact_tempo_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) - } else { - let tx_env: OpTransaction = + return Ok((result, base)); + } + #[cfg(feature = "optimism")] + if self.is_optimism() { + let op_tx: OpTransaction = FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); - let base = tx_env.base.clone(); - let result = self.transact_with_inspector_ref(db, evm_env, inspector, tx_env)?; - Ok((result, base)) + let base = op_tx.base.clone(); + let result = self.transact_op_with_inspector_ref(db, evm_env, inspector, op_tx)?; + return Ok((result, base)); } + let tx_env: TxEnv = + FromTxWithEncoded::from_encoded_tx(tx, sender, tx.encoded_2718().into()); + let base = tx_env.clone(); + let result = self.transact_eth_with_inspector_ref(db, evm_env, inspector, tx_env)?; + Ok((result, base)) } /// Creates a Tempo EVM, injects precompiles, and transacts with a native [`TempoTxEnv`]. @@ -1298,6 +1356,7 @@ impl Backend { }}; } + #[cfg(feature = "optimism")] if self.is_optimism() { let op_env = EvmEnv::new( evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), @@ -1305,8 +1364,10 @@ impl Backend { ); let mut evm = OpEvmFactory::::default().create_evm_with_inspector(db, op_env, inspector); - run!(evm) - } else if self.is_tempo() { + return run!(evm); + } + + if self.is_tempo() { let hardfork = TempoHardfork::from(self.hardfork); let tempo_env = EvmEnv::new( evm_env @@ -1338,7 +1399,7 @@ impl Backend { request: WithOtherFields, fee_details: FeeDetails, block_env: BlockEnv, - ) -> (EvmEnv, OpTransaction) { + ) -> (EvmEnv, TxEnv, OpCallDepositInfo) { let tx_type = request.minimal_tx_type() as u8; let WithOtherFields:: { @@ -1391,7 +1452,7 @@ impl Backend { let caller = from.unwrap_or_default(); let to = to.as_ref().and_then(TxKind::to); let blob_hashes = blob_versioned_hashes.unwrap_or_default(); - let mut base = TxEnv { + let mut tx_env = TxEnv { caller, gas_limit, gas_price, @@ -1413,11 +1474,10 @@ impl Backend { blob_hashes, ..Default::default() }; - base.set_signed_authorization(authorization_list.unwrap_or_default()); - let mut tx_env = OpTransaction { base, ..Default::default() }; + tx_env.set_signed_authorization(authorization_list.unwrap_or_default()); if let Some(nonce) = nonce { - tx_env.base.nonce = nonce; + tx_env.nonce = nonce; } if evm_env.block_env.basefee == 0 { @@ -1427,13 +1487,22 @@ impl Backend { } // Deposit transaction? (only valid when op-stack deposits are active) - if self.ensure_op_deposits_active().is_ok() + #[cfg(feature = "optimism")] + let op_deposit = if self.ensure_op_deposits_active().is_ok() && let Ok(deposit) = get_deposit_tx_parts(&other) { - tx_env.deposit = deposit; - } + deposit + } else { + OpCallDepositInfo::default() + }; + #[cfg(not(feature = "optimism"))] + let op_deposit = { + // `other` carries OP-only deposit fields; consumed only when feature is enabled. + let _ = &other; + OpCallDepositInfo + }; - (evm_env, tx_env) + (evm_env, tx_env, op_deposit) } pub fn call_with_state( @@ -1467,13 +1536,13 @@ impl Backend { (fee_token, nonce_key, valid_before, valid_after) }); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state } = if let Some((fee_token, nonce_key, valid_before, valid_after)) = tempo_overrides { use tempo_primitives::transaction::Call; - let base = tx_env.base; + let base = tx_env; let mut tempo_tx = TempoTxEnv::from(base.clone()); tempo_tx.fee_token = fee_token; @@ -1495,7 +1564,13 @@ impl Backend { } self.transact_tempo_with_inspector_ref(state, &evm_env, &mut inspector, tempo_tx)? } else { - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)? + self.transact_with_inspector_ref( + state, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )? }; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -1518,9 +1593,9 @@ impl Backend { let mut inspector = AccessListInspector::new(request.access_list.clone().unwrap_or_default()); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block_env); + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block_env); let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env)?; + self.transact_with_inspector_ref(state, &evm_env, &mut inspector, tx_env, op_deposit)?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); let access_list = inspector.access_list(); Ok((exit_reason, out, gas_used, access_list)) @@ -2912,7 +2987,7 @@ where TracingInspectorConfig::from_geth_call_config(&call_config), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let ResultAndState { result, state: _ } = self .transact_with_inspector_ref( @@ -2920,6 +2995,7 @@ where &evm_env, &mut inspector, tx_env, + op_deposit, )?; inspector.print_logs(); @@ -2945,13 +3021,14 @@ where ), ); - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env, + op_deposit, )?; Ok(inspector @@ -2973,22 +3050,22 @@ where } #[cfg(feature = "js-tracer")] GethDebugTracerType::JsTracer(code) => { - use alloy_evm::IntoTxEnv; let config = tracer_config.into_json(); let mut inspector = revm_inspectors::tracing::js::JsInspector::new(code, config) .map_err(|err| BlockchainError::Message(err.to_string()))?; - let (evm_env, tx_env) = + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block.clone()); let result = self.transact_with_inspector_ref( &cache_db, &evm_env, &mut inspector, tx_env.clone(), + op_deposit, )?; let res = inspector - .json_result(result, &OpTx(tx_env).into_tx_env(), &block, &cache_db) + .json_result(result, &tx_env, &block, &cache_db) .map_err(|err| BlockchainError::Message(err.to_string()))?; Ok(GethTrace::JS(res)) @@ -3001,9 +3078,14 @@ where .build_inspector() .with_tracing_config(TracingInspectorConfig::from_geth_config(&config)); - let (evm_env, tx_env) = self.build_call_env(request, fee_details, block); - let ResultAndState { result, state: _ } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let (evm_env, tx_env, op_deposit) = self.build_call_env(request, fee_details, block); + let ResultAndState { result, state: _ } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; let (exit_reason, gas_used, out, _logs) = unpack_execution_result(result); @@ -3187,10 +3269,7 @@ where f: F, ) -> Result where - for<'a> I: Inspector>>>> - + Inspector>>>> - + Inspector>>>> - + 'a, + for<'a> I: BackendInspector>>> + 'a, for<'a> F: FnOnce(ResultAndState, CacheDB>, I, TxEnv, EvmEnv) -> T, { @@ -3965,7 +4044,7 @@ impl Backend { )? .or_zero_fees(); - let (mut evm_env, tx_env) = self.build_call_env( + let (mut evm_env, tx_env, op_deposit) = self.build_call_env( WithOtherFields::new(request.clone()), fee_details, block_env.clone(), @@ -3986,8 +4065,13 @@ impl Backend { inspector = inspector.with_transfers(); } trace!(target: "backend", env=?evm_env, spec=?evm_env.spec_id(),"simulate evm env"); - let ResultAndState { result, state } = - self.transact_with_inspector_ref(&cache_db, &evm_env, &mut inspector, tx_env)?; + let ResultAndState { result, state } = self.transact_with_inspector_ref( + &cache_db, + &evm_env, + &mut inspector, + tx_env, + op_deposit, + )?; trace!(target: "backend", ?result, ?request, "simulate call"); inspector.print_logs(); @@ -4359,7 +4443,10 @@ where } // Nonce validation — skip for deposits (L1→L2) and Tempo txs (2D nonce system) + #[cfg(feature = "optimism")] let is_deposit_tx = pending.transaction.as_ref().is_deposit(); + #[cfg(not(feature = "optimism"))] + let is_deposit_tx = false; let is_tempo_tx = pending.transaction.as_ref().is_tempo(); let nonce = tx.nonce(); if nonce < account.nonce && !is_deposit_tx && !is_tempo_tx { @@ -4475,6 +4562,7 @@ where ); let value = tx.value(); match tx.as_ref() { + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(deposit_tx) => { // Deposit transactions // https://specs.optimism.io/protocol/deposits.html#execution @@ -4538,6 +4626,7 @@ pub fn transaction_build( info: Option, base_fee: Option, ) -> AnyRpcTransaction { + #[cfg(feature = "optimism")] if let FoundryTxEnvelope::Deposit(deposit_tx) = eth_transaction.as_ref() { let dep_tx = deposit_tx; diff --git a/crates/anvil/src/eth/backend/mem/optimism.rs b/crates/anvil/src/eth/backend/mem/optimism.rs new file mode 100644 index 0000000000000..e9a94cc254fb7 --- /dev/null +++ b/crates/anvil/src/eth/backend/mem/optimism.rs @@ -0,0 +1,61 @@ +//! Optimism-specific transact helpers for the in-memory backend. + +use super::Backend; +use crate::eth::error::BlockchainError; +use alloy_evm::{Database, Evm, EvmEnv, EvmFactory}; +use alloy_network::Network; +use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; +use foundry_evm::backend::DatabaseError; +use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; +use revm::{ + DatabaseRef, Inspector, + context::{ + TxEnv, + result::{EVMError, HaltReason, ResultAndState}, + }, + database_interface::WrapDatabaseRef, +}; + +impl Backend { + /// Optimism path of [`Backend::transact_with_inspector_ref`]. + /// + /// Creates an OP EVM, injects precompiles, transacts, and maps the + /// OP-specific halt reason back to the shared [`HaltReason`]. + pub(super) fn transact_op_with_inspector_ref<'db, I, DB>( + &self, + db: &'db DB, + evm_env: &EvmEnv, + inspector: &mut I, + tx_env: OpTransaction, + ) -> Result, BlockchainError> + where + DB: DatabaseRef + ?Sized, + I: Inspector>>, + WrapDatabaseRef<&'db DB>: Database, + { + let op_env = EvmEnv::new( + evm_env.cfg_env.clone().with_spec_and_mainnet_gas_params(OpSpecId::ISTHMUS), + evm_env.block_env.clone(), + ); + let mut evm = OpEvmFactory::default().create_evm_with_inspector( + WrapDatabaseRef(db), + op_env, + inspector, + ); + self.inject_precompiles(evm.precompiles_mut()); + let result = evm.transact(OpTx(tx_env)).map_err(|e| match e { + EVMError::Database(db) => EVMError::Database(db), + EVMError::Header(h) => EVMError::Header(h), + EVMError::Custom(s) => EVMError::Custom(s), + EVMError::CustomAny(err) => EVMError::CustomAny(err), + EVMError::Transaction(t) => EVMError::Transaction(t), + })?; + Ok(ResultAndState { + result: result.result.map_haltreason(|h| match h { + OpHaltReason::Base(eth) => eth, + _ => HaltReason::PrecompileError, + }), + state: result.state, + }) + } +} diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error/mod.rs similarity index 91% rename from crates/anvil/src/eth/error.rs rename to crates/anvil/src/eth/error/mod.rs index 3b2ada43d7731..482df681b184a 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error/mod.rs @@ -12,7 +12,6 @@ use anvil_rpc::{ response::ResponseResult, }; use foundry_evm::{backend::DatabaseError, decode::RevertDecoder}; -use op_revm::OpTransactionError; use revm::{ context_interface::result::{EVMError, InvalidHeader, InvalidTransaction}, interpreter::InstructionResult, @@ -21,6 +20,9 @@ use serde::Serialize; use tempo_revm::TempoInvalidTransaction; use tokio::time::Duration; +#[cfg(feature = "optimism")] +mod optimism; + pub(crate) type Result = std::result::Result; #[derive(Debug, thiserror::Error)] @@ -163,51 +165,6 @@ where } } -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => match err { - OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), - OpTransactionError::DepositSystemTxPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::HaltedDepositPostRegolith => { - Self::DepositTransactionUnsupported - } - OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), - }, - EVMError::Header(err) => match err { - InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, - InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, - }, - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - -impl From> for BlockchainError -where - T: Into, -{ - fn from(err: EVMError) -> Self { - match err { - EVMError::Transaction(err) => { - let op_err: OpTransactionError = err.0; - EVMError::::Transaction(op_err).into() - } - EVMError::Header(err) => EVMError::::Header(err).into(), - EVMError::Database(err) => err.into(), - EVMError::Custom(err) => Self::Message(err), - EVMError::CustomAny(err) => Self::Message(err.to_string()), - } - } -} - impl From> for BlockchainError where T: Into, @@ -451,16 +408,6 @@ impl From for InvalidTransactionError { } } -impl From for InvalidTransactionError { - fn from(value: OpTransactionError) -> Self { - match value { - OpTransactionError::Base(err) => err.into(), - OpTransactionError::DepositSystemTxPostRegolith - | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, - OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, - } - } -} /// Helper trait to easily convert results to rpc results pub(crate) trait ToRpcResponseResult { fn to_rpc_result(self) -> ResponseResult; @@ -577,9 +524,13 @@ impl ToRpcResponseResult for Result { err => RpcError::internal_error_with(format!("Fork Error: {err:?}")), } } - err @ BlockchainError::EvmError(_) => { - RpcError::internal_error_with(err.to_string()) - } + err @ BlockchainError::EvmError(_) => RpcError { + // VM halts are execution failures, not JSON-RPC server faults. REVERT has a + // dedicated code/data path above; other halts, such as invalid opcode, do not. + code: ErrorCode::TransactionRejected, + message: err.to_string().into(), + data: None, + }, err @ BlockchainError::EvmOverrideError(_) => { RpcError::invalid_params(err.to_string()) } diff --git a/crates/anvil/src/eth/error/optimism.rs b/crates/anvil/src/eth/error/optimism.rs new file mode 100644 index 0000000000000..1207fde30a72a --- /dev/null +++ b/crates/anvil/src/eth/error/optimism.rs @@ -0,0 +1,62 @@ +//! Optimism-specific error conversions for [`BlockchainError`] and +//! [`InvalidTransactionError`]. + +use super::{BlockchainError, InvalidTransactionError}; +use op_revm::OpTransactionError; +use revm::context_interface::result::{EVMError, InvalidHeader}; + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => match err { + OpTransactionError::Base(err) => InvalidTransactionError::from(err).into(), + OpTransactionError::DepositSystemTxPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::HaltedDepositPostRegolith => { + Self::DepositTransactionUnsupported + } + OpTransactionError::MissingEnvelopedTx => Self::InvalidTransaction(err.into()), + }, + EVMError::Header(err) => match err { + InvalidHeader::ExcessBlobGasNotSet => Self::ExcessBlobGasNotSet, + InvalidHeader::PrevrandaoNotSet => Self::PrevrandaoNotSet, + }, + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From> for BlockchainError +where + T: Into, +{ + fn from(err: EVMError) -> Self { + match err { + EVMError::Transaction(err) => { + let op_err: OpTransactionError = err.0; + EVMError::::Transaction(op_err).into() + } + EVMError::Header(err) => EVMError::::Header(err).into(), + EVMError::Database(err) => err.into(), + EVMError::Custom(err) => Self::Message(err), + EVMError::CustomAny(err) => Self::Message(err.to_string()), + } + } +} + +impl From for InvalidTransactionError { + fn from(value: OpTransactionError) -> Self { + match value { + OpTransactionError::Base(err) => err.into(), + OpTransactionError::DepositSystemTxPostRegolith + | OpTransactionError::HaltedDepositPostRegolith => Self::DepositTxErrorPostRegolith, + OpTransactionError::MissingEnvelopedTx => Self::MissingEnvelopedTx, + } + } +} diff --git a/crates/anvil/src/eth/otterscan/api.rs b/crates/anvil/src/eth/otterscan/api.rs index 4da86933020ae..2ccf65ba721f5 100644 --- a/crates/anvil/src/eth/otterscan/api.rs +++ b/crates/anvil/src/eth/otterscan/api.rs @@ -155,9 +155,12 @@ impl EthApi { let best = self.backend.best_number(); // we go from given block (defaulting to best) down to first block - // considering only post-fork + // considering only post-fork (or post-genesis in non-fork mode) let from = if block_number == 0 { best } else { block_number - 1 }; - let to = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + let to = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let first_page = from >= best; let mut last_page = false; @@ -198,8 +201,11 @@ impl EthApi { node_info!("ots_searchTransactionsAfter"); let best = self.backend.best_number(); - // we go from the first post-fork block, up to the tip - let first_block = self.get_fork().map(|f| f.block_number() + 1).unwrap_or(1); + // we go from the first post-fork (or post-genesis) block, up to the tip + let first_block = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let from = if block_number == 0 { first_block } else { block_number + 1 }; let to = best; @@ -248,7 +254,10 @@ impl EthApi { ) -> Result> { node_info!("ots_getTransactionBySenderAndNonce"); - let from = self.get_fork().map(|f| f.block_number() + 1).unwrap_or_default(); + let from = self + .get_fork() + .map(|f| f.block_number() + 1) + .unwrap_or_else(|| self.backend.genesis_number() + 1); let to = self.backend.best_number(); for n in (from..=to).rev() { diff --git a/crates/anvil/src/eth/pool/transactions.rs b/crates/anvil/src/eth/pool/transactions.rs index df65822e1eab3..5280987483dd7 100644 --- a/crates/anvil/src/eth/pool/transactions.rs +++ b/crates/anvil/src/eth/pool/transactions.rs @@ -123,10 +123,10 @@ impl PoolTransaction { impl fmt::Debug for PoolTransaction { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "Transaction {{ ")?; - write!(fmt, "hash: {:?}, ", &self.pending_transaction.hash())?; + write!(fmt, "hash: {:?}, ", self.pending_transaction.hash())?; write!(fmt, "requires: [{}], ", hex_fmt_many(self.requires.iter()))?; write!(fmt, "provides: [{}], ", hex_fmt_many(self.provides.iter()))?; - write!(fmt, "raw tx: {:?}", &self.pending_transaction)?; + write!(fmt, "raw tx: {:?}", self.pending_transaction)?; write!(fmt, "}}")?; Ok(()) } diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index 3fdf6192c4537..d1736c3093056 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -1,5 +1,7 @@ use crate::eth::error::BlockchainError; -use alloy_consensus::{Sealed, SignableTransaction}; +#[cfg(feature = "optimism")] +use alloy_consensus::Sealed; +use alloy_consensus::SignableTransaction; use alloy_dyn_abi::TypedData; use alloy_network::{Network, TxSignerSync}; use alloy_primitives::{Address, B256, Signature, map::AddressHashMap}; @@ -130,9 +132,11 @@ impl Signer for DevSigner { let sig = signer.sign_transaction_sync(&mut t)?; FoundryTxEnvelope::Eip4844(t.into_signed(sig)) } + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(_) => { unreachable!("op deposit txs should not be signed") } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be signed") } @@ -156,7 +160,9 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope { FoundryTypedTx::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)), FoundryTypedTx::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)), FoundryTypedTx::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(_) => { unreachable!("op post-exec txs should not be impersonated") } diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm/mod.rs similarity index 64% rename from crates/anvil/src/evm.rs rename to crates/anvil/src/evm/mod.rs index d1b40ba56ebbd..85e43d371b097 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm/mod.rs @@ -2,6 +2,9 @@ use alloy_evm::precompiles::DynPrecompile; use alloy_primitives::Address; use std::fmt::Debug; +#[cfg(feature = "optimism")] +mod optimism; + /// Object-safe trait that enables injecting extra precompiles when using /// `anvil` as a library. pub trait PrecompileFactory: Send + Sync + Unpin + Debug { @@ -15,14 +18,12 @@ mod tests { use crate::PrecompileFactory; use alloy_evm::{ - EthEvm, Evm, EvmEnv, EvmFactory, + EthEvm, Evm, eth::EthEvmContext, precompiles::{DynPrecompile, PrecompilesMap}, }; - use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; - use alloy_primitives::{Address, Bytes, TxKind, U256, address}; + use alloy_primitives::{Address, Bytes, TxKind, address}; use itertools::Itertools; - use op_revm::{OpSpecId, OpTransaction}; use revm::{ Journal, context::{BlockEnv, CfgEnv, Evm as RevmEvm, JournalTr, LocalContext, TxEnv}, @@ -35,20 +36,19 @@ mod tests { }; // A precompile activated in the `Prague` spec (BLS12-381 G2 map). - const ETH_PRAGUE_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000011"); + pub(super) const ETH_PRAGUE_PRECOMPILE: Address = + address!("0x0000000000000000000000000000000000000011"); // A precompile activated in the `Osaka` spec (EIP-7951). const ETH_OSAKA_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A precompile activated in the `Isthmus` spec. - const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); - // A custom precompile address and payload for testing. - const PRECOMPILE_ADDR: Address = address!("0x0000000000000000000000000000000000000071"); - const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; + pub(super) const PRECOMPILE_ADDR: Address = + address!("0x0000000000000000000000000000000000000071"); + pub(super) const PAYLOAD: &[u8] = &[0xde, 0xad, 0xbe, 0xef]; #[derive(Debug)] - struct CustomPrecompileFactory; + pub(super) struct CustomPrecompileFactory; impl PrecompileFactory for CustomPrecompileFactory { fn precompiles(&self) -> Vec<(Address, DynPrecompile)> { @@ -109,34 +109,6 @@ mod tests { (tx_env, eth_evm) } - /// Creates a new OP EVM instance. - fn create_op_evm( - _spec: SpecId, - op_spec: OpSpecId, - ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { - let tx = OpTx(OpTransaction:: { - base: TxEnv { - kind: TxKind::Call(PRECOMPILE_ADDR), - data: PAYLOAD.into(), - ..Default::default() - }, - ..Default::default() - }); - - let mut evm = OpEvmFactory::::default().create_evm_with_inspector( - EmptyDB::default(), - EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), - NoOpInspector, - ); - - if op_spec == OpSpecId::ISTHMUS { - evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); - evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); - } - - (tx, evm) - } - #[test] fn build_eth_evm_with_extra_precompiles_osaka_spec() { let (tx_env, mut evm) = create_eth_evm(SpecId::OSAKA); @@ -187,38 +159,4 @@ mod tests { assert!(result.result.is_success()); assert_eq!(result.result.output(), Some(&PAYLOAD.into())); } - - #[test] - fn build_op_evm_with_extra_precompiles_isthmus_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); - - assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } - - #[test] - fn build_op_evm_with_extra_precompiles_bedrock_spec() { - let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); - - assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); - assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); - - assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); - - let result = evm.transact(tx).unwrap(); - assert!(result.result.is_success()); - assert_eq!(result.result.output(), Some(&PAYLOAD.into())); - } } diff --git a/crates/anvil/src/evm/optimism.rs b/crates/anvil/src/evm/optimism.rs new file mode 100644 index 0000000000000..526375fec31ea --- /dev/null +++ b/crates/anvil/src/evm/optimism.rs @@ -0,0 +1,87 @@ +//! Optimism-specific EVM helpers. + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use super::super::tests::{ + CustomPrecompileFactory, ETH_PRAGUE_PRECOMPILE, PAYLOAD, PRECOMPILE_ADDR, + }; + use crate::PrecompileFactory; + use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; + use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; + use alloy_primitives::{Address, TxKind, U256, address}; + use itertools::Itertools; + use op_revm::{OpSpecId, OpTransaction}; + use revm::{ + context::{BlockEnv, CfgEnv, TxEnv}, + database::{EmptyDB, EmptyDBTyped}, + inspector::NoOpInspector, + primitives::hardfork::SpecId, + }; + + // A precompile activated in the `Isthmus` spec. + const OP_ISTHMUS_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000100"); + + /// Creates a new OP EVM instance. + fn create_op_evm( + _spec: SpecId, + op_spec: OpSpecId, + ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { + let tx = OpTx(OpTransaction:: { + base: TxEnv { + kind: TxKind::Call(PRECOMPILE_ADDR), + data: PAYLOAD.into(), + ..Default::default() + }, + ..Default::default() + }); + + let mut evm = OpEvmFactory::::default().create_evm_with_inspector( + EmptyDB::default(), + EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), + NoOpInspector, + ); + + if op_spec == OpSpecId::ISTHMUS { + evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); + evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); + } + + (tx, evm) + } + + #[test] + fn build_op_evm_with_extra_precompiles_isthmus_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::ISTHMUS); + + assert!(evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } + + #[test] + fn build_op_evm_with_extra_precompiles_bedrock_spec() { + let (tx, mut evm) = create_op_evm(SpecId::OSAKA, OpSpecId::BEDROCK); + + assert!(!evm.precompiles().addresses().contains(&OP_ISTHMUS_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(Ð_PRAGUE_PRECOMPILE)); + assert!(!evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + evm.precompiles_mut().extend_precompiles(CustomPrecompileFactory.precompiles()); + + assert!(evm.precompiles().addresses().contains(&PRECOMPILE_ADDR)); + + let result = evm.transact(tx).unwrap(); + assert!(result.result.is_success()); + assert_eq!(result.result.output(), Some(&PAYLOAD.into())); + } +} diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index 26e587e8b5123..a661e9c765b26 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::{ error::{NodeError, NodeResult}, eth::{ diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index c4879e36d5240..84cc9b861bbbf 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -11,6 +11,7 @@ mod gas; mod genesis; mod ipc; mod logs; +#[cfg(feature = "optimism")] mod optimism; mod otterscan; mod proof; @@ -18,6 +19,7 @@ mod pubsub; mod revert; mod sign; mod simulate; +#[cfg(feature = "cmd")] mod state; mod tempo; mod traces; diff --git a/crates/anvil/tests/it/revert.rs b/crates/anvil/tests/it/revert.rs index ab85fc89abf80..a15454fa5593e 100644 --- a/crates/anvil/tests/it/revert.rs +++ b/crates/anvil/tests/it/revert.rs @@ -28,6 +28,38 @@ async fn test_deploy_reverting() { assert!(!receipt.inner.inner.status()); } +#[tokio::test(flavor = "multi_thread")] +async fn test_invalid_opcode_rpc_error_code() { + let (_api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + let sender = handle.dev_accounts().next().unwrap(); + + // Deploy a contract whose runtime bytecode is the invalid opcode 0xfe. + let code = bytes!("60fe60005360016000f3"); + let tx = TransactionRequest::default().from(sender).with_deploy_code(code); + let receipt = provider + .send_transaction(WithOtherFields::new(tx)) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + let contract = receipt.contract_address.unwrap(); + + for (method, params) in [ + ("eth_call", serde_json::json!([{ "from": sender, "to": contract }, "latest"])), + ("eth_estimateGas", serde_json::json!([{ "from": sender, "to": contract }])), + ] { + let error = rpc_error(&handle.http_endpoint(), method, params).await; + assert_eq!(error["code"], serde_json::json!(-32003), "{error}"); + assert!(error.get("data").is_none(), "{error}"); + + let message = error["message"].as_str().unwrap(); + assert!(message.contains("EVM error InvalidFEOpcode"), "{error}"); + assert!(!message.contains("execution reverted"), "{error}"); + } +} + #[tokio::test(flavor = "multi_thread")] async fn test_revert_messages() { sol!( @@ -124,3 +156,21 @@ async fn test_solc_revert_custom_errors() { let s = err.to_string(); assert!(s.contains("execution reverted"), "{s:?}"); } + +async fn rpc_error(endpoint: &str, method: &str, params: serde_json::Value) -> serde_json::Value { + let response = reqwest::Client::new() + .post(endpoint) + .json(&serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + })) + .send() + .await + .unwrap(); + assert_eq!(response.status(), reqwest::StatusCode::OK); + + let body = response.json::().await.unwrap(); + body.get("error").cloned().unwrap_or_else(|| panic!("expected JSON-RPC error, got {body}")) +} diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index dc5be77a244da..03be26a631a11 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -61,9 +61,9 @@ tempo-contracts.workspace = true tempo-primitives.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-flz.workspace = true -op-alloy-network.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-flz = { workspace = true, optional = true } +op-alloy-network = { workspace = true, optional = true } chrono.workspace = true eyre.workspace = true @@ -100,7 +100,7 @@ anvil.workspace = true foundry-test-utils.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -109,3 +109,12 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "dep:op-alloy-flz", + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "foundry-common/optimism", + "foundry-evm-networks/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 6dc2ed6acf430..23fda25c40faa 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -29,6 +29,7 @@ use foundry_common::{ shell, stdin, }; use foundry_evm_networks::NetworkVariant; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; use std::time::Instant; use tempo_alloy::TempoNetwork; @@ -351,6 +352,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let output = if raw || fields.contains(&"raw".into()) { match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -569,6 +571,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // Can use either --raw or specify raw as a field let is_raw = raw || field.as_ref().is_some_and(|f| f == "raw"); let output = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { let provider = ProviderBuilder::::from_config(&config)?.build()?; @@ -791,6 +794,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::DecodeTransaction { tx, network } => { let tx = stdin::unwrap_line(tx)?; let decoded_tx = match network { + #[cfg(feature = "optimism")] Some(NetworkVariant::Optimism) => { SimpleCast::decode_raw_transaction::(&tx)? } @@ -809,6 +813,9 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::Erc20Token { command } => command.run().await?, CastSubcommand::Tip20Token { command } => command.run().await?, CastSubcommand::Keychain { command } => command.run().await?, + CastSubcommand::Tempo { command } => command.run().await?, + CastSubcommand::VirtualAddress { command } => command.run().await?, + #[cfg(feature = "optimism")] CastSubcommand::DAEstimate(cmd) => { cmd.run().await?; } diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index ae5c9668f522f..e25798086d512 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy_consensus::SignableTransaction; use alloy_eips::eip2718::Encodable2718; -use alloy_network::{EthereumWallet, NetworkTransactionBuilder}; +use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder}; use alloy_primitives::Address; use alloy_provider::Provider; use alloy_signer::Signer; @@ -17,7 +17,7 @@ use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use tempo_alloy::TempoNetwork; @@ -53,7 +53,7 @@ pub struct BatchMakeTxArgs { impl BatchMakeTxArgs { pub async fn run(self) -> Result<()> { - let Self { calls, tx, eth, raw_unsigned, ethsign } = self; + let Self { calls, mut tx, eth, raw_unsigned, ethsign } = self; let has_nonce = tx.nonce.is_some(); if calls.is_empty() { @@ -63,6 +63,10 @@ impl BatchMakeTxArgs { let config = eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + // Resolve signer to detect keychain mode let (signer, tempo_access_key) = eth.wallet.maybe_signer().await?; @@ -92,14 +96,14 @@ impl BatchMakeTxArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as raw unsigned. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Set calls on the transaction builder.tx.calls = tempo_calls; @@ -117,6 +121,7 @@ impl BatchMakeTxArgs { let from = eth.wallet.from.unwrap_or(Address::ZERO); let (tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -125,6 +130,7 @@ impl BatchMakeTxArgs { if ethsign { let (tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; return Ok(()); @@ -137,7 +143,9 @@ impl BatchMakeTxArgs { }; let signed_tx = if let Some(ref access_key) = tempo_access_key { - let (tx, _) = tx_builder.build(access_key.wallet_address).await?; + let (tx, _) = + tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let raw_tx = tx .sign_with_access_key( &provider, @@ -151,6 +159,7 @@ impl BatchMakeTxArgs { } else { tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?; let (tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; let envelope = tx.build(&EthereumWallet::new(signer)).await?; alloy_primitives::hex::encode(envelope.encoded_2718()) }; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index ec7254b08e2f5..33128ae897898 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -9,14 +9,14 @@ use crate::{ cmd::send::{cast_send, cast_send_with_access_key}, tx::{self, CastTxBuilder, SendTxOpts}, }; -use alloy_network::EthereumWallet; +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::TransactionOpts, - utils::{self, LoadConfig}, + utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::provider::ProviderBuilder; use std::time::Duration; @@ -50,7 +50,7 @@ pub struct BatchSendArgs { impl BatchSendArgs { pub async fn run(self) -> Result<()> { - let Self { calls, send_tx, tx, unlocked } = self; + let Self { calls, send_tx, mut tx, unlocked } = self; if calls.is_empty() { return Err(eyre!("No calls specified. Use --call to specify at least one call.")); @@ -59,6 +59,10 @@ impl BatchSendArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -93,14 +97,14 @@ impl BatchSendArgs { sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; - // Build transaction request with calls - let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; - - // Set key_id for access key transactions + // Preserve key_id for modes that do not call build_with_access_key, such as unlocked. if let Some(ref access_key) = tempo_access_key { - builder.tx.set_key_id(access_key.key_address); + tx.tempo.key_id = Some(access_key.key_address); } + // Build transaction request with calls + let mut builder = CastTxBuilder::::new(&provider, tx, &config).await?; + // Access the inner tx and set calls builder.tx.calls = tempo_calls; @@ -116,6 +120,7 @@ impl BatchSendArgs { if unlocked { let (tx, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; cast_send( provider, tx, @@ -132,7 +137,12 @@ impl BatchSendArgs { }; if let Some(ref access_key) = tempo_access_key { - let (tx_request, _) = builder.build(access_key.wallet_address).await?; + let (tx_request, _) = + builder.build_with_access_key(access_key.wallet_address, access_key).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; cast_send_with_access_key( &provider, tx_request, @@ -146,6 +156,10 @@ impl BatchSendArgs { } else { tx::validate_from_address(send_tx.eth.wallet.from, Signer::address(&signer))?; let (tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() .wallet(wallet) diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 3637f166a53df..63ea17f707e03 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -33,10 +33,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork}, }, executors::TracingExecutor, opts::EvmOpts, @@ -222,18 +224,19 @@ impl CallArgs { return self.run_curl().await; } if self.tx.tempo.is_tempo() { - self.run_with_network::().await - } else { - let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); - let mut evm_opts = figment.extract::()?; - evm_opts.infer_network_from_fork().await; + return self.run_with_network::().await; + } - if evm_opts.networks.is_optimism() { - self.run_with_network::().await - } else { - self.run_with_network::().await - } + let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); + let mut evm_opts = figment.extract::()?; + evm_opts.infer_network_from_fork().await; + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_network::().await; } + + self.run_with_network::().await } pub async fn run_with_network(self) -> Result<()> diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index 8b7d80786dfad..897a01c39202a 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -1,9 +1,12 @@ use alloy_ens::NameOrAddress; -use alloy_network::EthereumWallet; +use std::time::Duration; + +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_primitives::{Address, U256, hex, keccak256}; -use alloy_provider::ProviderBuilder as AlloyProviderBuilder; +use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::Signer; use alloy_sol_types::SolCall; +use alloy_transport::TransportError; use chrono::DateTime; use clap::Parser; use eyre::Result; @@ -12,11 +15,16 @@ use foundry_cli::{ utils::LoadConfig, }; use foundry_common::{ + FoundryTransactionBuilder, provider::ProviderBuilder, - shell, - tempo::{self, KeyType, KeysFile, WalletType, read_tempo_keys_file, tempo_keys_path}, + sh_warn, shell, + tempo::{ + self, KeyType, KeysFile, TEMPO_BROWSER_GAS_BUFFER, WalletType, read_tempo_keys_file, + tempo_keys_path, + }, }; use foundry_evm::hardfork::TempoHardfork; +use serde::Deserialize; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_contracts::precompiles::{ ACCOUNT_KEYCHAIN_ADDRESS, IAccountKeychain, @@ -24,13 +32,16 @@ use tempo_contracts::precompiles::{ CallScope, KeyInfo, KeyRestrictions, LegacyTokenLimit, SelectorRule, SignatureType, TokenLimit, }, + ITIP20, PATH_USD_ADDRESS, account_keychain::{authorizeKeyCall, legacyAuthorizeKeyCall}, }; use yansi::Paint; +use foundry_cli::utils::{maybe_print_resolved_lane, resolve_lane}; + use crate::{ - cmd::send::{cast_send, cast_send_with_access_key}, - tx::{CastTxBuilder, SendTxOpts}, + cmd::send::cast_send, + tx::{CastTxBuilder, CastTxSender, SendTxOpts}, }; /// Tempo keychain management commands. @@ -62,6 +73,19 @@ pub enum KeychainSubcommand { rpc: RpcOpts, }, + /// Inspect an access key policy using the local key registry and on-chain state. + Inspect { + /// The key address to inspect. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + #[command(flatten)] + rpc: RpcOpts, + }, + /// Authorize a new key on-chain via the AccountKeychain precompile. #[command(visible_alias = "auth")] Authorize { @@ -183,8 +207,92 @@ pub enum KeychainSubcommand { #[command(flatten)] send_tx: SendTxOpts, }, + + /// Read or edit TIP-1011 access-key permissions. + Policy { + #[command(subcommand)] + command: KeychainPolicySubcommand, + }, +} + +/// Higher-level access-key policy editing commands. +#[derive(Debug, Parser)] +pub enum KeychainPolicySubcommand { + /// Add or widen an allowed call rule for a target contract. + AddCall { + /// The key address to update. + key_address: Address, + + /// Root account address. Required when the key is not present in the local keys.toml. + #[arg(long, visible_alias = "wallet-address", value_name = "ADDRESS")] + root_account: Option
, + + /// Target contract address. + #[arg(long)] + target: Address, + + /// Function selector, full signature, or known TIP-20 shorthand. + #[arg(long, value_parser = parse_selector_arg)] + selector: SelectorArg, + + /// Optional recipient/spender restrictions for selector calls. + #[arg(long, value_delimiter = ',')] + recipients: Vec
, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Update a token spending limit amount for a key. + SetLimit { + /// The key address to update. + key_address: Address, + + /// Token address, numeric TIP-20 token id, or PathUSD. + #[arg(long, value_parser = parse_policy_token)] + token: Address, + + /// New raw token-denominated limit. + #[arg(long)] + amount: U256, + + /// Limit period such as 7d, 24h, or 3600s. + /// + /// The current AccountKeychain update entrypoint cannot change periods, so non-zero + /// values are rejected. + #[arg(long, value_parser = parse_period)] + period: Option, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, + + /// Remove all allowed-call rules for a target contract. + RemoveTarget { + /// The key address to update. + key_address: Address, + + /// Target contract address to remove. + #[arg(long)] + target: Address, + + #[command(flatten)] + tx: TransactionOpts, + + #[command(flatten)] + send_tx: SendTxOpts, + }, } +#[derive(Debug, Clone, Copy)] +pub struct SelectorArg([u8; 4]); + fn parse_signature_type(s: &str) -> Result { match s.to_lowercase().as_str() { "secp256k1" => Ok(SignatureType::Secp256k1), @@ -203,6 +311,15 @@ const fn signature_type_name(t: &SignatureType) -> &'static str { } } +const fn signature_type_label(t: &SignatureType) -> &'static str { + match t { + SignatureType::Secp256k1 => "Secp256k1", + SignatureType::P256 => "P256", + SignatureType::WebAuthn => "WebAuthn", + _ => "unknown", + } +} + const fn key_type_name(t: &KeyType) -> &'static str { match t { KeyType::Secp256k1 => "secp256k1", @@ -211,6 +328,14 @@ const fn key_type_name(t: &KeyType) -> &'static str { } } +const fn key_type_label(t: &KeyType) -> &'static str { + match t { + KeyType::Secp256k1 => "Secp256k1", + KeyType::P256 => "P256", + KeyType::WebAuthn => "WebAuthn", + } +} + const fn wallet_type_name(t: &WalletType) -> &'static str { match t { WalletType::Local => "local", @@ -332,6 +457,48 @@ fn parse_selector_bytes(s: &str) -> Result<[u8; 4], String> { } } +fn parse_selector_arg(s: &str) -> Result { + parse_selector_bytes(s).map(SelectorArg) +} + +fn parse_policy_token(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "pathusd" | "path_usd" | "path-usd" | "usd" => Ok(PATH_USD_ADDRESS), + _ => foundry_cli::utils::parse_fee_token_address(s).map_err(|e| e.to_string()), + } +} + +fn parse_period(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err("period cannot be empty".to_string()); + } + + let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()); + if split == 0 { + return Err(format!( + "invalid period '{s}': expected a number followed by s, m, h, d, or w" + )); + } + + let value: u64 = + s[..split].parse().map_err(|e| format!("invalid period value '{}': {e}", &s[..split]))?; + let multiplier = match &s[split..].to_ascii_lowercase()[..] { + "" | "s" => 1, + "m" => 60, + "h" => 60 * 60, + "d" => 24 * 60 * 60, + "w" => 7 * 24 * 60 * 60, + unit => { + return Err(format!( + "invalid period unit '{unit}' in '{s}' (expected s, m, h, d, or w)" + )); + } + }; + + value.checked_mul(multiplier).ok_or_else(|| format!("period '{s}' is too large")) +} + /// Represents a single scope entry in JSON format for `--scopes`. #[derive(serde::Deserialize)] struct JsonCallScope { @@ -402,6 +569,9 @@ impl KeychainSubcommand { Self::Check { wallet_address, key_address, rpc } => { run_check(wallet_address, key_address, rpc).await } + Self::Inspect { key_address, root_account, rpc } => { + run_inspect(key_address, root_account, rpc).await + } Self::Authorize { key_address, key_type, @@ -443,6 +613,40 @@ impl KeychainSubcommand { Self::RemoveScope { key_address, target, tx, send_tx } => { run_remove_scope(key_address, target, tx, send_tx).await } + Self::Policy { command } => command.run().await, + } + } +} + +impl KeychainPolicySubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::AddCall { + key_address, + root_account, + target, + selector, + recipients, + tx, + send_tx, + } => { + run_policy_add_call( + key_address, + root_account, + target, + selector.0, + recipients, + tx, + send_tx, + ) + .await + } + Self::SetLimit { key_address, token, amount, period, tx, send_tx } => { + run_policy_set_limit(key_address, token, amount, period, tx, send_tx).await + } + Self::RemoveTarget { key_address, target, tx, send_tx } => { + run_remove_scope(key_address, target, tx, send_tx).await + } } } } @@ -500,6 +704,143 @@ fn run_show(wallet_address: Address) -> Result<()> { Ok(()) } +#[derive(Debug, Clone)] +struct LocalLimitMetadata { + token: Address, + amount: String, +} + +#[derive(Debug, Clone)] +struct KeyMetadata { + root_account: Address, + key_type: Option, + limits: Vec, +} + +#[derive(Debug, Clone)] +struct InspectedLimit { + token: Address, + configured_amount: Option, + remaining: U256, + period_end: Option, +} + +#[derive(Debug, Clone)] +enum AllowedCallsView { + Unsupported, + Unrestricted, + Scoped(Vec), +} + +/// `cast keychain inspect ` — inspect on-chain key policy. +async fn run_inspect( + key_address: Address, + root_account: Option
, + rpc: RpcOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let info: KeyInfo = provider.get_keychain_key(metadata.root_account, key_address).await?; + let provisioned = info.keyId != Address::ZERO; + let is_t3 = is_tempo_hardfork_active(&provider, TempoHardfork::T3).await?; + + let mut limits = Vec::new(); + if info.enforceLimits { + for local_limit in &metadata.limits { + let (remaining, period_end) = if is_t3 { + let limit = provider + .get_keychain_remaining_limit_with_period( + metadata.root_account, + key_address, + local_limit.token, + ) + .await?; + (limit.remaining, Some(limit.periodEnd)) + } else { + let remaining = provider + .account_keychain() + .getRemainingLimit(metadata.root_account, key_address, local_limit.token) + .call() + .await?; + (remaining, None) + }; + + limits.push(InspectedLimit { + token: local_limit.token, + configured_amount: Some(local_limit.amount.clone()), + remaining, + period_end, + }); + } + } + + let allowed_calls = if is_t3 { + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + if allowed.isScoped { + AllowedCallsView::Scoped(allowed.scopes) + } else { + AllowedCallsView::Unrestricted + } + } else { + AllowedCallsView::Unsupported + }; + + if shell::is_json() { + let key_type = if provisioned { + signature_type_name(&info.signatureType).to_string() + } else { + metadata + .key_type + .map(|key_type| key_type_name(&key_type).to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }; + let json = serde_json::json!({ + "root_account": metadata.root_account.to_string(), + "key_id": key_address.to_string(), + "provisioned": provisioned, + "type": key_type, + "expiry": provisioned.then_some(info.expiry), + "expiry_human": provisioned.then(|| format_expiry_for_inspect(info.expiry)), + "enforce_limits": info.enforceLimits, + "is_revoked": info.isRevoked, + "limits": limits.iter().map(inspected_limit_to_json).collect::>(), + "allowed_calls": allowed_calls_to_json(&allowed_calls), + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + return Ok(()); + } + + let key_type = if provisioned { + signature_type_label(&info.signatureType) + } else { + metadata.key_type.map(|key_type| key_type_label(&key_type)).unwrap_or("unknown") + }; + + sh_println!("Root account: {}", metadata.root_account)?; + sh_println!("Key id: {key_address}")?; + sh_println!("Type: {key_type}")?; + + if info.isRevoked { + sh_println!("Status: revoked")?; + } else if !provisioned { + sh_println!("Status: not provisioned")?; + } else { + sh_println!("Status: active")?; + sh_println!("Expiry: {}", format_expiry_for_inspect(info.expiry))?; + } + + print_inspected_limits(info.enforceLimits, &limits)?; + print_allowed_calls(&allowed_calls)?; + + Ok(()) +} + /// `cast keychain check` / `cast keychain info` — query on-chain key status. async fn run_check(wallet_address: Address, key_address: Address, rpc: RpcOpts) -> Result<()> { let config = rpc.load_config()?; @@ -584,7 +925,7 @@ async fn run_authorize( let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let calldata = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let calldata = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { // T3+ authorizeKey(address,SignatureType,KeyRestrictions) let restrictions = KeyRestrictions { expiry, @@ -634,7 +975,7 @@ async fn run_remaining_limit( let config = rpc.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; - let remaining: U256 = if provider.is_hardfork_active(TempoHardfork::T3).await? { + let remaining: U256 = if is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { provider.get_keychain_remaining_limit(wallet_address, key_address, token).await? } else { // Pre-T3: use the legacy getRemainingLimit(address,address,address) @@ -646,7 +987,7 @@ async fn run_remaining_limit( }; if shell::is_json() { - sh_println!("{}", serde_json::to_string(&remaining.to_string())?)?; + sh_println!("{}", serde_json::json!({ "remaining": remaining.to_string() }))?; } else { sh_println!("{remaining}")?; } @@ -695,6 +1036,88 @@ async fn run_remove_scope( send_keychain_tx(calldata, tx_opts, &send_tx).await } +/// `cast keychain policy add-call` — merge a selector rule into a target scope. +async fn run_policy_add_call( + key_address: Address, + root_account: Option
, + target: Address, + selector: [u8; 4], + recipients: Vec
, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + let metadata = resolve_key_metadata(key_address, root_account)?; + let config = send_tx.eth.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + if !is_tempo_hardfork_active(&provider, TempoHardfork::T3).await? { + eyre::bail!("allowed-call policy editing requires the Tempo T3 hardfork"); + } + + let allowed = provider + .account_keychain() + .getAllowedCalls(metadata.root_account, key_address) + .call() + .await?; + + let new_rule = SelectorRule { selector: selector.into(), recipients }; + let existing_target = allowed + .isScoped + .then(|| allowed.scopes.into_iter().find(|scope| scope.target == target)) + .flatten(); + + let (target_scope, changed) = match existing_target { + Some(mut scope) => { + if scope.selectorRules.is_empty() { + sh_warn!( + "Allowed calls for {} already allow any selector; leaving wildcard scope unchanged", + address_label_with_address(target) + )?; + } + let changed = add_selector_rule_to_scope(&mut scope, new_rule); + (scope, changed) + } + None => (CallScope { target, selectorRules: vec![new_rule] }, true), + }; + + if !changed { + if shell::is_json() { + sh_println!( + "{}", + serde_json::json!({ "status": "already_present", "target": target.to_string() }) + )?; + } else { + sh_println!("Allowed call already present for {}", address_label_with_address(target))?; + } + return Ok(()); + } + + let calldata = + IAccountKeychain::setAllowedCallsCall { keyId: key_address, scopes: vec![target_scope] } + .abi_encode(); + send_keychain_tx(calldata, tx_opts, &send_tx).await +} + +/// `cast keychain policy set-limit` — update a spending limit amount. +async fn run_policy_set_limit( + key_address: Address, + token: Address, + amount: U256, + period: Option, + tx_opts: TransactionOpts, + send_tx: SendTxOpts, +) -> Result<()> { + if period.is_some_and(|period| period != 0) { + eyre::bail!( + "--period is not supported by the current AccountKeychain updateSpendingLimit \ + precompile; periods can only be set when authorizing a key" + ); + } + + // updateSpendingLimit authorizes against msg.sender; the root account is not part of calldata. + run_update_limit(key_address, token, amount, tx_opts, send_tx).await +} + /// Shared helper to send a keychain precompile transaction. async fn send_keychain_tx( calldata: Vec, @@ -702,16 +1125,22 @@ async fn send_keychain_tx( send_tx: &SendTxOpts, ) -> Result<()> { let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; + let tempo_sponsor = + if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? }; let config = send_tx.eth.load_config()?; let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); let provider = ProviderBuilder::::from_config(&config)?.build()?; - // Inject key_id for correct gas estimation with keychain signature overhead. - if let Some(ref ak) = tempo_access_key { - tx_opts.tempo.key_id = Some(ak.key_address); + if let Some(interval) = send_tx.poll_interval { + provider.client().set_poll_interval(Duration::from_secs(interval)); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx_opts.tempo.nonce_key` from the lane. + let resolved_lane = resolve_lane(&mut tx_opts.tempo, &config.root)?; + let builder = CastTxBuilder::new(&provider, tx_opts, &config) .await? .with_to(Some(NameOrAddress::Address(ACCOUNT_KEYCHAIN_ADDRESS))) @@ -719,27 +1148,70 @@ async fn send_keychain_tx( .with_code_sig_and_args(None, Some(hex::encode_prefixed(&calldata)), vec![]) .await?; - if let Some(ref ak) = tempo_access_key { - let signer = - signer.as_ref().ok_or_else(|| eyre::eyre!("signer required for access key"))?; - let (tx, _) = builder.build(ak.wallet_address).await?; - cast_send_with_access_key( - &provider, - tx, - signer, - ak, - send_tx.cast_async, - send_tx.confirmations, - timeout, - ) - .await?; + // Keychain management calls are authorized by the root account. Access keys can use their + // permissions, but cannot mutate their own key policy. + let browser = send_tx.browser.run::().await?; + + if print_sponsor_hash { + let from = if let Some(ref browser) = browser { + browser.address() + } else { + signer + .as_ref() + .ok_or_else(|| { + eyre::eyre!( + "--tempo.print-sponsor-hash requires a root account signer, such as \ + --browser, --private-key, or --keystore" + ) + })? + .address() + }; + + let (tx, _) = builder.build(from).await?; + let hash = tx + .compute_sponsor_hash(from) + .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?; + if shell::is_json() { + sh_println!("{}", serde_json::json!({ "sponsor_hash": format!("{hash:?}") }))?; + } else { + sh_println!("{hash:?}")?; + } + return Ok(()); + } + + if let Some(browser) = browser { + let chain = builder.chain(); + let (mut tx, _) = builder.build(browser.address()).await?; + if chain.is_tempo() + && let Some(gas) = tx.gas_limit() + { + tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); + } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } + + let tx_hash = browser.send_transaction_via_browser(tx).await?; + CastTxSender::new(&provider) + .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout) + .await?; + } else if tempo_access_key.is_some() { + eyre::bail!( + "keychain policy changes must be signed by the root account; the selected `--from` \ + resolved to a Tempo access key. Use `--browser` for passkey roots, or pass a root \ + account signer with `--private-key`, `--keystore`, Ledger, Trezor, AWS, GCP, or Turnkey." + ); } else { let signer = match signer { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (mut tx, _) = builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let wallet = EthereumWallet::from(signer); let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default() @@ -753,6 +1225,361 @@ async fn send_keychain_tx( Ok(()) } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AnvilNodeInfo { + hard_fork: Option, + network: Option, +} + +async fn is_tempo_hardfork_active

(provider: &P, hardfork: TempoHardfork) -> Result +where + P: Provider, +{ + match provider.is_hardfork_active(hardfork).await { + Ok(active) => Ok(active), + Err(err) if is_rpc_method_not_found(&err) => { + match anvil_tempo_hardfork_active(provider, hardfork).await { + Ok(Some(active)) => Ok(active), + _ => Err(err.into()), + } + } + Err(err) => Err(err.into()), + } +} + +async fn anvil_tempo_hardfork_active

( + provider: &P, + hardfork: TempoHardfork, +) -> Result, TransportError> +where + P: Provider, +{ + let info = provider.raw_request::<_, AnvilNodeInfo>("anvil_nodeInfo".into(), ()).await?; + Ok(active_from_anvil_node_info(&info, hardfork)) +} + +fn active_from_anvil_node_info(info: &AnvilNodeInfo, hardfork: TempoHardfork) -> Option { + (info.network.as_deref() == Some("tempo")).then(|| { + info.hard_fork + .as_deref() + .and_then(|active_hardfork| active_hardfork.parse::().ok()) + .is_some_and(|active_hardfork| active_hardfork >= hardfork) + }) +} + +fn is_rpc_method_not_found(err: &TransportError) -> bool { + err.as_error_resp().is_some_and(|payload| payload.code == -32601) +} + +fn resolve_key_metadata( + key_address: Address, + root_account: Option

, +) -> Result { + let keys_file = read_tempo_keys_file(); + + if let Some(root_account) = root_account { + if let Some(keys_file) = keys_file.as_ref() + && let Some(entry) = keys_file.keys.iter().find(|entry| { + entry.wallet_address == root_account + && key_entry_effective_key(entry) == key_address + }) + { + return Ok(key_metadata_from_entry(entry)); + } + + return Ok(KeyMetadata { root_account, key_type: None, limits: Vec::new() }); + } + + let Some(keys_file) = keys_file.as_ref() else { + eyre::bail!( + "key {key_address} was not found because the local keys file could not be read at {}; pass --root-account", + tempo_keys_path_display() + ); + }; + + let matches: Vec<_> = keys_file + .keys + .iter() + .filter(|entry| key_entry_effective_key(entry) == key_address) + .collect(); + + if matches.is_empty() { + eyre::bail!( + "key {key_address} was not found in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let root_account = matches[0].wallet_address; + if matches.iter().any(|entry| entry.wallet_address != root_account) { + eyre::bail!( + "key {key_address} matches multiple root accounts in {}; pass --root-account", + tempo_keys_path_display() + ); + } + + let entry = + matches.iter().copied().find(|entry| !entry.limits.is_empty()).unwrap_or(matches[0]); + Ok(key_metadata_from_entry(entry)) +} + +fn key_entry_effective_key(entry: &tempo::KeyEntry) -> Address { + entry.key_address.unwrap_or(entry.wallet_address) +} + +fn key_metadata_from_entry(entry: &tempo::KeyEntry) -> KeyMetadata { + KeyMetadata { + root_account: entry.wallet_address, + key_type: Some(entry.key_type), + limits: entry + .limits + .iter() + .map(|limit| LocalLimitMetadata { token: limit.currency, amount: limit.limit.clone() }) + .collect(), + } +} + +fn tempo_keys_path_display() -> String { + tempo_keys_path() + .map(|path| path.display().to_string()) + .unwrap_or_else(|| "(unknown)".to_string()) +} + +fn add_selector_rule_to_scope(scope: &mut CallScope, rule: SelectorRule) -> bool { + if scope.selectorRules.is_empty() { + return false; + } + + let Some(existing_rule) = + scope.selectorRules.iter_mut().find(|existing| existing.selector == rule.selector) + else { + scope.selectorRules.push(rule); + return true; + }; + + if existing_rule.recipients.is_empty() { + return false; + } + + if rule.recipients.is_empty() { + existing_rule.recipients = Vec::new(); + return true; + } + + let mut changed = false; + for recipient in rule.recipients { + if !existing_rule.recipients.contains(&recipient) { + existing_rule.recipients.push(recipient); + changed = true; + } + } + changed +} + +fn inspected_limit_to_json(limit: &InspectedLimit) -> serde_json::Value { + serde_json::json!({ + "token": limit.token.to_string(), + "token_label": address_label(limit.token), + "configured_amount": limit.configured_amount.as_deref(), + "remaining": limit.remaining.to_string(), + "period_end": limit.period_end, + "period_end_human": limit.period_end.and_then(|period_end| { + (period_end != 0).then(|| format_period_end(period_end)) + }), + }) +} + +fn allowed_calls_to_json(allowed_calls: &AllowedCallsView) -> serde_json::Value { + match allowed_calls { + AllowedCallsView::Unsupported => serde_json::json!({ + "mode": "unsupported", + "scopes": [], + }), + AllowedCallsView::Unrestricted => serde_json::json!({ + "mode": "any", + "scopes": [], + }), + AllowedCallsView::Scoped(scopes) => serde_json::json!({ + "mode": if scopes.is_empty() { "none" } else { "scoped" }, + "scopes": scopes.iter().map(call_scope_to_json).collect::>(), + }), + } +} + +fn call_scope_to_json(scope: &CallScope) -> serde_json::Value { + serde_json::json!({ + "target": scope.target.to_string(), + "target_label": address_label(scope.target), + "selectors": scope.selectorRules.iter().map(selector_rule_to_json).collect::>(), + }) +} + +fn selector_rule_to_json(rule: &SelectorRule) -> serde_json::Value { + serde_json::json!({ + "selector": selector_hex(&rule.selector.0), + "signature": selector_signature(&rule.selector.0), + "recipients": rule.recipients.iter().map(ToString::to_string).collect::>(), + }) +} + +fn print_inspected_limits(enforce_limits: bool, limits: &[InspectedLimit]) -> Result<()> { + if !enforce_limits { + sh_println!("Limits: none")?; + return Ok(()); + } + + sh_println!("Limits:")?; + if limits.is_empty() { + sh_println!(" enforced, but no local limit metadata was found")?; + return Ok(()); + } + + for limit in limits { + let configured = limit.configured_amount.as_deref().unwrap_or("unknown"); + let period = limit + .period_end + .and_then(|period_end| { + (period_end != 0).then(|| format!(" ({})", format_period_end(period_end))) + }) + .unwrap_or_default(); + sh_println!( + " {}: {} / {} remaining{}", + address_label(limit.token), + limit.remaining, + configured, + period + )?; + } + + Ok(()) +} + +fn print_allowed_calls(allowed_calls: &AllowedCallsView) -> Result<()> { + match allowed_calls { + AllowedCallsView::Unsupported => sh_println!("Allowed calls: unsupported before T3")?, + AllowedCallsView::Unrestricted => sh_println!("Allowed calls: any")?, + AllowedCallsView::Scoped(scopes) if scopes.is_empty() => { + sh_println!("Allowed calls: none")?; + } + AllowedCallsView::Scoped(scopes) => { + sh_println!("Allowed calls:")?; + for scope in scopes { + sh_println!(" {}:", address_label_with_address(scope.target))?; + if scope.selectorRules.is_empty() { + sh_println!(" any selector")?; + continue; + } + + for rule in &scope.selectorRules { + sh_println!( + " {} -> {}", + format_selector(&rule.selector.0), + format_recipients(&rule.recipients) + )?; + } + } + } + } + + Ok(()) +} + +fn address_label(address: Address) -> String { + if address == PATH_USD_ADDRESS { "PathUSD".to_string() } else { address.to_string() } +} + +fn address_label_with_address(address: Address) -> String { + if address == PATH_USD_ADDRESS { format!("PathUSD ({address})") } else { address.to_string() } +} + +fn format_selector(selector: &[u8; 4]) -> String { + selector_signature(selector).map(str::to_string).unwrap_or_else(|| selector_hex(selector)) +} + +fn selector_signature(selector: &[u8; 4]) -> Option<&'static str> { + if selector == &ITIP20::transferCall::SELECTOR { + Some("transfer(address,uint256)") + } else if selector == &ITIP20::approveCall::SELECTOR { + Some("approve(address,uint256)") + } else if selector == &ITIP20::transferFromCall::SELECTOR { + Some("transferFrom(address,address,uint256)") + } else if selector == &ITIP20::transferWithMemoCall::SELECTOR { + Some("transferWithMemo(address,uint256,bytes32)") + } else if selector == &ITIP20::transferFromWithMemoCall::SELECTOR { + Some("transferFromWithMemo(address,address,uint256,bytes32)") + } else if selector == &ITIP20::mintCall::SELECTOR { + Some("mint(address,uint256)") + } else if selector == &ITIP20::burnCall::SELECTOR { + Some("burn(uint256)") + } else { + None + } +} + +fn selector_hex(selector: &[u8; 4]) -> String { + hex::encode_prefixed(selector) +} + +fn format_recipients(recipients: &[Address]) -> String { + if recipients.is_empty() { + return "any recipient".to_string(); + } + + let recipients = recipients.iter().map(ToString::to_string).collect::>().join(", "); + format!("recipients [{recipients}]") +} + +fn format_expiry_for_inspect(expiry: u64) -> String { + if expiry == u64::MAX { + return "never".to_string(); + } + + format!("{} ({})", format_timestamp_iso(expiry), format_relative_timestamp(expiry)) +} + +fn format_period_end(period_end: u64) -> String { + format!("period resets {}", format_relative_timestamp(period_end)) +} + +fn format_timestamp_iso(timestamp: u64) -> String { + DateTime::from_timestamp(timestamp as i64, 0) + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| timestamp.to_string()) +} + +fn format_relative_timestamp(timestamp: u64) -> String { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if timestamp == now { + "now".to_string() + } else if timestamp > now { + format!("in {}", format_duration_words(timestamp - now)) + } else { + format!("{} ago", format_duration_words(now - timestamp)) + } +} + +fn format_duration_words(seconds: u64) -> String { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + const DAY: u64 = 24 * HOUR; + + if seconds >= DAY { + let days = seconds / DAY; + if days == 1 { "1 day".to_string() } else { format!("{days} days") } + } else if seconds >= HOUR { + format!("{}h", seconds / HOUR) + } else if seconds >= MINUTE { + format!("{}m", seconds / MINUTE) + } else { + format!("{seconds}s") + } +} + fn format_expiry(expiry: u64) -> String { if expiry == u64::MAX { return "never".to_string(); @@ -842,6 +1669,7 @@ fn key_entry_to_json(entry: &tempo::KeyEntry) -> serde_json::Value { #[cfg(test)] mod tests { use super::*; + use alloy_json_rpc::ErrorPayload; use std::str::FromStr; #[test] @@ -967,4 +1795,144 @@ mod tests { let json = r#"[{"target":"0x20c0000000000000000000000000000000000001","selectors":[{"selector":"transfer","recipients":[],"bogus":true}]}]"#; assert!(parse_scopes_json(json).is_err()); } + + #[test] + fn test_parse_policy_token_path_usd() { + assert_eq!(parse_policy_token("PathUSD").unwrap(), PATH_USD_ADDRESS); + assert_eq!(parse_policy_token("path-usd").unwrap(), PATH_USD_ADDRESS); + } + + #[test] + fn test_parse_period_units() { + assert_eq!(parse_period("0").unwrap(), 0); + assert_eq!(parse_period("30s").unwrap(), 30); + assert_eq!(parse_period("5m").unwrap(), 300); + assert_eq!(parse_period("2h").unwrap(), 7200); + assert_eq!(parse_period("7d").unwrap(), 604800); + assert_eq!(parse_period("2w").unwrap(), 1209600); + assert!(parse_period("1mo").is_err()); + } + + #[test] + fn test_add_selector_rule_merges_recipients() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let second = Address::from_str("0x2222222222222222222222222222222222222222").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![second], + }, + ); + + assert!(changed); + assert_eq!(scope.selectorRules.len(), 1); + assert_eq!(scope.selectorRules[0].recipients, vec![first, second]); + } + + #[test] + fn test_add_selector_rule_empty_recipients_widens_to_any() { + let first = Address::from_str("0x1111111111111111111111111111111111111111").unwrap(); + let mut scope = CallScope { + target: PATH_USD_ADDRESS, + selectorRules: vec![SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![first], + }], + }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("approve").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(changed); + assert!(scope.selectorRules[0].recipients.is_empty()); + } + + #[test] + fn test_add_selector_rule_target_wildcard_is_unchanged() { + let mut scope = CallScope { target: PATH_USD_ADDRESS, selectorRules: vec![] }; + + let changed = add_selector_rule_to_scope( + &mut scope, + SelectorRule { + selector: parse_selector_bytes("transfer").unwrap().into(), + recipients: vec![], + }, + ); + + assert!(!changed); + assert!(scope.selectorRules.is_empty()); + } + + #[test] + fn test_policy_set_limit_parses() { + let key = "0x1111111111111111111111111111111111111111"; + + let command = KeychainSubcommand::try_parse_from([ + "keychain", + "policy", + "set-limit", + key, + "--token", + "PathUSD", + "--amount", + "123", + ]) + .unwrap(); + + match command { + KeychainSubcommand::Policy { + command: + KeychainPolicySubcommand::SetLimit { key_address, token, amount, period, .. }, + } => { + assert_eq!(key_address, Address::from_str(key).unwrap()); + assert_eq!(token, PATH_USD_ADDRESS); + assert_eq!(amount, U256::from(123)); + assert_eq!(period, None); + } + other => panic!("unexpected command: {other:?}"), + } + } + + #[test] + fn test_active_from_anvil_node_info_requires_tempo_network() { + let tempo_t3 = + AnvilNodeInfo { network: Some("tempo".to_string()), hard_fork: Some("T3".to_string()) }; + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T2), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T3), Some(true)); + assert_eq!(active_from_anvil_node_info(&tempo_t3, TempoHardfork::T4), Some(false)); + + let ethereum_t3 = AnvilNodeInfo { + network: Some("ethereum".to_string()), + hard_fork: Some("T3".to_string()), + }; + assert_eq!(active_from_anvil_node_info(ðereum_t3, TempoHardfork::T3), None); + } + + #[test] + fn test_rpc_method_not_found_detection() { + let method_missing: TransportError = + TransportError::ErrorResp(ErrorPayload::method_not_found()); + assert!(is_rpc_method_not_found(&method_missing)); + + let internal_error: TransportError = + TransportError::ErrorResp(ErrorPayload::internal_error()); + assert!(!is_rpc_method_not_found(&internal_error)); + + let transport_error = alloy_transport::TransportErrorKind::backend_gone(); + assert!(!is_rpc_method_not_found(&transport_error)); + } } diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index 8aaf6e97a1827..67178cd093d7b 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -2,7 +2,9 @@ use crate::tx::{self, CastTxBuilder}; use alloy_consensus::{SignableTransaction, Signed}; use alloy_eips::Encodable2718; use alloy_ens::NameOrAddress; -use alloy_network::{Ethereum, EthereumWallet, Network, NetworkTransactionBuilder}; +use alloy_network::{ + Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, TransactionBuilder, +}; use alloy_primitives::{Address, hex}; use alloy_provider::Provider; use alloy_signer::{Signature, Signer}; @@ -10,7 +12,7 @@ use clap::Parser; use eyre::Result; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, - utils::LoadConfig, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder}; use std::{path::PathBuf, str::FromStr}; @@ -94,9 +96,13 @@ impl MakeTxArgs { N::UnsignedTx: SignableTransaction, N::TransactionRequest: FoundryTransactionBuilder, { - let Self { to, mut sig, mut args, command, tx, path, eth, raw_unsigned, ethsign } = self; + let Self { to, mut sig, mut args, command, mut tx, path, eth, raw_unsigned, ethsign } = + self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -117,6 +123,11 @@ impl MakeTxArgs { let provider = ProviderBuilder::::from_config(&config)?.build()?; + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane. + // Must happen before `tx.clone()` so the cloned tx carries the resolved nonce_key. + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config) .await? .with_to(to) @@ -139,6 +150,10 @@ impl MakeTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + if raw_unsigned { // Build unsigned raw tx // Check if nonce is provided when --from is not specified @@ -148,11 +163,20 @@ impl MakeTxArgs { "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce" ); } + if tempo_sponsor.is_some() && eth.wallet.from.is_none() { + eyre::bail!( + "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender" + ); + } // Use zero address as placeholder for unsigned transactions let from = eth.wallet.from.unwrap_or(Address::ZERO); - let (tx, _) = tx_builder.build(from).await?; + let (mut tx, _) = tx_builder.build(from).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -162,7 +186,11 @@ impl MakeTxArgs { if ethsign { // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has // unlocked accounts. - let (tx, _) = tx_builder.build(config.sender).await?; + let (mut tx, _) = tx_builder.build(config.sender).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, config.sender).await?; + } let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; @@ -176,7 +204,11 @@ impl MakeTxArgs { tx::validate_from_address(eth.wallet.from, from)?; - let (tx, _) = tx_builder.build(&signer).await?; + let (mut tx, _) = tx_builder.build(&signer).await?; + maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx, from).await?; + } let tx = tx.build(&EthereumWallet::new(signer)).await?; diff --git a/crates/cast/src/cmd/mod.rs b/crates/cast/src/cmd/mod.rs index 6a9d11f5dc61d..0b1b26615694a 100644 --- a/crates/cast/src/cmd/mod.rs +++ b/crates/cast/src/cmd/mod.rs @@ -15,6 +15,7 @@ pub mod call; pub mod constructor_args; pub mod create2; pub mod creation_code; +#[cfg(feature = "optimism")] pub mod da_estimate; pub mod erc20; pub mod estimate; @@ -28,7 +29,9 @@ pub mod rpc; pub mod run; pub mod send; pub mod storage; +pub mod tempo; pub mod tip20; pub mod trace; pub mod txpool; +pub mod vaddr; pub mod wallet; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 84699d6cd6956..7e52a9e265f25 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -29,10 +29,12 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock as _, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, }, executors::{EvmError, Executor, TracingExecutor}, hardforks::FoundryHardfork, @@ -123,12 +125,15 @@ impl RunArgs { evm_opts.infer_network_from_fork().await; if evm_opts.networks.is_tempo() { - self.run_with_evm::().await - } else if evm_opts.networks.is_optimism() { - self.run_with_evm::().await - } else { - self.run_with_evm::().await + return self.run_with_evm::().await; + } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_with_evm::().await; } + + self.run_with_evm::().await } async fn run_with_evm(self) -> Result<()> { diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 2d6e248cc7a73..421aae1f2153e 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -8,7 +8,10 @@ use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_signer::{Signature, Signer}; use clap::Parser; use eyre::{Result, eyre}; -use foundry_cli::{opts::TransactionOpts, utils::LoadConfig}; +use foundry_cli::{ + opts::TransactionOpts, + utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, +}; use foundry_common::{ FoundryTransactionBuilder, fmt::{UIfmt, UIfmtReceiptExt}, @@ -119,7 +122,9 @@ impl SendTxArgs { self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; - let sponsor_signature = tx.tempo.sponsor_signature; + let expires_at = tx.tempo.resolve_expires(); + let tempo_sponsor = + if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None }; @@ -183,6 +188,8 @@ impl SendTxArgs { let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?; + if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } @@ -202,13 +209,19 @@ impl SendTxArgs { // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit. if print_sponsor_hash { - // Use the pre-resolved signer to derive the actual sender address, since the - // sponsor hash commits to the sender. - let signer = pre_resolved_signer.as_ref().ok_or_else(|| { - eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") - })?; - let from = signer.address(); - let (tx, _) = builder.build(from).await?; + let (tx, from) = if let Some(ref ak) = access_key { + let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?; + (tx, ak.wallet_address) + } else { + // Use the pre-resolved signer to derive the actual sender address, since the + // sponsor hash commits to the sender. + let signer = pre_resolved_signer.as_ref().ok_or_else(|| { + eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)") + })?; + let from = signer.address(); + let (tx, _) = builder.build(from).await?; + (tx, from) + }; let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?; @@ -216,6 +229,10 @@ impl SendTxArgs { return Ok(()); } + if let Some(ts) = expires_at { + sh_println!("Transaction expires at unix timestamp {ts}")?; + } + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); // Launch browser signer if `--browser` flag is set @@ -245,11 +262,18 @@ impl SendTxArgs { } } - let (tx, _) = builder.build(config.sender).await?; + let (mut tx_request, _) = builder.build(config.sender).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, config.sender).await?; + } cast_send( provider, - tx, + tx_request, send_tx.cast_async, send_tx.sync, send_tx.confirmations, @@ -261,6 +285,10 @@ impl SendTxArgs { } else if let Some(browser) = browser { let chain = builder.chain(); let (mut tx_request, _) = builder.build(browser.address()).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which // costs more gas for signature verification on Tempo chains. Add a @@ -270,6 +298,9 @@ impl SendTxArgs { { tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, browser.address()).await?; + } let tx_hash = browser.send_transaction_via_browser(tx_request).await?; @@ -283,7 +314,14 @@ impl SendTxArgs { Some(s) => s, None => send_tx.eth.wallet.signer().await?, }; - let (tx_request, _) = builder.build(ak.wallet_address).await?; + let (mut tx_request, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, ak.wallet_address).await?; + } cast_send_with_access_key( &provider, tx_request, @@ -308,11 +346,13 @@ impl SendTxArgs { tx::validate_from_address(send_tx.eth.wallet.from, from)?; let (mut tx_request, _) = builder.build(&signer).await?; + maybe_print_resolved_lane( + resolved_lane.as_ref(), + tx_request.nonce().unwrap_or_default(), + )?; - // Apply sponsor signature after gas estimation so the estimate is - // consistent with what `--tempo.print-sponsor-hash` computes. - if let Some(sig) = sponsor_signature { - tx_request.set_fee_payer_signature(sig); + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut tx_request, from).await?; } let wallet = EthereumWallet::from(signer); diff --git a/crates/cast/src/cmd/tempo.rs b/crates/cast/src/cmd/tempo.rs new file mode 100644 index 0000000000000..6744e6b73c039 --- /dev/null +++ b/crates/cast/src/cmd/tempo.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use eyre::Result; +use foundry_common::tempo::{EnsureAccessKeyConfig, ensure_access_key}; + +/// Tempo wallet integration commands. +#[derive(Debug, Parser)] +pub enum TempoSubcommand { + /// Authorize a new access key against your Tempo wallet via wallet.tempo. + /// + /// Persists the key to `$TEMPO_HOME/wallet/keys.toml` (default + /// `~/.tempo/wallet/keys.toml`). Also runs automatically on a 402 from a + /// Tempo RPC when no local key is configured. + /// + /// Env: `TEMPO_HOME`, `TEMPO_CLI_AUTH_URL` (override auth service). + Login { + /// Chain ID to authorize the key for. Defaults to Tempo mainnet (4217). + #[arg(long, default_value_t = 4217)] + chain_id: u64, + + /// Print the authorization URL to stderr instead of opening a browser. + #[arg(long)] + no_browser: bool, + }, +} + +impl TempoSubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::Login { chain_id, no_browser } => { + let mut cfg = EnsureAccessKeyConfig::from_env(chain_id); + if no_browser { + cfg.no_browser = true; + } + let outcome = ensure_access_key(cfg).await?; + let _ = foundry_common::sh_println!( + "Authorized key {} for wallet {} on chain {}", + outcome.key_address, + outcome.wallet_address, + outcome.chain_id, + ); + Ok(()) + } + } + } +} diff --git a/crates/cast/src/cmd/tip20/mine.rs b/crates/cast/src/cmd/tip20/mine.rs index a5f9062482a01..0367450a19b06 100644 --- a/crates/cast/src/cmd/tip20/mine.rs +++ b/crates/cast/src/cmd/tip20/mine.rs @@ -20,11 +20,11 @@ use tempo_primitives::{MasterId, TempoAddressExt, UserTag}; const POW_BYTES: usize = 4; -pub(super) struct Output { - pub(super) salt: B256, - pub(super) registration_hash: B256, - pub(super) master_id: MasterId, - pub(super) zero_tag_virtual_address: Address, +pub(crate) struct Output { + pub(crate) salt: B256, + pub(crate) registration_hash: B256, + pub(crate) master_id: MasterId, + pub(crate) zero_tag_virtual_address: Address, } pub(super) fn run( @@ -127,7 +127,12 @@ pub(super) async fn register( Ok(()) } -fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Result { +pub(crate) fn mine( + master: Address, + salt: B256, + n_threads: usize, + pow_bytes: usize, +) -> Result { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); @@ -144,7 +149,7 @@ fn mine(master: Address, salt: B256, n_threads: usize, pow_bytes: usize) -> Resu .ok_or_else(|| eyre::eyre!("virtual master mining failed: all threads panicked")) } -fn derive(master: Address, salt: B256) -> Output { +pub(crate) fn derive(master: Address, salt: B256) -> Output { let registration_hash = registration_hash(master, salt); let master_id = MasterId::from_slice(®istration_hash[4..8]); let zero_tag_virtual_address = Address::new_virtual(master_id, UserTag::ZERO); @@ -152,14 +157,14 @@ fn derive(master: Address, salt: B256) -> Output { Output { salt, registration_hash, master_id, zero_tag_virtual_address } } -fn registration_hash(master: Address, salt: B256) -> B256 { +pub(crate) fn registration_hash(master: Address, salt: B256) -> B256 { let mut packed = [0u8; 52]; packed[..20].copy_from_slice(master.as_slice()); packed[20..].copy_from_slice(salt.as_slice()); keccak256(packed) } -fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { +pub(crate) fn has_pow(registration_hash: &B256, pow_bytes: usize) -> bool { registration_hash[..pow_bytes].iter().all(|byte| *byte == 0) } diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index e3c39b2c9bb18..edb4c3b7a57b3 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; mod create; pub(crate) use create::iso4217_warning_message; -mod mine; +pub(crate) mod mine; /// TIP-20 token operations (Tempo). #[derive(Debug, Parser, Clone)] diff --git a/crates/cast/src/cmd/vaddr/create.rs b/crates/cast/src/cmd/vaddr/create.rs new file mode 100644 index 0000000000000..0563b09601323 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/create.rs @@ -0,0 +1,181 @@ +use crate::{ + cmd::{ + erc20::build_provider_with_signer, + send::{cast_send, cast_send_with_access_key}, + tip20::mine, + }, + tx::{SendTxOpts, TxParams}, +}; +use alloy_primitives::{Address, B256}; +use alloy_signer::Signer; +use eyre::Result; +use foundry_cli::utils::{LoadConfig, get_chain}; +use foundry_common::{provider::ProviderBuilder, shell}; +use rand::{RngCore, SeedableRng, rngs::StdRng}; +use serde_json::json; +use std::time::Instant; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; +use tempo_primitives::{TempoAddressExt, UserTag}; + +const POW_BYTES: usize = 4; + +#[allow(clippy::too_many_arguments)] +pub(super) async fn run( + owner: Address, + salt: Option, + tag: u64, + count: u32, + threads: Option, + seed: Option, + no_random: bool, + no_register: bool, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + if count == 0 { + // no virtual addresses to compute + return Ok(()); + } + + if !owner.is_valid_master() { + eyre::bail!( + "invalid owner address {owner}; see https://docs.tempo.xyz/protocol/tips/tip-1022" + ); + } + + let output = if let Some(salt) = salt { + let output = mine::derive(owner, salt); + if !mine::has_pow(&output.registration_hash, POW_BYTES) { + eyre::bail!( + "provided salt does not satisfy TIP-1022 proof of work: {}", + output.registration_hash + ); + } + output + } else { + let mut n_threads = threads.unwrap_or(0); + if n_threads == 0 { + n_threads = std::thread::available_parallelism().map_or(1, |n| n.get()); + } + + let mut start_salt = B256::ZERO; + if !no_random { + let mut rng = match seed { + Some(seed) => StdRng::from_seed(seed.0), + None => StdRng::from_os_rng(), + }; + rng.fill_bytes(&mut start_salt[..]); + } + + if !shell::is_json() { + sh_println!("Mining TIP-1022 salt for {owner} with {n_threads} threads...")?; + } + let timer = Instant::now(); + let output = mine::mine(owner, start_salt, n_threads, POW_BYTES)?; + if !shell::is_json() { + sh_println!("Found salt in {:?}", timer.elapsed())?; + } + output + }; + + const MAX_USER_TAG: u64 = 0x0000_FFFF_FFFF_FFFF; + let mut virtual_addresses = Vec::with_capacity(count as usize); + for i in 0..count { + let tag_value = tag + .checked_add(i as u64) + .filter(|&t| t <= MAX_USER_TAG) + .ok_or_else(|| eyre::eyre!("tag overflow: tag + count exceeds the 6-byte user tag range (max {MAX_USER_TAG:#x})"))?; + let raw = tag_value.to_be_bytes(); + let user_tag = UserTag::new(raw[2..].try_into().expect("slice is 6 bytes")); + let vaddr = Address::new_virtual(output.master_id, user_tag); + virtual_addresses.push((user_tag, vaddr)); + } + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "salt": format!("{}", output.salt), + "registration_hash": format!("{}", output.registration_hash), + "master_id": format!("{}", output.master_id), + "virtual_addresses": virtual_addresses.iter().map(|(tag, addr)| json!({ + "tag": format!("{tag}"), + "address": format!("{addr}"), + })).collect::>(), + }))? + )?; + } else { + sh_println!( + "Salt: {} +Registration hash: {} +Master ID: {}", + output.salt, + output.registration_hash, + output.master_id, + )?; + sh_println!("\nVirtual addresses:")?; + for (tag, vaddr) in &virtual_addresses { + sh_println!(" tag={tag} {vaddr}")?; + } + } + + if no_register { + return Ok(()); + } + + register(owner, output.salt, send_tx, tx_opts).await +} + +async fn register( + owner: Address, + salt: B256, + send_tx: SendTxOpts, + tx_opts: TxParams, +) -> Result<()> { + let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?; + let signer = signer.ok_or_else(|| { + eyre::eyre!("cast vaddr create requires a signer (for example --private-key or --from)") + })?; + + let sender = + tempo_access_key.as_ref().map(|ak| ak.wallet_address).unwrap_or_else(|| signer.address()); + + if sender != owner { + eyre::bail!( + "signer mismatch: salt is for {owner}, but the configured signer would register as {sender}" + ); + } + + let config = send_tx.eth.load_config()?; + let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout); + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider) + .registerVirtualMaster(salt) + .into_transaction_request(); + tx_opts.apply::(&mut tx, get_chain(config.chain, &provider).await?.is_legacy()); + + sh_println!("Submitting registerVirtualMaster({salt})...")?; + + if let Some(ref access_key) = tempo_access_key { + cast_send_with_access_key( + &provider, + tx, + &signer, + access_key, + send_tx.cast_async, + send_tx.confirmations, + timeout, + ) + .await?; + } else { + let provider = build_provider_with_signer::(&send_tx, signer)?; + cast_send(provider, tx, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout) + .await?; + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/mod.rs b/crates/cast/src/cmd/vaddr/mod.rs new file mode 100644 index 0000000000000..446d1e7ece5e2 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/mod.rs @@ -0,0 +1,131 @@ +use crate::tx::{SendTxOpts, TxParams}; +use alloy_primitives::{Address, B256}; +use clap::Parser; +use foundry_cli::opts::RpcOpts; + +mod create; +mod resolve; +mod watch; + +/// TIP-1022 virtual address registry operations (Tempo). +/// +/// Virtual addresses are deterministic 20-byte aliases (masterId || VIRTUAL_MAGIC || userTag) +/// that auto-forward TIP-20 deposits to a registered master wallet at the protocol level, +/// with no on-chain sweep transaction required. +/// +/// See: +#[derive(Debug, Parser, Clone)] +pub enum VaddrSubcommand { + /// Mine a TIP-1022 proof-of-work salt, register as a virtual address master, and print + /// derived virtual addresses for the given owner. + #[command(visible_alias = "c")] + Create { + /// The master (owner) address that will control all virtual addresses under this + /// registration. Must not be the zero address, a virtual address, or a TIP-20 token. + #[arg(long, value_name = "ADDRESS")] + owner: Address, + + /// Use this salt directly instead of mining one. Must satisfy the 32-bit PoW requirement. + #[arg(long, conflicts_with_all = ["seed", "no_random"], value_name = "HEX")] + salt: Option, + + /// Starting user tag for the derived virtual address output (hex-encoded 6 bytes). + #[arg(long, default_value = "0", value_name = "U64")] + tag: u64, + + /// Number of virtual addresses to derive and print. + #[arg(long, default_value = "1", value_name = "N")] + count: u32, + + /// Number of threads to use for mining. Defaults to number of logical cores. + #[arg(long, short = 'j', visible_alias = "jobs")] + threads: Option, + + /// Seed for the random number generator used to initialize the salt search. + #[arg(long, value_name = "HEX")] + seed: Option, + + /// Start salt search from zero instead of a random value. + #[arg(long, conflicts_with = "seed")] + no_random: bool, + + /// Mine and print the salt and derived virtual addresses without submitting the + /// registerVirtualMaster transaction. + #[arg(long)] + no_register: bool, + + #[command(flatten)] + send_tx: Box, + + #[command(flatten)] + tx: Box, + }, + + /// Resolve a virtual address to its registered master and decode its components. + #[command(visible_alias = "r")] + Resolve { + /// The virtual address to resolve. + #[arg(value_name = "ADDRESS")] + addr: Address, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Watch (tail) incoming TIP-20 transfers to a virtual address. + #[command(visible_alias = "w")] + Watch { + /// The virtual address to monitor. + #[arg(value_name = "ADDRESS")] + addr: Address, + + /// Filter on a specific TIP-20 token address. Watches all tokens if omitted. + #[arg(long, value_name = "ADDRESS")] + token: Option
, + + /// Block number to start from. Defaults to the current latest block. + #[arg(long, value_name = "BLOCK")] + from_block: Option, + + #[command(flatten)] + rpc: RpcOpts, + }, +} + +impl VaddrSubcommand { + pub async fn run(self) -> eyre::Result<()> { + match self { + Self::Create { + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + send_tx, + tx, + } => { + create::run( + owner, + salt, + tag, + count, + threads, + seed, + no_random, + no_register, + *send_tx, + *tx, + ) + .await? + } + Self::Resolve { addr, rpc } => resolve::run(addr, rpc).await?, + Self::Watch { addr, token, from_block, rpc } => { + watch::run(addr, token, from_block, rpc).await? + } + } + Ok(()) + } +} diff --git a/crates/cast/src/cmd/vaddr/resolve.rs b/crates/cast/src/cmd/vaddr/resolve.rs new file mode 100644 index 0000000000000..96936f4fe4713 --- /dev/null +++ b/crates/cast/src/cmd/vaddr/resolve.rs @@ -0,0 +1,52 @@ +use alloy_primitives::{Address, hex}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use tempo_alloy::{ + TempoNetwork, + contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry}, +}; + +pub(super) async fn run(addr: Address, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let registry = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider); + + let decode_builder = registry.decodeVirtualAddress(addr); + let resolve_builder = registry.resolveVirtualAddress(addr); + let (decoded, master) = tokio::try_join!(decode_builder.call(), resolve_builder.call())?; + + if !decoded.isVirtual { + sh_println!("{addr} is not a virtual address")?; + return Ok(()); + } + + let master_id = decoded.masterId; + let user_tag = decoded.userTag; + let master: Address = master; + + if shell::is_json() { + let master_address = if master.is_zero() { None } else { Some(format!("{master}")) }; + sh_println!( + "{}", + serde_json::to_string_pretty(&json!({ + "address": format!("{addr}"), + "master_id": format!("0x{}", hex::encode(master_id)), + "user_tag": format!("0x{}", hex::encode(user_tag)), + "master_address": master_address, + }))? + )?; + } else { + sh_println!("Virtual address: {addr}")?; + sh_println!("Master ID: 0x{}", hex::encode(master_id))?; + sh_println!("User tag: 0x{}", hex::encode(user_tag))?; + if master.is_zero() { + sh_println!("Master address: (unregistered)")?; + } else { + sh_println!("Master address: {master}")?; + } + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/vaddr/watch.rs b/crates/cast/src/cmd/vaddr/watch.rs new file mode 100644 index 0000000000000..dc159d5e37c8e --- /dev/null +++ b/crates/cast/src/cmd/vaddr/watch.rs @@ -0,0 +1,108 @@ +use alloy_primitives::{Address, B256, keccak256}; +use alloy_provider::Provider; +use alloy_rpc_types::{BlockNumberOrTag, Filter}; +use eyre::Result; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::{provider::ProviderBuilder, shell}; +use serde_json::json; +use std::sync::LazyLock; +use tempo_alloy::TempoNetwork; +use tempo_primitives::TempoAddressExt; + +static TRANSFER_TOPIC: LazyLock = + LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)")); + +pub(super) async fn run( + addr: Address, + token: Option
, + from_block: Option, + rpc: RpcOpts, +) -> Result<()> { + if !addr.is_virtual() { + eyre::bail!("{addr} is not a virtual address"); + } + + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + + // Transfer(address indexed from, address indexed to, uint256 value) + // topic[0] = event sig, topic[1] = from, topic[2] = to + let to_topic: B256 = { + let mut buf = [0u8; 32]; + buf[12..].copy_from_slice(addr.as_slice()); + buf.into() + }; + + let start = from_block.map(BlockNumberOrTag::Number).unwrap_or(BlockNumberOrTag::Latest); + + let mut filter = + Filter::new().event_signature(*TRANSFER_TOPIC).topic2(to_topic).from_block(start); + + if let Some(tok) = token { + filter = filter.address(tok); + } + + if !shell::is_json() { + sh_println!("Watching transfers to {addr}... (Ctrl-C to stop)")?; + } + + // Fetch logs from the requested start block (historical when from_block is set) + let logs = provider.get_logs(&filter).await?; + for log in &logs { + print_transfer_log(log)?; + } + + // Poll for new logs + let mut last_block = provider.get_block_number().await?; + loop { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let current = provider.get_block_number().await?; + if current > last_block { + let poll_filter = filter.clone().from_block(last_block + 1).to_block(current); + let new_logs = provider.get_logs(&poll_filter).await?; + for log in &new_logs { + print_transfer_log(log)?; + } + last_block = current; + } + } +} + +fn print_transfer_log(log: &alloy_rpc_types::Log) -> Result<()> { + let block = log.block_number.unwrap_or(0); + let tx = log.transaction_hash.unwrap_or_default(); + let token = log.address(); + + // Decode topics: topic[1]=from, topic[2]=to + let from = log.topics().get(1).map(|t| { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&t[12..]); + Address::from(addr) + }); + + // Decode amount from data + let amount = if log.data().data.len() >= 32 { + alloy_primitives::U256::from_be_slice(&log.data().data[..32]) + } else { + alloy_primitives::U256::ZERO + }; + + if shell::is_json() { + sh_println!( + "{}", + serde_json::to_string(&json!({ + "block": block, + "tx": format!("{tx}"), + "token": format!("{token}"), + "from": from.map(|a| format!("{a}")).unwrap_or_default(), + "amount": amount.to_string(), + }))? + )?; + } else { + sh_println!( + "block={block} tx={tx} token={token} from={} amount={amount}", + from.map(|a| a.to_string()).unwrap_or_default(), + )?; + } + Ok(()) +} diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 8e0dd8dd3ed8c..b2378d8bfdc58 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -779,8 +779,7 @@ flag to set your key via: )?; let address = wallet.address(); let success_message = format!( - "`{}` keystore was saved successfully. Address: {:?}", - &account_name, address, + "`{account_name}` keystore was saved successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } @@ -815,7 +814,7 @@ flag to set your key via: format!("Failed to remove keystore file at {}", keystore_path.display()) })?; - let success_message = format!("`{}` keystore was removed successfully.", &name); + let success_message = format!("`{name}` keystore was removed successfully."); sh_println!("{}", success_message.green())?; } Self::PrivateKey { @@ -886,8 +885,7 @@ flag to set your key via: let private_key = B256::from_slice(&wallet.credential().to_bytes()); - let success_message = - format!("{}'s private key is: {}", &account_name, private_key); + let success_message = format!("{account_name}'s private key is: {private_key}"); sh_println!("{}", success_message.green())?; } @@ -945,10 +943,9 @@ flag to set your key via: Some(&account_name), )?; + let address = wallet.address(); let success_message = format!( - "Password for keystore `{}` was changed successfully. Address: {:?}", - &account_name, - wallet.address(), + "Password for keystore `{account_name}` was changed successfully. Address: {address:?}", ); sh_println!("{}", success_message.green())?; } diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index ce5572acebc13..2b1b03486bf04 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -40,6 +40,7 @@ use foundry_common::{ use foundry_config::Chain; use foundry_evm::core::bytecode::InstIter; use futures::{FutureExt, StreamExt, future::Either}; +#[cfg(feature = "optimism")] use op_alloy_consensus as _; use rayon::prelude::*; @@ -60,6 +61,7 @@ pub use foundry_evm::*; pub mod args; pub mod cmd; pub mod opts; +pub mod tempo; pub mod base; pub mod call_spec; @@ -246,7 +248,7 @@ impl + Clone + Unpin, N: Network> Cast { let mut s = vec![format!("gas used: {}", access_list.gas_used), "access list:".to_string()]; for al in access_list.access_list.0 { - s.push(format!("- address: {}", &al.address.to_checksum(None))); + s.push(format!("- address: {}", al.address.to_checksum(None))); if !al.storage_keys.is_empty() { s.push(" keys:".to_string()); for key in al.storage_keys { diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index effc081e8072a..763eb132ddb5c 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -1,11 +1,13 @@ +#[cfg(feature = "optimism")] +use crate::cmd::da_estimate::DAEstimateArgs; use crate::cmd::{ access_list::AccessListArgs, artifact::ArtifactArgs, b2e_payload::B2EPayloadArgs, batch_mktx::BatchMakeTxArgs, batch_send::BatchSendArgs, bind::BindArgs, call::CallArgs, constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs, - da_estimate::DAEstimateArgs, erc20::Erc20Subcommand, estimate::EstimateArgs, - find_block::FindBlockArgs, interface::InterfaceArgs, keychain::KeychainSubcommand, - logs::LogsArgs, mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, - storage::StorageArgs, tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, + erc20::Erc20Subcommand, estimate::EstimateArgs, find_block::FindBlockArgs, + interface::InterfaceArgs, keychain::KeychainSubcommand, logs::LogsArgs, mktx::MakeTxArgs, + rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, tempo::TempoSubcommand, + tip20::Tip20Subcommand, trace::TraceArgs, txpool::TxPoolSubcommands, vaddr::VaddrSubcommand, wallet::WalletSubcommands, }; use alloy_ens::NameOrAddress; @@ -1163,6 +1165,7 @@ pub enum CastSubcommand { command: TxPoolSubcommands, }, /// Estimates the data availability size of a given opstack block. + #[cfg(feature = "optimism")] #[command(name = "da-estimate")] DAEstimate(DAEstimateArgs), @@ -1186,6 +1189,20 @@ pub enum CastSubcommand { #[command(subcommand)] command: KeychainSubcommand, }, + + /// Tempo wallet integration (login, etc.). + Tempo { + #[command(subcommand)] + command: TempoSubcommand, + }, + + /// TIP-1022 virtual address registry operations (Tempo). + #[command(visible_alias = "vaddr")] + VirtualAddress { + #[command(subcommand)] + command: VaddrSubcommand, + }, + #[command(name = "trace")] Trace(TraceArgs), } diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 96f5fc5137575..b58136f4ae9de 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -20,7 +20,7 @@ use foundry_common::{ get_pretty_receipt_w_reason_attr, shell, }; use foundry_config::{Chain, Config}; -use foundry_wallets::{BrowserWalletOpts, WalletOpts, WalletSigner}; +use foundry_wallets::{BrowserWalletOpts, TempoAccessKeyConfig, WalletOpts, WalletSigner}; use itertools::Itertools; use serde_json::value::RawValue; use std::{fmt::Write, marker::PhantomData, str::FromStr, time::Duration}; @@ -535,13 +535,29 @@ where sender: impl Into>, ) -> Result<(N::TransactionRequest, Option)> { let fill = self.fill; - self._build(sender, fill).await + self._build(sender, fill, None).await + } + + /// Builds a transaction that will be signed by a Tempo access key. + /// + /// The access-key id is set before gas estimation. If the access key needs on-chain + /// provisioning, its authorization is embedded before access-list/gas estimation and before + /// any sponsor digest can be computed. + pub async fn build_with_access_key( + mut self, + sender: impl Into>, + access_key: &TempoAccessKeyConfig, + ) -> Result<(N::TransactionRequest, Option)> { + self.tx.set_key_id(access_key.key_address); + let fill = self.fill; + self._build(sender, fill, Some(access_key)).await } async fn _build( mut self, sender: impl Into>, fill: bool, + access_key: Option<&TempoAccessKeyConfig>, ) -> Result<(N::TransactionRequest, Option)> { // prepare let sender = sender.into(); @@ -555,6 +571,16 @@ where // resolve let tx_nonce = self.resolve_nonce(sender.address(), fill).await?; self.resolve_auth(&sender, tx_nonce).await?; + if let Some(access_key) = access_key { + self.tx + .prepare_access_key_authorization( + &self.provider, + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.as_ref(), + ) + .await?; + } self.resolve_access_list().await?; // fill diff --git a/crates/cast/tests/cli/keychain.rs b/crates/cast/tests/cli/keychain.rs new file mode 100644 index 0000000000000..88e9e16983cc5 --- /dev/null +++ b/crates/cast/tests/cli/keychain.rs @@ -0,0 +1,76 @@ +//! CLI tests for `cast keychain` subcommands. + +use anvil::NodeConfig; +use foundry_test_utils::util::OutputExt; + +/// Anvil test accounts (standard mnemonic). +mod accounts { + pub const PK1: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + pub const ADDR1: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + pub const ADDR2: &str = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"; + pub const TOKEN: &str = "0x20C000000000000000000000b9537d11c60E8b50"; // PathUSD +} + +// `cast keychain rl --json` must emit `{"remaining":""}`, not a bare string. +casttest!(keychain_rl_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "rl", + accounts::ADDR1, + accounts::ADDR2, + accounts::TOKEN, + "--rpc-url", + &rpc, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain rl --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + assert!( + parsed.get("remaining").is_some(), + "expected 'remaining' key in JSON output, got: {output}" + ); + // Must not be a bare string (old bug: `"0"`) + assert!(!parsed.is_string(), "JSON output must not be a bare string, got: {output}"); +}); + +// `cast keychain authorize --tempo.print-sponsor-hash --json` must emit +// `{"sponsor_hash":"0x..."}`, not a raw hex string. +casttest!(keychain_authorize_sponsor_hash_json_is_object, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test_tempo()).await; + let rpc = handle.http_endpoint(); + + let output = cmd + .args([ + "keychain", + "authorize", + accounts::ADDR2, // key to authorize + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + "--tempo.print-sponsor-hash", + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain authorize --tempo.print-sponsor-hash --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + let hash = parsed + .get("sponsor_hash") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("expected 'sponsor_hash' key in JSON output, got: {output}")); + assert!(hash.starts_with("0x"), "sponsor_hash should be 0x-prefixed, got: {hash}"); + assert_eq!(hash.len(), 66, "sponsor_hash should be 32-byte hex (66 chars), got: {hash}"); +}); diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index c26baaca638da..9071614e3a7a9 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,6 +1,7 @@ //! Contains various tests for checking cast commands use alloy_chains::NamedChain; +use alloy_eips::Decodable2718; use alloy_hardforks::EthereumHardfork; use alloy_network::{TransactionBuilder, TransactionResponse}; use alloy_primitives::{B256, Bytes, U256, address, b256, hex}; @@ -20,11 +21,13 @@ use foundry_test_utils::{ }; use serde_json::json; use std::{fs, path::Path, str::FromStr}; +use tempo_primitives::TempoTxEnvelope; #[macro_use] extern crate foundry_test_utils; mod erc20; +mod keychain; mod selectors; casttest!(print_short_version, |_prj, cmd| { @@ -2055,6 +2058,55 @@ casttest!(mktx_ethsign, async |_prj, cmd| { ]]); }); +// tests that `cast mktx --tempo.lane ` resolves the lane against a `tempo.lanes.toml` file at +// the project root, sets the corresponding `nonce_key` on the produced Tempo AA transaction. +casttest!(mktx_tempo_lane_resolves_nonce_key, |prj, cmd| { + // Write a shared lanes file at the project root. + let lanes_path = prj.root().join("tempo.lanes.toml"); + fs::write(&lanes_path, "deploy = 1\nops = 2\npayments = 42\n").unwrap(); + + let output = cmd + .current_dir(prj.root()) + .args([ + "mktx", + "--tempo.lane", + "payments", + "--private-key", + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--chain", + "1", + "--nonce", + "0", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + ]) + .assert_success() + .get_output() + .clone(); + + // The resolved-lane breadcrumb is printed to stderr so it doesn't pollute stdout + // (which carries the raw signed transaction). + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("lane: payments (nonce_key=42, nonce=0)"), + "expected lane breadcrumb on stderr, got: {stderr}", + ); + + // Decode the produced signed Tempo AA transaction and verify it carries the + // resolved 2D nonce key. + let stdout = String::from_utf8_lossy(&output.stdout); + let raw_hex = stdout.trim().trim_start_matches("0x"); + let raw = hex::decode(raw_hex).expect("decode hex output"); + let envelope = TempoTxEnvelope::decode_2718(&mut raw.as_slice()).expect("decode tempo tx"); + assert!(envelope.is_aa(), "expected Tempo AA transaction, got: {envelope:?}"); + assert_eq!(envelope.nonce_key(), Some(U256::from(42_u64))); +}); + // tests that the raw encoded transaction is returned casttest!(tx_raw, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); @@ -4052,6 +4104,7 @@ Warning: Contract code is empty }); // +#[cfg(feature = "optimism")] casttest!(tx_raw_opstack_deposit, |_prj, cmd| { cmd.args([ "tx", @@ -5048,6 +5101,7 @@ casttest!(cast_decode_tx_network_flag_short_and_long_equivalent, |_prj, cmd| { // Test that `--network optimism` and `-n optimism` produce identical output for decode-tx. // Uses a known OP-stack deposit transaction (same tx as tx_raw_opstack_deposit test). +#[cfg(feature = "optimism")] casttest!(cast_decode_tx_network_optimism_short_and_long_equivalent, |_prj, cmd| { let tx = "0x7ef90207a0cbde10ec697aff886f95d2514bab434e455620627b9bb8ba33baaaa4d537d62794d45955f4de64f1840e5686e64278da901e263031944200000000000000000000000000000000000007872386f26fc10000872386f26fc1000083096c4980b901a4d764ad0b0001000000000000000000000000000000000000000000000000000000065132000000000000000000000000fd0bf71f60660e2f608ed56e1659c450eb1131200000000000000000000000004200000000000000000000000000000000000010000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a41635f5fd000000000000000000000000ca11bde05977b3631167028862be2a173976ca110000000000000000000000005703b26fe5a7be820db1bf34c901a79da1a46ba4000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -5104,3 +5158,68 @@ casttest!(run_evm_version_updates_gas_params, |_prj, cmd| { "expected Spurious Dragon gas (177241), got: {sd_output}" ); }); + +// Tests for `cast vaddr` JSON output +casttest!(vaddr_create_json_output, |_prj, cmd| { + // Use a pre-computed salt that satisfies the 4-byte PoW requirement for this owner. + // Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 + // Owner: 0x1234567890123456789012345678901234567890 + let out = cmd + .args([ + "--json", + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + "--count", + "2", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let v: serde_json::Value = serde_json::from_str(out.trim()).expect("valid JSON"); + assert_eq!(v["salt"], "0x0000000000000000000000000000000000000000000000003ee0a78d00000000"); + assert_eq!( + v["registration_hash"], + "0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396" + ); + assert_eq!(v["master_id"], "0x2f51c0c4"); + let addrs = v["virtual_addresses"].as_array().expect("array"); + assert_eq!(addrs.len(), 2); + assert_eq!(addrs[0]["tag"], "0x000000000000"); + assert_eq!( + addrs[0]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000000" + ); + assert_eq!(addrs[1]["tag"], "0x000000000001"); + assert_eq!( + addrs[1]["address"].as_str().unwrap().to_lowercase(), + "0x2f51c0c4fdfdfdfdfdfdfdfdfdfd000000000001" + ); +}); + +casttest!(vaddr_create_plain_output, |_prj, cmd| { + cmd.args([ + "vaddr", + "create", + "--owner", + "0x1234567890123456789012345678901234567890", + "--salt", + "0x0000000000000000000000000000000000000000000000003ee0a78d00000000", + "--no-register", + ]) + .assert_success() + .stdout_eq(str![[r#" +Salt: 0x0000000000000000000000000000000000000000000000003ee0a78d00000000 +Registration hash: 0x000000002f51c0c4f66f3910f799c6b98e2123ef43a401a062eb8ee07498c396 +Master ID: 0x2f51c0c4 + +Virtual addresses: + tag=0x000000000000 [..] + +"#]]); +}); diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 659fec7f1a333..0eab12331be04 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -68,3 +68,13 @@ tracing.workspace = true walkdir.workspace = true proptest.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", + "forge-script-sequence/optimism", +] diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 7d6cb9e481502..01de77b9c95fd 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -864,7 +864,7 @@ "func": { "id": "assertApproxEqAbsDecimal_1", "description": "Compares two `uint256` values. Expects difference to be less than or equal to `maxDelta`.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertApproxEqAbsDecimal(uint256 left, uint256 right, uint256 maxDelta, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertApproxEqAbsDecimal(uint256 left, uint256 right, uint256 maxDelta, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqAbsDecimal(uint256,uint256,uint256,uint256,string)", @@ -904,7 +904,7 @@ "func": { "id": "assertApproxEqAbsDecimal_3", "description": "Compares two `int256` values. Expects difference to be less than or equal to `maxDelta`.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertApproxEqAbsDecimal(int256 left, int256 right, uint256 maxDelta, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertApproxEqAbsDecimal(int256 left, int256 right, uint256 maxDelta, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqAbsDecimal(int256,int256,uint256,uint256,string)", @@ -944,7 +944,7 @@ "func": { "id": "assertApproxEqAbs_1", "description": "Compares two `uint256` values. Expects difference to be less than or equal to `maxDelta`.\nIncludes error message into revert string on failure.", - "declaration": "function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata error) external pure;", + "declaration": "function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqAbs(uint256,uint256,uint256,string)", @@ -984,7 +984,7 @@ "func": { "id": "assertApproxEqAbs_3", "description": "Compares two `int256` values. Expects difference to be less than or equal to `maxDelta`.\nIncludes error message into revert string on failure.", - "declaration": "function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata error) external pure;", + "declaration": "function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqAbs(int256,int256,uint256,string)", @@ -1024,7 +1024,7 @@ "func": { "id": "assertApproxEqRelDecimal_1", "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqRelDecimal(uint256,uint256,uint256,uint256,string)", @@ -1064,7 +1064,7 @@ "func": { "id": "assertApproxEqRelDecimal_3", "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqRelDecimal(int256,int256,uint256,uint256,string)", @@ -1104,7 +1104,7 @@ "func": { "id": "assertApproxEqRel_1", "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nIncludes error message into revert string on failure.", - "declaration": "function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure;", + "declaration": "function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqRel(uint256,uint256,uint256,string)", @@ -1144,7 +1144,7 @@ "func": { "id": "assertApproxEqRel_3", "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nIncludes error message into revert string on failure.", - "declaration": "function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure;", + "declaration": "function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertApproxEqRel(int256,int256,uint256,string)", @@ -1184,7 +1184,7 @@ "func": { "id": "assertEqDecimal_1", "description": "Asserts that two `uint256` values are equal, formatting them with decimals in failure message.\nIncludes error message into revert string on failure.", - "declaration": "function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEqDecimal(uint256,uint256,uint256,string)", @@ -1224,7 +1224,7 @@ "func": { "id": "assertEqDecimal_3", "description": "Asserts that two `int256` values are equal, formatting them with decimals in failure message.\nIncludes error message into revert string on failure.", - "declaration": "function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEqDecimal(int256,int256,uint256,string)", @@ -1264,7 +1264,7 @@ "func": { "id": "assertEq_1", "description": "Asserts that two `bool` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bool left, bool right, string calldata error) external pure;", + "declaration": "function assertEq(bool left, bool right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bool,bool,string)", @@ -1304,7 +1304,7 @@ "func": { "id": "assertEq_11", "description": "Asserts that two `string` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(string calldata left, string calldata right, string calldata error) external pure;", + "declaration": "function assertEq(string calldata left, string calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(string,string,string)", @@ -1344,7 +1344,7 @@ "func": { "id": "assertEq_13", "description": "Asserts that two `bytes` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bytes calldata left, bytes calldata right, string calldata error) external pure;", + "declaration": "function assertEq(bytes calldata left, bytes calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bytes,bytes,string)", @@ -1384,7 +1384,7 @@ "func": { "id": "assertEq_15", "description": "Asserts that two arrays of `bool` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bool[],bool[],string)", @@ -1424,7 +1424,7 @@ "func": { "id": "assertEq_17", "description": "Asserts that two arrays of `uint256` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(uint256[],uint256[],string)", @@ -1464,7 +1464,7 @@ "func": { "id": "assertEq_19", "description": "Asserts that two arrays of `int256` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(int256[],int256[],string)", @@ -1524,7 +1524,7 @@ "func": { "id": "assertEq_21", "description": "Asserts that two arrays of `address` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(address[] calldata left, address[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(address[] calldata left, address[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(address[],address[],string)", @@ -1564,7 +1564,7 @@ "func": { "id": "assertEq_23", "description": "Asserts that two arrays of `bytes32` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bytes32[],bytes32[],string)", @@ -1604,7 +1604,7 @@ "func": { "id": "assertEq_25", "description": "Asserts that two arrays of `string` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(string[] calldata left, string[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(string[] calldata left, string[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(string[],string[],string)", @@ -1644,7 +1644,7 @@ "func": { "id": "assertEq_27", "description": "Asserts that two arrays of `bytes` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure;", + "declaration": "function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bytes[],bytes[],string)", @@ -1664,7 +1664,7 @@ "func": { "id": "assertEq_3", "description": "Asserts that two `uint256` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertEq(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(uint256,uint256,string)", @@ -1704,7 +1704,7 @@ "func": { "id": "assertEq_5", "description": "Asserts that two `int256` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertEq(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(int256,int256,string)", @@ -1744,7 +1744,7 @@ "func": { "id": "assertEq_7", "description": "Asserts that two `address` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(address left, address right, string calldata error) external pure;", + "declaration": "function assertEq(address left, address right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(address,address,string)", @@ -1784,7 +1784,7 @@ "func": { "id": "assertEq_9", "description": "Asserts that two `bytes32` values are equal and includes error message into revert string on failure.", - "declaration": "function assertEq(bytes32 left, bytes32 right, string calldata error) external pure;", + "declaration": "function assertEq(bytes32 left, bytes32 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertEq(bytes32,bytes32,string)", @@ -1824,7 +1824,7 @@ "func": { "id": "assertFalse_1", "description": "Asserts that the given condition is false and includes error message into revert string on failure.", - "declaration": "function assertFalse(bool condition, string calldata error) external pure;", + "declaration": "function assertFalse(bool condition, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertFalse(bool,string)", @@ -1864,7 +1864,7 @@ "func": { "id": "assertGeDecimal_1", "description": "Compares two `uint256` values. Expects first value to be greater than or equal to second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGeDecimal(uint256,uint256,uint256,string)", @@ -1904,7 +1904,7 @@ "func": { "id": "assertGeDecimal_3", "description": "Compares two `int256` values. Expects first value to be greater than or equal to second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGeDecimal(int256,int256,uint256,string)", @@ -1944,7 +1944,7 @@ "func": { "id": "assertGe_1", "description": "Compares two `uint256` values. Expects first value to be greater than or equal to second.\nIncludes error message into revert string on failure.", - "declaration": "function assertGe(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertGe(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGe(uint256,uint256,string)", @@ -1984,7 +1984,7 @@ "func": { "id": "assertGe_3", "description": "Compares two `int256` values. Expects first value to be greater than or equal to second.\nIncludes error message into revert string on failure.", - "declaration": "function assertGe(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertGe(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGe(int256,int256,string)", @@ -2024,7 +2024,7 @@ "func": { "id": "assertGtDecimal_1", "description": "Compares two `uint256` values. Expects first value to be greater than second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGtDecimal(uint256,uint256,uint256,string)", @@ -2064,7 +2064,7 @@ "func": { "id": "assertGtDecimal_3", "description": "Compares two `int256` values. Expects first value to be greater than second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGtDecimal(int256,int256,uint256,string)", @@ -2104,7 +2104,7 @@ "func": { "id": "assertGt_1", "description": "Compares two `uint256` values. Expects first value to be greater than second.\nIncludes error message into revert string on failure.", - "declaration": "function assertGt(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertGt(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGt(uint256,uint256,string)", @@ -2144,7 +2144,7 @@ "func": { "id": "assertGt_3", "description": "Compares two `int256` values. Expects first value to be greater than second.\nIncludes error message into revert string on failure.", - "declaration": "function assertGt(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertGt(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertGt(int256,int256,string)", @@ -2184,7 +2184,7 @@ "func": { "id": "assertLeDecimal_1", "description": "Compares two `uint256` values. Expects first value to be less than or equal to second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLeDecimal(uint256,uint256,uint256,string)", @@ -2224,7 +2224,7 @@ "func": { "id": "assertLeDecimal_3", "description": "Compares two `int256` values. Expects first value to be less than or equal to second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLeDecimal(int256,int256,uint256,string)", @@ -2264,7 +2264,7 @@ "func": { "id": "assertLe_1", "description": "Compares two `uint256` values. Expects first value to be less than or equal to second.\nIncludes error message into revert string on failure.", - "declaration": "function assertLe(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertLe(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLe(uint256,uint256,string)", @@ -2304,7 +2304,7 @@ "func": { "id": "assertLe_3", "description": "Compares two `int256` values. Expects first value to be less than or equal to second.\nIncludes error message into revert string on failure.", - "declaration": "function assertLe(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertLe(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLe(int256,int256,string)", @@ -2344,7 +2344,7 @@ "func": { "id": "assertLtDecimal_1", "description": "Compares two `uint256` values. Expects first value to be less than second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLtDecimal(uint256,uint256,uint256,string)", @@ -2384,7 +2384,7 @@ "func": { "id": "assertLtDecimal_3", "description": "Compares two `int256` values. Expects first value to be less than second.\nFormats values with decimals in failure message. Includes error message into revert string on failure.", - "declaration": "function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLtDecimal(int256,int256,uint256,string)", @@ -2424,7 +2424,7 @@ "func": { "id": "assertLt_1", "description": "Compares two `uint256` values. Expects first value to be less than second.\nIncludes error message into revert string on failure.", - "declaration": "function assertLt(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertLt(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLt(uint256,uint256,string)", @@ -2464,7 +2464,7 @@ "func": { "id": "assertLt_3", "description": "Compares two `int256` values. Expects first value to be less than second.\nIncludes error message into revert string on failure.", - "declaration": "function assertLt(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertLt(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertLt(int256,int256,string)", @@ -2504,7 +2504,7 @@ "func": { "id": "assertNotEqDecimal_1", "description": "Asserts that two `uint256` values are not equal, formatting them with decimals in failure message.\nIncludes error message into revert string on failure.", - "declaration": "function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEqDecimal(uint256,uint256,uint256,string)", @@ -2544,7 +2544,7 @@ "func": { "id": "assertNotEqDecimal_3", "description": "Asserts that two `int256` values are not equal, formatting them with decimals in failure message.\nIncludes error message into revert string on failure.", - "declaration": "function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure;", + "declaration": "function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEqDecimal(int256,int256,uint256,string)", @@ -2584,7 +2584,7 @@ "func": { "id": "assertNotEq_1", "description": "Asserts that two `bool` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bool left, bool right, string calldata error) external pure;", + "declaration": "function assertNotEq(bool left, bool right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bool,bool,string)", @@ -2624,7 +2624,7 @@ "func": { "id": "assertNotEq_11", "description": "Asserts that two `string` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(string calldata left, string calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(string calldata left, string calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(string,string,string)", @@ -2664,7 +2664,7 @@ "func": { "id": "assertNotEq_13", "description": "Asserts that two `bytes` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bytes calldata left, bytes calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(bytes calldata left, bytes calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bytes,bytes,string)", @@ -2704,7 +2704,7 @@ "func": { "id": "assertNotEq_15", "description": "Asserts that two arrays of `bool` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bool[],bool[],string)", @@ -2744,7 +2744,7 @@ "func": { "id": "assertNotEq_17", "description": "Asserts that two arrays of `uint256` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(uint256[],uint256[],string)", @@ -2784,7 +2784,7 @@ "func": { "id": "assertNotEq_19", "description": "Asserts that two arrays of `int256` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(int256[],int256[],string)", @@ -2844,7 +2844,7 @@ "func": { "id": "assertNotEq_21", "description": "Asserts that two arrays of `address` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(address[] calldata left, address[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(address[] calldata left, address[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(address[],address[],string)", @@ -2884,7 +2884,7 @@ "func": { "id": "assertNotEq_23", "description": "Asserts that two arrays of `bytes32` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bytes32[],bytes32[],string)", @@ -2924,7 +2924,7 @@ "func": { "id": "assertNotEq_25", "description": "Asserts that two arrays of `string` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(string[] calldata left, string[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(string[] calldata left, string[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(string[],string[],string)", @@ -2964,7 +2964,7 @@ "func": { "id": "assertNotEq_27", "description": "Asserts that two arrays of `bytes` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure;", + "declaration": "function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bytes[],bytes[],string)", @@ -2984,7 +2984,7 @@ "func": { "id": "assertNotEq_3", "description": "Asserts that two `uint256` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(uint256 left, uint256 right, string calldata error) external pure;", + "declaration": "function assertNotEq(uint256 left, uint256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(uint256,uint256,string)", @@ -3024,7 +3024,7 @@ "func": { "id": "assertNotEq_5", "description": "Asserts that two `int256` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(int256 left, int256 right, string calldata error) external pure;", + "declaration": "function assertNotEq(int256 left, int256 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(int256,int256,string)", @@ -3064,7 +3064,7 @@ "func": { "id": "assertNotEq_7", "description": "Asserts that two `address` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(address left, address right, string calldata error) external pure;", + "declaration": "function assertNotEq(address left, address right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(address,address,string)", @@ -3104,7 +3104,7 @@ "func": { "id": "assertNotEq_9", "description": "Asserts that two `bytes32` values are not equal and includes error message into revert string on failure.", - "declaration": "function assertNotEq(bytes32 left, bytes32 right, string calldata error) external pure;", + "declaration": "function assertNotEq(bytes32 left, bytes32 right, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertNotEq(bytes32,bytes32,string)", @@ -3144,7 +3144,7 @@ "func": { "id": "assertTrue_1", "description": "Asserts that the given condition is true and includes error message into revert string on failure.", - "declaration": "function assertTrue(bool condition, string calldata error) external pure;", + "declaration": "function assertTrue(bool condition, string calldata err) external pure;", "visibility": "external", "mutability": "pure", "signature": "assertTrue(bool,string)", @@ -5447,7 +5447,7 @@ { "func": { "id": "expectEmit_0", - "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).", + "description": "Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external;", "visibility": "external", "mutability": "", @@ -5487,7 +5487,7 @@ { "func": { "id": "expectEmit_2", - "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.", + "description": "Prepare an expected log with all topic and data checks enabled.\nCall this function, then emit an event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.\nMust be placed immediately before the call you want to assert on. If the next call reverts and the\nrevert is caught by the caller (low-level call or try/catch), the expectation remains active and may\nbe satisfied by a log emitted from a later call.", "declaration": "function expectEmit() external;", "visibility": "external", "mutability": "", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 65f181f6b0e35..12cfd19017770 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1082,6 +1082,9 @@ interface Vm { /// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.). /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data (as specified by the booleans). + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; @@ -1093,6 +1096,9 @@ interface Vm { /// Prepare an expected log with all topic and data checks enabled. /// Call this function, then emit an event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data. + /// Must be placed immediately before the call you want to assert on. If the next call reverts and the + /// revert is caught by the caller (low-level call or try/catch), the expectation remains active and may + /// be satisfied by a log emitted from a later call. #[cheatcode(group = Testing, safety = Unsafe)] function expectEmit() external; @@ -1243,7 +1249,7 @@ interface Vm { /// Asserts that the given condition is true and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertTrue(bool condition, string calldata error) external pure; + function assertTrue(bool condition, string calldata err) external pure; /// Asserts that the given condition is false. #[cheatcode(group = Testing, safety = Safe)] @@ -1251,7 +1257,7 @@ interface Vm { /// Asserts that the given condition is false and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertFalse(bool condition, string calldata error) external pure; + function assertFalse(bool condition, string calldata err) external pure; /// Asserts that two `bool` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1259,7 +1265,7 @@ interface Vm { /// Asserts that two `bool` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bool left, bool right, string calldata error) external pure; + function assertEq(bool left, bool right, string calldata err) external pure; /// Asserts that two `uint256` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1267,7 +1273,7 @@ interface Vm { /// Asserts that two `uint256` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(uint256 left, uint256 right, string calldata error) external pure; + function assertEq(uint256 left, uint256 right, string calldata err) external pure; /// Asserts that two `int256` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1275,7 +1281,7 @@ interface Vm { /// Asserts that two `int256` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(int256 left, int256 right, string calldata error) external pure; + function assertEq(int256 left, int256 right, string calldata err) external pure; /// Asserts that two `address` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1283,7 +1289,7 @@ interface Vm { /// Asserts that two `address` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(address left, address right, string calldata error) external pure; + function assertEq(address left, address right, string calldata err) external pure; /// Asserts that two `bytes32` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1291,7 +1297,7 @@ interface Vm { /// Asserts that two `bytes32` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bytes32 left, bytes32 right, string calldata error) external pure; + function assertEq(bytes32 left, bytes32 right, string calldata err) external pure; /// Asserts that two `string` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1299,7 +1305,7 @@ interface Vm { /// Asserts that two `string` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(string calldata left, string calldata right, string calldata error) external pure; + function assertEq(string calldata left, string calldata right, string calldata err) external pure; /// Asserts that two `bytes` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1307,7 +1313,7 @@ interface Vm { /// Asserts that two `bytes` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bytes calldata left, bytes calldata right, string calldata error) external pure; + function assertEq(bytes calldata left, bytes calldata right, string calldata err) external pure; /// Asserts that two arrays of `bool` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1315,7 +1321,7 @@ interface Vm { /// Asserts that two arrays of `bool` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure; + function assertEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `uint256 values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1323,7 +1329,7 @@ interface Vm { /// Asserts that two arrays of `uint256` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure; + function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `int256` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1331,7 +1337,7 @@ interface Vm { /// Asserts that two arrays of `int256` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure; + function assertEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `address` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1339,7 +1345,7 @@ interface Vm { /// Asserts that two arrays of `address` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(address[] calldata left, address[] calldata right, string calldata error) external pure; + function assertEq(address[] calldata left, address[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `bytes32` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1347,7 +1353,7 @@ interface Vm { /// Asserts that two arrays of `bytes32` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure; + function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `string` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1355,7 +1361,7 @@ interface Vm { /// Asserts that two arrays of `string` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(string[] calldata left, string[] calldata right, string calldata error) external pure; + function assertEq(string[] calldata left, string[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `bytes` values are equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1363,7 +1369,7 @@ interface Vm { /// Asserts that two arrays of `bytes` values are equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure; + function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure; /// Asserts that two `uint256` values are equal, formatting them with decimals in failure message. #[cheatcode(group = Testing, safety = Safe)] @@ -1372,7 +1378,7 @@ interface Vm { /// Asserts that two `uint256` values are equal, formatting them with decimals in failure message. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Asserts that two `int256` values are equal, formatting them with decimals in failure message. #[cheatcode(group = Testing, safety = Safe)] @@ -1381,7 +1387,7 @@ interface Vm { /// Asserts that two `int256` values are equal, formatting them with decimals in failure message. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Asserts that two `bool` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1389,7 +1395,7 @@ interface Vm { /// Asserts that two `bool` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bool left, bool right, string calldata error) external pure; + function assertNotEq(bool left, bool right, string calldata err) external pure; /// Asserts that two `uint256` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1397,7 +1403,7 @@ interface Vm { /// Asserts that two `uint256` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(uint256 left, uint256 right, string calldata error) external pure; + function assertNotEq(uint256 left, uint256 right, string calldata err) external pure; /// Asserts that two `int256` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1405,7 +1411,7 @@ interface Vm { /// Asserts that two `int256` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(int256 left, int256 right, string calldata error) external pure; + function assertNotEq(int256 left, int256 right, string calldata err) external pure; /// Asserts that two `address` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1413,7 +1419,7 @@ interface Vm { /// Asserts that two `address` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(address left, address right, string calldata error) external pure; + function assertNotEq(address left, address right, string calldata err) external pure; /// Asserts that two `bytes32` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1421,7 +1427,7 @@ interface Vm { /// Asserts that two `bytes32` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bytes32 left, bytes32 right, string calldata error) external pure; + function assertNotEq(bytes32 left, bytes32 right, string calldata err) external pure; /// Asserts that two `string` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1429,7 +1435,7 @@ interface Vm { /// Asserts that two `string` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(string calldata left, string calldata right, string calldata error) external pure; + function assertNotEq(string calldata left, string calldata right, string calldata err) external pure; /// Asserts that two `bytes` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1437,7 +1443,7 @@ interface Vm { /// Asserts that two `bytes` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bytes calldata left, bytes calldata right, string calldata error) external pure; + function assertNotEq(bytes calldata left, bytes calldata right, string calldata err) external pure; /// Asserts that two arrays of `bool` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1445,7 +1451,7 @@ interface Vm { /// Asserts that two arrays of `bool` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure; + function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `uint256` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1453,7 +1459,7 @@ interface Vm { /// Asserts that two arrays of `uint256` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure; + function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `int256` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1461,7 +1467,7 @@ interface Vm { /// Asserts that two arrays of `int256` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure; + function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `address` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1469,7 +1475,7 @@ interface Vm { /// Asserts that two arrays of `address` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(address[] calldata left, address[] calldata right, string calldata error) external pure; + function assertNotEq(address[] calldata left, address[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `bytes32` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1477,7 +1483,7 @@ interface Vm { /// Asserts that two arrays of `bytes32` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure; + function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `string` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1485,7 +1491,7 @@ interface Vm { /// Asserts that two arrays of `string` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(string[] calldata left, string[] calldata right, string calldata error) external pure; + function assertNotEq(string[] calldata left, string[] calldata right, string calldata err) external pure; /// Asserts that two arrays of `bytes` values are not equal. #[cheatcode(group = Testing, safety = Safe)] @@ -1493,7 +1499,7 @@ interface Vm { /// Asserts that two arrays of `bytes` values are not equal and includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure; + function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure; /// Asserts that two `uint256` values are not equal, formatting them with decimals in failure message. #[cheatcode(group = Testing, safety = Safe)] @@ -1502,7 +1508,7 @@ interface Vm { /// Asserts that two `uint256` values are not equal, formatting them with decimals in failure message. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Asserts that two `int256` values are not equal, formatting them with decimals in failure message. #[cheatcode(group = Testing, safety = Safe)] @@ -1511,7 +1517,7 @@ interface Vm { /// Asserts that two `int256` values are not equal, formatting them with decimals in failure message. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be greater than second. #[cheatcode(group = Testing, safety = Safe)] @@ -1520,7 +1526,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be greater than second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGt(uint256 left, uint256 right, string calldata error) external pure; + function assertGt(uint256 left, uint256 right, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be greater than second. #[cheatcode(group = Testing, safety = Safe)] @@ -1529,7 +1535,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be greater than second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGt(int256 left, int256 right, string calldata error) external pure; + function assertGt(int256 left, int256 right, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be greater than second. /// Formats values with decimals in failure message. @@ -1539,7 +1545,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be greater than second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be greater than second. /// Formats values with decimals in failure message. @@ -1549,7 +1555,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be greater than second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be greater than or equal to second. #[cheatcode(group = Testing, safety = Safe)] @@ -1558,7 +1564,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be greater than or equal to second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGe(uint256 left, uint256 right, string calldata error) external pure; + function assertGe(uint256 left, uint256 right, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be greater than or equal to second. #[cheatcode(group = Testing, safety = Safe)] @@ -1567,7 +1573,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be greater than or equal to second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGe(int256 left, int256 right, string calldata error) external pure; + function assertGe(int256 left, int256 right, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be greater than or equal to second. /// Formats values with decimals in failure message. @@ -1577,7 +1583,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be greater than or equal to second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be greater than or equal to second. /// Formats values with decimals in failure message. @@ -1587,7 +1593,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be greater than or equal to second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be less than second. #[cheatcode(group = Testing, safety = Safe)] @@ -1596,7 +1602,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be less than second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLt(uint256 left, uint256 right, string calldata error) external pure; + function assertLt(uint256 left, uint256 right, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be less than second. #[cheatcode(group = Testing, safety = Safe)] @@ -1605,7 +1611,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be less than second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLt(int256 left, int256 right, string calldata error) external pure; + function assertLt(int256 left, int256 right, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be less than second. /// Formats values with decimals in failure message. @@ -1615,7 +1621,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be less than second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be less than second. /// Formats values with decimals in failure message. @@ -1625,7 +1631,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be less than second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be less than or equal to second. #[cheatcode(group = Testing, safety = Safe)] @@ -1634,7 +1640,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be less than or equal to second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLe(uint256 left, uint256 right, string calldata error) external pure; + function assertLe(uint256 left, uint256 right, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be less than or equal to second. #[cheatcode(group = Testing, safety = Safe)] @@ -1643,7 +1649,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be less than or equal to second. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLe(int256 left, int256 right, string calldata error) external pure; + function assertLe(int256 left, int256 right, string calldata err) external pure; /// Compares two `uint256` values. Expects first value to be less than or equal to second. /// Formats values with decimals in failure message. @@ -1653,7 +1659,7 @@ interface Vm { /// Compares two `uint256` values. Expects first value to be less than or equal to second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; /// Compares two `int256` values. Expects first value to be less than or equal to second. /// Formats values with decimals in failure message. @@ -1663,7 +1669,7 @@ interface Vm { /// Compares two `int256` values. Expects first value to be less than or equal to second. /// Formats values with decimals in failure message. Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; /// Compares two `uint256` values. Expects difference to be less than or equal to `maxDelta`. #[cheatcode(group = Testing, safety = Safe)] @@ -1672,7 +1678,7 @@ interface Vm { /// Compares two `uint256` values. Expects difference to be less than or equal to `maxDelta`. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata error) external pure; + function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata err) external pure; /// Compares two `int256` values. Expects difference to be less than or equal to `maxDelta`. #[cheatcode(group = Testing, safety = Safe)] @@ -1681,7 +1687,7 @@ interface Vm { /// Compares two `int256` values. Expects difference to be less than or equal to `maxDelta`. /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata error) external pure; + function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata err) external pure; /// Compares two `uint256` values. Expects difference to be less than or equal to `maxDelta`. /// Formats values with decimals in failure message. @@ -1696,7 +1702,7 @@ interface Vm { uint256 right, uint256 maxDelta, uint256 decimals, - string calldata error + string calldata err ) external pure; /// Compares two `int256` values. Expects difference to be less than or equal to `maxDelta`. @@ -1712,7 +1718,7 @@ interface Vm { int256 right, uint256 maxDelta, uint256 decimals, - string calldata error + string calldata err ) external pure; /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. @@ -1724,7 +1730,7 @@ interface Vm { /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure; + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata err) external pure; /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% @@ -1735,7 +1741,7 @@ interface Vm { /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% /// Includes error message into revert string on failure. #[cheatcode(group = Testing, safety = Safe)] - function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata err) external pure; /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% @@ -1757,7 +1763,7 @@ interface Vm { uint256 right, uint256 maxPercentDelta, uint256 decimals, - string calldata error + string calldata err ) external pure; /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. @@ -1780,7 +1786,7 @@ interface Vm { int256 right, uint256 maxPercentDelta, uint256 decimals, - string calldata error + string calldata err ) external pure; /// Returns true if the current Foundry version is greater than or equal to the given version. diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index b22f76714dd0f..27545c1b6cd33 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -856,42 +856,6 @@ impl Cheatcodes { } } - // Handle mocked calls - if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { - let ctx = MockCallDataContext { - calldata: call.input.bytes(ecx), - value: call.transfer_value(), - }; - - if let Some(return_data_queue) = match mocks.get_mut(&ctx) { - Some(queue) => Some(queue), - None => mocks - .iter_mut() - .find(|(mock, _)| { - call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) - && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) - }) - .map(|(_, v)| v), - } && let Some(return_data) = if return_data_queue.len() == 1 { - // If the mocked calls stack has a single element in it, don't empty it - return_data_queue.front().map(|x| x.to_owned()) - } else { - // Else, we pop the front element - return_data_queue.pop_front() - } { - return Some(CallOutcome { - result: InterpreterResult { - result: return_data.ret_type, - output: return_data.data, - gas, - }, - memory_offset: call.return_memory_offset.clone(), - was_precompile_called: true, - precompile_call_logs: vec![], - }); - } - } - // Apply our prank if let Some(prank) = &self.get_prank(curr_depth) { // Apply delegate call, `call.caller`` will not equal `prank.prank_caller` @@ -932,6 +896,72 @@ impl Cheatcodes { } } + // Handle mocked calls + if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { + let ctx = MockCallDataContext { + calldata: call.input.bytes(ecx), + value: call.transfer_value(), + }; + + if let Some(return_data_queue) = match mocks.get_mut(&ctx) { + Some(queue) => Some(queue), + None => mocks + .iter_mut() + .find(|(mock, _)| { + call.input.bytes(ecx).get(..mock.calldata.len()) == Some(&mock.calldata[..]) + && mock.value.is_none_or(|value| Some(value) == call.transfer_value()) + }) + .map(|(_, v)| v), + } && let Some(return_data) = return_data_queue.front().map(|x| x.to_owned()) + { + if let Some(value) = call.transfer_value() { + let checkpoint = ecx.journal_mut().checkpoint(); + match ecx.journal_mut().transfer_loaded( + call.transfer_from(), + call.transfer_to(), + value, + ) { + None => { + if return_data.ret_type.is_ok() { + ecx.journal_mut().checkpoint_commit(); + } else { + ecx.journal_mut().checkpoint_revert(checkpoint); + } + } + Some(err) => { + ecx.journal_mut().checkpoint_revert(checkpoint); + return Some(CallOutcome { + result: InterpreterResult { + result: err.into(), + output: Bytes::new(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: false, + precompile_call_logs: vec![], + }); + } + } + } + + // If the mocked calls stack has a single element in it, don't empty it + if return_data_queue.len() > 1 { + return_data_queue.pop_front(); + } + + return Some(CallOutcome { + result: InterpreterResult { + result: return_data.ret_type, + output: return_data.data, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + was_precompile_called: true, + precompile_call_logs: vec![], + }); + } + } + // Apply EIP-2930 access list self.apply_accesslist(ecx); @@ -1497,6 +1527,21 @@ impl Inspector> for Cheatcode } } + // this will ensure we don't have false positives when trying to diagnose reverts in fork + // mode + let diag = self.fork_revert_diagnostic.take(); + + // If the call already reverted, preserve that primary failure and skip post-call + // expect* validation so it cannot overwrite the original revert. + if outcome.result.is_revert() { + // if there's a revert and a previous call was diagnosed as fork related revert then we + // can return a better error here + if let Some(err) = diag { + outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); + } + return; + } + // At the end of the call, // we need to check if we've found all the emits. // We know we've found all the expected emits in the right order @@ -1574,19 +1619,6 @@ impl Inspector> for Cheatcode self.expected_emits.clear() } - // this will ensure we don't have false positives when trying to diagnose reverts in fork - // mode - let diag = self.fork_revert_diagnostic.take(); - - // if there's a revert and a previous call was diagnosed as fork related revert then we can - // return a better error here - if outcome.result.is_revert() - && let Some(err) = diag - { - outcome.result.output = Error::encode(err.to_error_msg(&self.labels)); - return; - } - // try to diagnose reverts in multi-fork mode where a call is made to an address that does // not exist if let TxKind::Call(test_contract) = ecx.tx().kind() { @@ -1867,10 +1899,23 @@ impl Inspector> for Cheatcode } // Handle expected reverts - if let Some(expected_revert) = &self.expected_revert + if let Some(expected_revert) = &mut self.expected_revert && curr_depth <= expected_revert.depth && matches!(expected_revert.kind, ExpectedRevertKind::Default) { + // Mirror the logic in `call_end`: when an expected reverter address is set + // and we don't yet have one (or we're matching multiple reverts), record the + // would-be deployed address as the reverter. revm guarantees `outcome.address` + // is `Some(_)` whenever the constructor actually ran (including the revert + // case); it is only `None` for pre-frame rejection (depth/balance/nonce), + // for which a reverter address is meaningless. + if outcome.result.is_revert() + && expected_revert.reverter.is_some() + && (expected_revert.reverted_by.is_none() || expected_revert.count > 1) + && let Some(addr) = outcome.address + { + expected_revert.reverted_by = Some(addr); + } let mut expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); return match revert_handlers::handle_expect_revert( false, diff --git a/crates/cheatcodes/src/test/assert.rs b/crates/cheatcodes/src/test/assert.rs index 632ba8e04245b..12d625768a0c9 100644 --- a/crates/cheatcodes/src/test/assert.rs +++ b/crates/cheatcodes/src/test/assert.rs @@ -164,7 +164,7 @@ impl EqRelAssertionError { format_units_uint(&f.left, decimals), format_units_uint(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } @@ -179,7 +179,7 @@ impl EqRelAssertionError { format_units_int(&f.left, decimals), format_units_int(&f.right, decimals), format_delta_percent(&f.max_delta), - &f.real_delta, + f.real_delta, ), Self::Overflow => self.to_string(), } @@ -222,9 +222,9 @@ fn handle_assertion_result_mono( /// Implements [crate::Cheatcode] for pairs of cheatcodes. /// /// Accepts a list of pairs of cheatcodes, where the first cheatcode is the one that doesn't contain -/// a custom error message, and the second one contains it at `error` field. +/// a custom error message, and the second one contains it at `err` field. /// -/// Passed `args` are the common arguments for both cheatcode structs (excluding `error` field). +/// Passed `args` are the common arguments for both cheatcode structs (excluding `err` field). /// /// Macro also accepts an optional closure that formats the error returned by the assertion. macro_rules! impl_assertions { @@ -267,10 +267,12 @@ macro_rules! impl_assertions { ccx: &mut CheatsCtxt<'_, '_, FEN>, executor: &mut dyn CheatcodesExecutor, ) -> Result { - let Self { $($arg,)* error } = self; + let Self { $($arg,)* err } = self; match $body { Ok(()) => Ok(Default::default()), - Err(err) => handle_assertion_result(ccx, executor, err, $error_formatter, Some(error)) + Err(assertion_err) => { + handle_assertion_result(ccx, executor, assertion_err, $error_formatter, Some(err)) + } } } } diff --git a/crates/cheatcodes/src/version.rs b/crates/cheatcodes/src/version.rs index fb722c2814baa..2b8f81518a621 100644 --- a/crates/cheatcodes/src/version.rs +++ b/crates/cheatcodes/src/version.rs @@ -20,7 +20,14 @@ impl Cheatcode for foundryVersionAtLeastCall { } fn foundry_version_cmp(version: &str) -> Result { - version_cmp(SEMVER_VERSION.split('-').next().unwrap(), version) + version_cmp(strip_semver_metadata(SEMVER_VERSION), version) +} + +/// Strips pre-release (e.g. `-nightly`, `-dev`) and build metadata +/// (e.g. `+..`) from a version string +/// so we compare on `MAJOR.MINOR.PATCH` only. +fn strip_semver_metadata(version: &str) -> &str { + version.split(['-', '+']).next().unwrap() } fn version_cmp(version_a: &str, version_b: &str) -> Result { @@ -42,3 +49,61 @@ fn parse_version(version: &str) -> Result { } Ok(version) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_build_metadata_only() { + // Tagged release: `1.7.1+..` + assert_eq!(strip_semver_metadata("1.7.1+abc1234567.1737036656.release"), "1.7.1"); + } + + #[test] + fn strips_pre_release_and_build_metadata() { + // Nightly: `1.7.1-nightly+..` + assert_eq!(strip_semver_metadata("1.7.1-nightly+abc1234567.1737036656.release"), "1.7.1"); + // Dev: `1.7.1-dev+..` + assert_eq!(strip_semver_metadata("1.7.1-dev+abc1234567.1737036656.debug"), "1.7.1"); + } + + #[test] + fn strips_plain_version() { + assert_eq!(strip_semver_metadata("1.7.1"), "1.7.1"); + } + + #[test] + fn version_cmp_orders_correctly() { + assert_eq!(version_cmp("1.7.1", "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp("1.7.1", "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "1.7.2").unwrap(), Ordering::Less); + assert_eq!(version_cmp("1.7.1", "0.0.1").unwrap(), Ordering::Greater); + assert_eq!(version_cmp("1.7.1", "99.0.0").unwrap(), Ordering::Less); + } + + #[test] + fn parse_version_rejects_pre_release_and_build_metadata() { + // User-supplied versions must be plain `MAJOR.MINOR.PATCH`. + assert!(parse_version("1.7.1-nightly").is_err()); + assert!(parse_version("1.7.1+abc").is_err()); + assert!(parse_version("not-a-version").is_err()); + assert!(parse_version("1.7.1").is_ok()); + } + + #[test] + fn cmp_works_against_full_semver_version_strings() { + // Simulate comparing each shape of `SEMVER_VERSION` against a user-supplied version. + for current in [ + "1.7.1+abc1234567.1737036656.release", + "1.7.1-nightly+abc1234567.1737036656.release", + "1.7.1-dev+abc1234567.1737036656.debug", + "1.7.1", + ] { + let stripped = strip_semver_metadata(current); + assert_eq!(version_cmp(stripped, "1.7.1").unwrap(), Ordering::Equal); + assert_eq!(version_cmp(stripped, "1.7.0").unwrap(), Ordering::Greater); + assert_eq!(version_cmp(stripped, "1.7.2").unwrap(), Ordering::Less); + } + } +} diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index e9a1936013272..bb673c9219e10 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -19,7 +19,6 @@ path = "bin/main.rs" [dependencies] # forge -forge-doc.workspace = true forge-fmt.workspace = true foundry-cli.workspace = true foundry-common.workspace = true @@ -50,7 +49,6 @@ itertools.workspace = true semver.workspace = true serde_json.workspace = true serde.workspace = true -solang-parser.workspace = true time = { version = "0.3", features = ["formatting"] } yansi.workspace = true tracing.workspace = true @@ -65,8 +63,13 @@ foundry-test-utils.workspace = true rexpect = "0.6" [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] tracy-allocator = ["foundry-cli/tracy-allocator"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-cli/optimism", +] diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index da2c7f4caff02..2ec057b8167c0 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -2,10 +2,7 @@ //! //! This module contains the execution logic for the [SessionSource]. -use crate::{ - prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}, - source::IntermediateOutput, -}; +use crate::prelude::{ChiselDispatcher, ChiselResult, ChiselRunner, SessionSource, SolidityHelper}; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_json_abi::EventParam; use alloy_primitives::{Address, B256, U256, hex}; @@ -15,7 +12,17 @@ use foundry_evm::{ backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder, inspectors::CheatsConfig, traces::TraceMode, }; -use solang_parser::pt; +use solar::{ + ast::{BinOpKind, ElementaryType, FunctionKind, LitKind, StateMutability, StrKind, UnOpKind}, + interface::Symbol, + sema::{ + hir::{ + ContractId, Event, Expr, ExprKind, Function, ItemId, Res, StmtKind, Type as HirType, + TypeKind, Visibility, + }, + ty::{Gcx, Ty, TyKind}, + }, +}; use std::ops::ControlFlow; use yansi::Paint; @@ -86,8 +93,10 @@ impl SessionSource { if let Some(err) = err { let output = source_without_inspector.build()?; - let formatted_event = - output.enter(|output| output.get_event(input).map(format_event_definition)); + let formatted_event = output.enter(|output| { + let gcx = output.gcx(); + output.get_event(input).map(|eid| format_event_definition(gcx, gcx.hir.event(eid))) + }); if let Some(formatted_event) = formatted_event { return Ok((ControlFlow::Break(()), Some(formatted_event?))); } @@ -122,30 +131,37 @@ impl SessionSource { // which was wrapped in `abi.encode`. let generated_output = source.build()?; - // If the expression is a variable declaration within the REPL contract, use its type; - // otherwise, attempt to infer the type. - let contract_expr = generated_output - .intermediate - .repl_contract_expressions - .get(input) - .or_else(|| source.infer_inner_expr_type()); + // Inside the compiler closure, infer the DynSolType of the inspected expression and + // determine whether the REPL should continue. + let res_ty = generated_output.enter(|out| -> Option<(bool, DynSolType)> { + let gcx = out.gcx(); - // If the current action is a function call, we get its return type - // otherwise it returns None - let function_call_return_type = - Type::get_function_return_type(contract_expr, &generated_output.intermediate); + // Try direct lookup of `input` as a named variable in the REPL contract. + if let Some(direct_ty) = lookup_named_variable_type(gcx, input) { + return Some((false, direct_ty)); + } - let (contract_expr, ty) = if let Some(function_call_return_type) = function_call_return_type - { - (function_call_return_type.0, function_call_return_type.1) - } else { - match contract_expr.and_then(|e| { - Type::ethabi(e, Some(&generated_output.intermediate)).map(|ty| (e, ty)) - }) { - Some(res) => res, - // this type was denied for inspection, continue - None => return Ok((ControlFlow::Continue(()), None)), + // Otherwise, find the appended `bytes memory inspectoor = abi.encode();` + // and pull out the first call argument. + let block = out.run_func_body(); + let last = block.last()?; + let StmtKind::DeclSingle(vid) = last.kind else { return None }; + let var = gcx.hir.variable(vid); + let init = var.initializer?; + let ExprKind::Call(_callee, args, _) = &init.kind else { return None }; + let inner_expr = args.exprs().next()?; + + // If the call is `func()` returning a single value, prefer the function return type. + if let Some(ty) = get_function_return_type(gcx, inner_expr) { + return Some((should_continue(inner_expr), ty)); } + + let ty = expr_to_dyn(gcx, inner_expr, true)?; + Some((should_continue(inner_expr), ty)) + }); + + let Some((cont, ty)) = res_ty else { + return Ok((ControlFlow::Continue(()), None)); }; // the file compiled correctly, thus the last stack item must be the memory offset of @@ -162,42 +178,10 @@ impl SessionSource { eyre::bail!("Failed to inspect last expression: could not retrieve data from memory") }; let token = ty.abi_decode(data).wrap_err("Could not decode inspected values")?; - let c = if should_continue(contract_expr) { - ControlFlow::Continue(()) - } else { - ControlFlow::Break(()) - }; + let c = if cont { ControlFlow::Continue(()) } else { ControlFlow::Break(()) }; Ok((c, Some(format_token(token)))) } - /// Gracefully attempts to extract the type of the expression within the `abi.encode(...)` - /// call inserted by the inspect function. - /// - /// ### Takes - /// - /// A reference to a [SessionSource] - /// - /// ### Returns - /// - /// Optionally, a [Type] - fn infer_inner_expr_type(&self) -> Option<&pt::Expression> { - let out = self.build().ok()?; - let run = out.run_func_body().ok()?.last(); - match run { - Some(pt::Statement::VariableDefinition( - _, - _, - Some(pt::Expression::FunctionCall(_, _, args)), - )) => { - // We can safely unwrap the first expression because this function - // will only be called on a session source that has just had an - // `inspectoor` variable appended to it. - Some(args.first().unwrap()) - } - _ => None, - } - } - async fn build_runner(&mut self, final_pc: usize) -> Result { let (evm_env, tx_env, fork_block) = self.config.evm_opts.env().await?; @@ -241,6 +225,51 @@ impl SessionSource { } } +/// Looks up `name` as a named variable in the REPL contract (state variables or run() locals) +/// and returns its type as a [`DynSolType`]. +/// +/// Only top-level statements of `run()` are scanned. Variables declared inside nested blocks +/// (`if`, `for`, `while`, `unchecked`, etc.) are not visible here; the caller falls back to +/// the `inspectoor`-based path for those cases. +fn lookup_named_variable_type(gcx: Gcx<'_>, name: &str) -> Option { + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + + // State variables. + for vid in repl.variables() { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + + // Locals declared in run(). + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + for stmt in body.stmts { + match stmt.kind { + StmtKind::DeclSingle(vid) => { + let var = hir.variable(vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())); + } + } + StmtKind::DeclMulti(vids, _) => { + for vid in vids.iter().flatten() { + let var = hir.variable(*vid); + if var.name.map(|n| n.as_str() == name).unwrap_or(false) { + return solar_ty_to_dyn(gcx, gcx.type_of_item((*vid).into())); + } + } + } + _ => {} + } + } + None +} + /// Formats a value into an inspection message // TODO: Verbosity option fn format_token(token: DynSolValue) -> String { @@ -343,49 +372,37 @@ fn format_token(token: DynSolValue) -> String { } } -/// Formats a [pt::EventDefinition] into an inspection message -/// -/// ### Takes -/// -/// An borrowed [pt::EventDefinition] -/// -/// ### Returns -/// -/// A formatted [pt::EventDefinition] for use in inspection output. +/// Formats an [`Event`] into an inspection message. // TODO: Verbosity option -fn format_event_definition(event_definition: &pt::EventDefinition) -> Result { - let event_name = event_definition.name.as_ref().expect("Event has a name").to_string(); - let inputs = event_definition - .fields +fn format_event_definition(gcx: Gcx<'_>, event: &Event<'_>) -> Result { + let event_name = event.name.as_str().to_string(); + let inputs = event + .parameters .iter() - .map(|param| { - let name = param - .name - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "".to_string()); - let kind = Type::from_expression(¶m.ty) - .and_then(Type::into_builtin) + .map(|&pid| { + let var = gcx.hir.variable(pid); + let name = + var.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "".into()); + let kind = solar_ty_to_dyn(gcx, gcx.type_of_item(pid.into())) .ok_or_else(|| eyre::eyre!("Invalid type in event {event_name}"))?; Ok(EventParam { name, ty: kind.to_string(), components: vec![], - indexed: param.indexed, + indexed: var.indexed, internal_type: None, }) }) .collect::>>()?; - let event = - alloy_json_abi::Event { name: event_name, inputs, anonymous: event_definition.anonymous }; + let event = alloy_json_abi::Event { name: event_name, inputs, anonymous: event.anonymous }; Ok(format!( "Type: {}\n├ Name: {}\n├ Signature: {:?}\n└ Selector: {:?}", "event".red(), SolidityHelper::new().highlight(&format!( "{}({})", - &event.name, - &event + event.name, + event .inputs .iter() .map(|param| format!( @@ -395,7 +412,7 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result>() @@ -411,844 +428,724 @@ fn format_event_definition(event_definition: &pt::EventDefinition) -> Result), - - /// (type, length) - FixedArray(Box, usize), +/// Converts an [`Expr`] directly to a [`DynSolType`] for ABI inspection. +/// +/// `lookup` controls whether user-defined type names are resolved via the HIR. +fn expr_to_dyn(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + match &expr.kind { + // Elementary type expression: `uint256`, `address`, etc. + ExprKind::Type(ty) => hir_ty_to_dyn(gcx, ty), + + // `type(T)`: only meaningful as the lhs of a member access. + ExprKind::TypeCall(_) => None, + + // Literals. + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Address(_) => Some(DynSolType::Address), + LitKind::Bool(_) => Some(DynSolType::Bool), + LitKind::Str(kind, _, _) => match kind { + StrKind::Hex => Some(DynSolType::Bytes), + StrKind::Str | StrKind::Unicode => Some(DynSolType::String), + }, + LitKind::Number(_) | LitKind::Rational(_) => Some(DynSolType::Uint(256)), + LitKind::Err(_) => None, + }, + + // Resolved identifier: `foo`. + ExprKind::Ident(reses) => { + let res = reses.first()?; + match *res { + Res::Item(ItemId::Variable(vid)) => { + solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) + } + Res::Item(ItemId::Struct(sid)) => { + // Struct reference used as a constructor produces a tuple of field types. + Some(DynSolType::Tuple( + gcx.struct_field_types(sid) + .iter() + .filter_map(|&t| solar_ty_to_dyn(gcx, t)) + .collect(), + )) + } + // Other items and builtins: handled by enclosing Call/Member expressions. + _ => None, + } + } - /// (type, index) - ArrayIndex(Box, Option), + // Index/access: `arr[i]`, `MyType[]`, `MyType[N]`. + ExprKind::Index(base, idx) => { + let base_ty = expr_to_dyn(gcx, base, lookup)?; + let num = + idx.and_then(|e| parse_number_literal(e)).and_then(|n| usize::try_from(n).ok()); + match &base.kind { + // Type-level indexing builds an array type expression. + ExprKind::Type(_) | ExprKind::TypeCall(_) => { + if let Some(n) = num { + Some(DynSolType::FixedArray(Box::new(base_ty), n)) + } else { + Some(DynSolType::Array(Box::new(base_ty))) + } + } + // Runtime indexing returns the element type. + _ => match base_ty { + DynSolType::Array(inner) | DynSolType::FixedArray(inner, _) => Some(*inner), + DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_) => { + Some(DynSolType::FixedBytes(1)) + } + other => Some(other), + }, + } + } - /// (types) - Tuple(Vec>), + // Slice: same type as the base. + ExprKind::Slice(base, _, _) => expr_to_dyn(gcx, base, lookup), - /// (name, params, returns) - Function(Box, Vec>, Vec>), + // Array literal `[a, b, c]`. + ExprKind::Array(values) => values + .first() + .and_then(|e| expr_to_dyn(gcx, e, lookup)) + .map(|ty| DynSolType::FixedArray(Box::new(ty), values.len())), - /// (lhs, rhs) - Access(Box, String), + // Tuple expression `(a, b, c)`. + ExprKind::Tuple(items) => Some(DynSolType::Tuple( + items.iter().filter_map(|opt| opt.and_then(|e| expr_to_dyn(gcx, e, lookup))).collect(), + )), - /// (types) - Custom(Vec), -} + // Member access `lhs.member`. + ExprKind::Member(_, _) => resolve_member(gcx, expr, lookup), -impl Type { - /// Convert a [pt::Expression] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Expression] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_expression(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::Type(_, ty) => Self::from_type(ty), - - pt::Expression::Variable(ident) => Some(Self::Custom(vec![ident.name.clone()])), - - // array - pt::Expression::ArraySubscript(_, expr, num) => { - // if num is Some then this is either an index operation (arr[]) - // or a FixedArray statement (new uint256[]) - Self::from_expression(expr).and_then(|ty| { - let boxed = Box::new(ty); - let num = num.as_deref().and_then(parse_number_literal).and_then(|n| { - usize::try_from(n).ok() - }); - match expr.as_ref() { - // statement - pt::Expression::Type(_, _) => { - if let Some(num) = num { - Some(Self::FixedArray(boxed, num)) - } else { - Some(Self::Array(boxed)) - } - } - // index - pt::Expression::Variable(_) => { - Some(Self::ArrayIndex(boxed, num)) - } - _ => None - } - }) - } - pt::Expression::ArrayLiteral(_, values) => { - values.first().and_then(Self::from_expression).map(|ty| { - Self::FixedArray(Box::new(ty), values.len()) - }) - } + // Function/constructor call. + ExprKind::Call(_, _, _) => resolve_call(gcx, expr, lookup), - // tuple - pt::Expression::List(_, params) => Some(Self::Tuple(map_parameters(params))), + // `new T`: produces a value of type T. + ExprKind::New(ty) => hir_ty_to_dyn(gcx, ty), - // . - pt::Expression::MemberAccess(_, lhs, rhs) => { - Self::from_expression(lhs).map(|lhs| { - Self::Access(Box::new(lhs), rhs.name.clone()) - }) - } + // `payable(addr)`. + ExprKind::Payable(_) => Some(DynSolType::Address), - // - pt::Expression::Parenthesis(_, inner) | // () - pt::Expression::New(_, inner) | // new - pt::Expression::UnaryPlus(_, inner) | // + - // ops - pt::Expression::BitwiseNot(_, inner) | // ~ - pt::Expression::ArraySlice(_, inner, _, _) | // [*start*:*end*] - // assign ops - pt::Expression::PreDecrement(_, inner) | // -- - pt::Expression::PostDecrement(_, inner) | // -- - pt::Expression::PreIncrement(_, inner) | // ++ - pt::Expression::PostIncrement(_, inner) | // ++ - pt::Expression::Assign(_, inner, _) | // = ... - pt::Expression::AssignAdd(_, inner, _) | // += ... - pt::Expression::AssignSubtract(_, inner, _) | // -= ... - pt::Expression::AssignMultiply(_, inner, _) | // *= ... - pt::Expression::AssignDivide(_, inner, _) | // /= ... - pt::Expression::AssignModulo(_, inner, _) | // %= ... - pt::Expression::AssignAnd(_, inner, _) | // &= ... - pt::Expression::AssignOr(_, inner, _) | // |= ... - pt::Expression::AssignXor(_, inner, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, inner, _) | // <<= ... - pt::Expression::AssignShiftRight(_, inner, _) // >>= ... - => Self::from_expression(inner), - - // *condition* ? : - pt::Expression::ConditionalOperator(_, _, if_true, if_false) => { - Self::from_expression(if_true).or_else(|| Self::from_expression(if_false)) - } + // Ternary: prefer truthy branch's type, fall back to else branch. + ExprKind::Ternary(_, t, e) => { + expr_to_dyn(gcx, t, lookup).or_else(|| expr_to_dyn(gcx, e, lookup)) + } - // address - pt::Expression::AddressLiteral(_, _) => Some(Self::Builtin(DynSolType::Address)), - pt::Expression::HexNumberLiteral(_, s, _) => { - match s.parse::
() { - Ok(addr) if *s == addr.to_checksum(None) => { - Some(Self::Builtin(DynSolType::Address)) + // Delete has no return type. + ExprKind::Delete(_) => None, + + // Unary operations. + ExprKind::Unary(op, inner) => match op.kind { + UnOpKind::Neg => expr_to_dyn(gcx, inner, lookup).map(|ty| match ty { + DynSolType::Uint(n) => DynSolType::Int(n), + DynSolType::Int(n) => DynSolType::Uint(n), + x => x, + }), + UnOpKind::Not => Some(DynSolType::Bool), + UnOpKind::BitNot + | UnOpKind::PreInc + | UnOpKind::PreDec + | UnOpKind::PostInc + | UnOpKind::PostDec => expr_to_dyn(gcx, inner, lookup), + }, + + // Binary operations. + ExprKind::Binary(lhs, op, rhs) => match op.kind { + BinOpKind::Lt + | BinOpKind::Le + | BinOpKind::Gt + | BinOpKind::Ge + | BinOpKind::Eq + | BinOpKind::Ne + | BinOpKind::And + | BinOpKind::Or => Some(DynSolType::Bool), + BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul | BinOpKind::Div => { + match (expr_to_dyn(gcx, lhs, false), expr_to_dyn(gcx, rhs, false)) { + (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) + | (Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { + Some(DynSolType::Int(256)) } - _ => Some(Self::Builtin(DynSolType::Uint(256))), + _ => Some(DynSolType::Uint(256)), } } + BinOpKind::Rem + | BinOpKind::Pow + | BinOpKind::BitAnd + | BinOpKind::BitOr + | BinOpKind::BitXor + | BinOpKind::Shl + | BinOpKind::Shr + | BinOpKind::Sar => Some(DynSolType::Uint(256)), + }, + + // Assignments: type of the lhs. + ExprKind::Assign(lhs, _, _) => expr_to_dyn(gcx, lhs, lookup), + + ExprKind::Err(_) => None, + } +} - // uint and int - // invert - pt::Expression::Negate(_, inner) => Self::from_expression(inner).map(Self::invert_int), - - // int if either operand is int - // TODO: will need an update for Solidity v0.8.18 user defined operators: - // https://github.com/ethereum/solidity/issues/13718#issuecomment-1341058649 - pt::Expression::Add(_, lhs, rhs) | - pt::Expression::Subtract(_, lhs, rhs) | - pt::Expression::Multiply(_, lhs, rhs) | - pt::Expression::Divide(_, lhs, rhs) => { - match (Self::ethabi(lhs, None), Self::ethabi(rhs, None)) { - (Some(DynSolType::Int(_) | DynSolType::Uint(_)), Some(DynSolType::Int(_))) | -(Some(DynSolType::Int(_)), Some(DynSolType::Uint(_))) => { - Some(Self::Builtin(DynSolType::Int(256))) - } - _ => { - Some(Self::Builtin(DynSolType::Uint(256))) - } +/// Converts a [`HirType`] to a [`DynSolType`]. +fn hir_ty_to_dyn(gcx: Gcx<'_>, ty: &HirType<'_>) -> Option { + match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + TypeKind::Array(arr) => { + let elem = hir_ty_to_dyn(gcx, &arr.element)?; + if let Some(size) = arr.size { + let n = parse_number_literal(size).and_then(|n| usize::try_from(n).ok()); + if let Some(n) = n { + Some(DynSolType::FixedArray(Box::new(elem), n)) + } else { + Some(DynSolType::Array(Box::new(elem))) } + } else { + Some(DynSolType::Array(Box::new(elem))) } - - // always assume uint - pt::Expression::Modulo(_, _, _) | - pt::Expression::Power(_, _, _) | - pt::Expression::BitwiseOr(_, _, _) | - pt::Expression::BitwiseAnd(_, _, _) | - pt::Expression::BitwiseXor(_, _, _) | - pt::Expression::ShiftRight(_, _, _) | - pt::Expression::ShiftLeft(_, _, _) | - pt::Expression::NumberLiteral(_, _, _, _) => Some(Self::Builtin(DynSolType::Uint(256))), - - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(_, _, _, _, _) => { - Some(Self::Builtin(DynSolType::Uint(256))) + } + TypeKind::Function(f) => match f.returns.len() { + 0 => None, + 1 => { + let var = gcx.hir.variable(f.returns[0]); + hir_ty_to_dyn(gcx, &var.ty) } + _ => Some(DynSolType::Tuple( + f.returns + .iter() + .filter_map(|&pid| hir_ty_to_dyn(gcx, &gcx.hir.variable(pid).ty)) + .collect(), + )), + }, + TypeKind::Mapping(m) => hir_ty_to_dyn(gcx, &m.value), + TypeKind::Custom(item) => solar_ty_to_dyn(gcx, gcx.type_of_item(*item)), + TypeKind::Err(_) => None, + } +} - // bool - pt::Expression::BoolLiteral(_, _) | - pt::Expression::And(_, _, _) | - pt::Expression::Or(_, _, _) | - pt::Expression::Equal(_, _, _) | - pt::Expression::NotEqual(_, _, _) | - pt::Expression::Less(_, _, _) | - pt::Expression::LessEqual(_, _, _) | - pt::Expression::More(_, _, _) | - pt::Expression::MoreEqual(_, _, _) | - pt::Expression::Not(_, _) => Some(Self::Builtin(DynSolType::Bool)), - - // string - pt::Expression::StringLiteral(_) => Some(Self::Builtin(DynSolType::String)), - - // bytes - pt::Expression::HexLiteral(_) => Some(Self::Builtin(DynSolType::Bytes)), - - // function - pt::Expression::FunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(Self::from_expression).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } - pt::Expression::NamedFunctionCall(_, name, args) => { - Self::from_expression(name).map(|name| { - let args = args.iter().map(|arg| Self::from_expression(&arg.expr)).collect(); - Self::Function(Box::new(name), args, vec![]) - }) - } +/// Resolves a member-access expression (`lhs.member`) to its [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Member`. +fn resolve_member(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Member(lhs, ident) = &expr.kind else { return None }; + let member = ident.name; + + // `type(T).member` — type introspection. + if let ExprKind::TypeCall(ty) = &lhs.kind { + return match member.as_str() { + "name" => Some(DynSolType::String), + "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), + "interfaceId" => Some(DynSolType::FixedBytes(4)), + // Only valid for integer types; custom types (enums) fall back to Uint(256). + "min" | "max" => match &ty.kind { + TypeKind::Elementary(et) => elementary_to_dyn(*et), + _ => Some(DynSolType::Uint(256)), + }, + _ => None, + }; + } - // explicitly None - pt::Expression::Delete(_, _) | pt::Expression::FunctionCallBlock(_, _, _) => None, - } + // Built-in namespace identifier: `block.timestamp`, `msg.sender`, `abi.encode`, etc. + if let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + && let Some(ty) = builtin_member(b.name().as_str(), member.as_str()) + { + return Some(ty); } - /// Convert a [pt::Type] to a [Type] - /// - /// ### Takes - /// - /// A reference to a [pt::Type] to convert. - /// - /// ### Returns - /// - /// Optionally, an owned [Type] - fn from_type(ty: &pt::Type) -> Option { - let ty = match ty { - pt::Type::Address | pt::Type::AddressPayable | pt::Type::Payable => { - Self::Builtin(DynSolType::Address) - } - pt::Type::Bool => Self::Builtin(DynSolType::Bool), - pt::Type::String => Self::Builtin(DynSolType::String), - pt::Type::Int(size) => Self::Builtin(DynSolType::Int(*size as usize)), - pt::Type::Uint(size) => Self::Builtin(DynSolType::Uint(*size as usize)), - pt::Type::Bytes(size) => Self::Builtin(DynSolType::FixedBytes(*size as usize)), - pt::Type::DynamicBytes => Self::Builtin(DynSolType::Bytes), - pt::Type::Mapping { value, .. } => Self::from_expression(value)?, - pt::Type::Function { params, returns, .. } => { - let params = map_parameters(params); - let returns = returns - .as_ref() - .map(|(returns, _)| map_parameters(returns)) - .unwrap_or_default(); - Self::Function( - Box::new(Self::Custom(vec!["__fn_type__".to_string()])), - params, - returns, - ) - } - // TODO: Rational numbers - pt::Type::Rational => return None, + // Elementary type used as a namespace: `address.balance`, `bytes.concat`, etc. + if let ExprKind::Type(ty) = &lhs.kind + && let TypeKind::Elementary(et) = &ty.kind + { + return match et { + ElementaryType::Address(_) => match member.as_str() { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + ElementaryType::Bytes => match member.as_str() { + "concat" => Some(DynSolType::Bytes), + _ => None, + }, + ElementaryType::String => match member.as_str() { + "concat" => Some(DynSolType::String), + _ => None, + }, + _ => None, }; - Some(ty) } - /// Handle special expressions like [global variables](https://docs.soliditylang.org/en/latest/cheatsheet.html#global-variables) - /// - /// See: - fn map_special(self) -> Self { - if !matches!(self, Self::Function(_, _, _) | Self::Access(_, _) | Self::Custom(_)) { - return self; - } + // Members on a resolved DynSolType (`.length`, `.pop`, `.selector`, `.address`). + if let Some(lhs_ty) = expr_to_dyn(gcx, lhs, lookup) + && let Some(ty) = dyn_member(&lhs_ty, member.as_str()) + { + return Some(ty); + } - let mut types = Vec::with_capacity(5); - let mut args = None; - self.recurse(&mut types, &mut args); + // HIR lookup for user-defined type members. + if lookup && let Some(mut chain) = expr_name_chain(gcx, lhs) { + chain.insert(0, member); + return infer_custom_type(gcx, &mut chain, None).ok().flatten(); + } - let len = types.len(); - if len == 0 { - return self; - } + None +} + +/// Returns the type of `builtin_ns.member` for built-in global namespaces. +fn builtin_member(builtin: &str, member: &str) -> Option { + match builtin { + "block" => match member { + "coinbase" => Some(DynSolType::Address), + "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" | "chainid" + | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), + _ => None, + }, + "msg" => match member { + "sender" => Some(DynSolType::Address), + "gas" | "value" => Some(DynSolType::Uint(256)), + "data" => Some(DynSolType::Bytes), + "sig" => Some(DynSolType::FixedBytes(4)), + _ => None, + }, + "tx" => match member { + "origin" => Some(DynSolType::Address), + "gasprice" => Some(DynSolType::Uint(256)), + _ => None, + }, + "address" => match member { + "balance" => Some(DynSolType::Uint(256)), + "code" => Some(DynSolType::Bytes), + "codehash" => Some(DynSolType::FixedBytes(32)), + "send" => Some(DynSolType::Bool), + _ => None, + }, + _ => None, + } +} + +/// Returns the type of `ty.member` for a known [`DynSolType`]. +fn dyn_member(ty: &DynSolType, member: &str) -> Option { + match member { + "length" => match ty { + DynSolType::Array(_) + | DynSolType::FixedArray(_, _) + | DynSolType::Bytes + | DynSolType::String + | DynSolType::FixedBytes(_) => Some(DynSolType::Uint(256)), + _ => None, + }, + "pop" => match ty { + DynSolType::Array(inner) => Some(*inner.clone()), + _ => None, + }, + // Address members. + "balance" => match ty { + DynSolType::Address => Some(DynSolType::Uint(256)), + _ => None, + }, + "code" => match ty { + DynSolType::Address => Some(DynSolType::Bytes), + _ => None, + }, + "codehash" => match ty { + DynSolType::Address => Some(DynSolType::FixedBytes(32)), + _ => None, + }, + "send" => match ty { + DynSolType::Address => Some(DynSolType::Bool), + _ => None, + }, + // External function members. + "selector" => Some(DynSolType::FixedBytes(4)), + "address" => Some(DynSolType::Address), + _ => None, + } +} - // Type members, like array, bytes etc - #[expect(clippy::single_match)] - #[allow(clippy::collapsible_match)] - match &self { - Self::Access(inner, access) => { - if let Some(ty) = inner.as_ref().clone().try_as_ethabi(None) { - // Array / bytes members - let ty = Self::Builtin(ty); - match access.as_str() { - "length" if ty.is_dynamic() || ty.is_array() || ty.is_fixed_bytes() => { - return Self::Builtin(DynSolType::Uint(256)); +/// Resolves a call expression to its return [`DynSolType`]. +/// +/// `expr` must be `ExprKind::Call`. +fn resolve_call(gcx: Gcx<'_>, expr: &Expr<'_>, lookup: bool) -> Option { + let ExprKind::Call(callee, args, _named) = &expr.kind else { return None }; + + // Type cast: `uint256(x)`, `address(y)`, etc. + if let ExprKind::Type(ty) = &callee.kind { + return hir_ty_to_dyn(gcx, ty); + } + + // Member call: `ns.method(...)`. + if let ExprKind::Member(lhs, method) = &callee.kind + && let ExprKind::Ident(reses) = &lhs.kind + && let Some(Res::Builtin(b)) = reses.first() + { + match b.name().as_str() { + "abi" => { + return match method.as_str() { + "decode" => { + let last = args.exprs().last()?; + match expr_to_dyn(gcx, last, false)? { + DynSolType::Tuple(tys) => Some(DynSolType::Tuple(tys)), + ty => Some(DynSolType::Tuple(vec![ty])), } - "pop" if ty.is_dynamic_array() => return ty, - _ => {} } - } + s if s.starts_with("encode") => Some(DynSolType::Bytes), + _ => None, + }; } + "string" if method.as_str() == "concat" => return Some(DynSolType::String), + "bytes" if method.as_str() == "concat" => return Some(DynSolType::Bytes), _ => {} } + } - let this = { - let name = types.last().unwrap().as_str(); - match len { - 0 => unreachable!(), - 1 => match name { + // Simple identifier call: built-in global functions and HIR function calls. + if let ExprKind::Ident(reses) = &callee.kind { + match reses.first() { + Some(Res::Builtin(b)) => { + return match b.name().as_str() { "gasleft" | "addmod" | "mulmod" => Some(DynSolType::Uint(256)), "keccak256" | "sha256" | "blockhash" => Some(DynSolType::FixedBytes(32)), "ripemd160" => Some(DynSolType::FixedBytes(20)), "ecrecover" => Some(DynSolType::Address), _ => None, - }, - 2 => { - let access = types.first().unwrap().as_str(); - match name { - "block" => match access { - "coinbase" => Some(DynSolType::Address), - "timestamp" | "difficulty" | "prevrandao" | "number" | "gaslimit" - | "chainid" | "basefee" | "blobbasefee" => Some(DynSolType::Uint(256)), - _ => None, - }, - "msg" => match access { - "sender" => Some(DynSolType::Address), - "gas" => Some(DynSolType::Uint(256)), - "value" => Some(DynSolType::Uint(256)), - "data" => Some(DynSolType::Bytes), - "sig" => Some(DynSolType::FixedBytes(4)), - _ => None, - }, - "tx" => match access { - "origin" => Some(DynSolType::Address), - "gasprice" => Some(DynSolType::Uint(256)), - _ => None, - }, - "abi" => match access { - "decode" => { - // args = Some([Bytes(_), Tuple(args)]) - // unwrapping is safe because this is first compiled by solc so - // it is guaranteed to be a valid call - let mut args = args.unwrap(); - let last = args.pop().unwrap(); - match last { - Some(ty) => { - return match ty { - Self::Tuple(_) => ty, - ty => Self::Tuple(vec![Some(ty)]), - }; - } - None => None, - } - } - s if s.starts_with("encode") => Some(DynSolType::Bytes), - _ => None, - }, - "address" => match access { - "balance" => Some(DynSolType::Uint(256)), - "code" => Some(DynSolType::Bytes), - "codehash" => Some(DynSolType::FixedBytes(32)), - "send" => Some(DynSolType::Bool), - _ => None, - }, - "type" => match access { - "name" => Some(DynSolType::String), - "creationCode" | "runtimeCode" => Some(DynSolType::Bytes), - "interfaceId" => Some(DynSolType::FixedBytes(4)), - "min" | "max" => Some( - // Either a builtin or an enum - (|| args?.pop()??.into_builtin())() - .unwrap_or(DynSolType::Uint(256)), - ), - _ => None, - }, - "string" => match access { - "concat" => Some(DynSolType::String), - _ => None, - }, - "bytes" => match access { - "concat" => Some(DynSolType::Bytes), - _ => None, - }, - _ => None, - } - } - _ => None, - } - }; - - this.map(Self::Builtin).unwrap_or_else(|| match types.last().unwrap().as_str() { - "this" | "super" => Self::Custom(types), - _ => match self { - Self::Custom(_) | Self::Access(_, _) => Self::Custom(types), - Self::Function(_, _, _) => self, - _ => unreachable!(), - }, - }) - } - - /// Recurses over itself, appending all the idents and function arguments in the order that they - /// are found - fn recurse(&self, types: &mut Vec, args: &mut Option>>) { - match self { - Self::Builtin(ty) => types.push(ty.to_string()), - Self::Custom(tys) => types.extend(tys.clone()), - Self::Access(expr, name) => { - types.push(name.clone()); - expr.recurse(types, args); + }; } - Self::Function(fn_name, fn_args, _fn_ret) => { - if args.is_none() && !fn_args.is_empty() { - *args = Some(fn_args.clone()); + Some(Res::Item(ItemId::Function(fid))) if lookup => { + let func = gcx.hir.function(*fid); + if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) { + return None; } - fn_name.recurse(types, args); + let ret_id = *func.returns.first()?; + return solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())); } _ => {} } } - /// Infers a custom type's true type by recursing up the parse tree - /// - /// ### Takes - /// - A reference to the [IntermediateOutput] - /// - An array of custom types generated by the `MemberAccess` arm of [Self::from_expression] - /// - An optional contract name. This should always be `None` when this function is first - /// called. - /// - /// ### Returns - /// - /// If successful, an `Ok(Some(DynSolType))` variant. - /// If gracefully failed, an `Ok(None)` variant. - /// If failed, an `Err(e)` variant. - fn infer_custom_type( - intermediate: &IntermediateOutput, - custom_type: &mut Vec, - contract_name: Option, - ) -> Result> { - if let Some("this" | "super") = custom_type.last().map(String::as_str) { - custom_type.pop(); + // Fall back to the callee's resolved type. + expr_to_dyn(gcx, callee, lookup) +} + +/// Extracts a name chain from a member-access expression tree for HIR lookup. +/// +/// The chain is ordered outermost-first so `a.b.c` produces `["c", "b", "a"]` with the root +/// identifier at the back. This matches the convention expected by [`infer_custom_type`]. +fn expr_name_chain(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option> { + match &expr.kind { + ExprKind::Ident(reses) => { + let res = reses.first()?; + let name = match *res { + Res::Item(ItemId::Variable(vid)) => gcx.hir.variable(vid).name?.name, + Res::Item(ItemId::Function(fid)) => gcx.hir.function(fid).name?.name, + Res::Item(ItemId::Contract(cid)) => gcx.hir.contract(cid).name.name, + Res::Builtin(b) => b.name(), + _ => return None, + }; + Some(vec![name]) } - if custom_type.is_empty() { - return Ok(None); + ExprKind::Member(lhs, ident) => { + let mut chain = expr_name_chain(gcx, lhs)?; + chain.insert(0, ident.name); + Some(chain) } + _ => None, + } +} - // If a contract exists with the given name, check its definitions for a match. - // Otherwise look in the `run` - if let Some(contract_name) = contract_name { - let intermediate_contract = intermediate - .intermediate_contracts - .get(&contract_name) - .ok_or_else(|| eyre::eyre!("Could not find intermediate contract!"))?; - - let cur_type = custom_type.last().unwrap(); - if let Some(func) = intermediate_contract.function_definitions.get(cur_type) { - // Check if the custom type is a function pointer member access - if let res @ Some(_) = func_members(func, custom_type) { - return Ok(res); - } - - // Because tuple types cannot be passed to `abi.encode`, we will only be - // receiving functions that have 0 or 1 return parameters here. - if func.returns.is_empty() { - eyre::bail!( - "This call expression does not return any values to inspect. Insert as statement." - ) - } +/// Infers a custom type's true type by recursing through the HIR. +/// +/// `custom_type` is a name chain ordered outermost-first (root at back). This is mutated during +/// resolution. `contract_id` narrows the search to a specific contract scope. +fn infer_custom_type( + gcx: Gcx<'_>, + custom_type: &mut Vec, + contract_id: Option, +) -> Result> { + if let Some(last) = custom_type.last() + && (last.as_str() == "this" || last.as_str() == "super") + { + custom_type.pop(); + } + if custom_type.is_empty() { + return Ok(None); + } - // Empty return types check is done above - let (_, param) = func.returns.first().unwrap(); - // Return type should always be present - let return_ty = ¶m.as_ref().unwrap().ty; - - // If the return type is a variable (not a type expression), re-enter the recursion - // on the same contract for a variable / struct search. It could be a contract, - // struct, array, etc. - if let pt::Expression::Variable(ident) = return_ty { - custom_type.push(ident.name.clone()); - return Self::infer_custom_type(intermediate, custom_type, Some(contract_name)); - } + if let Some(cid) = contract_id { + let hir = &gcx.hir; + let contract = hir.contract(cid); - // Check if our final function call alters the state. If it does, we bail so that it - // will be inserted normally without inspecting. If the state mutability was not - // expressly set, the function is inferred to alter state. - if let Some(pt::FunctionAttribute::Mutability(_mut)) = func - .attributes - .iter() - .find(|attr| matches!(attr, pt::FunctionAttribute::Mutability(_))) - { - if let pt::Mutability::Payable(_) = _mut { - eyre::bail!("This function mutates state. Insert as a statement.") - } - } else { - eyre::bail!("This function mutates state. Insert as a statement.") - } + let cur_name = *custom_type.last().unwrap(); + let cur = cur_name.as_str(); - Ok(Self::ethabi(return_ty, Some(intermediate))) - } else if let Some(var) = intermediate_contract.variable_definitions.get(cur_type) { - Self::infer_var_expr(&var.ty, Some(intermediate), custom_type) - } else if let Some(strukt) = intermediate_contract.struct_definitions.get(cur_type) { - let inner_types = strukt - .fields - .iter() - .map(|var| { - Self::ethabi(&var.ty, Some(intermediate)) - .ok_or_else(|| eyre::eyre!("Struct `{cur_type}` has invalid fields")) - }) - .collect::>>()?; - Ok(Some(DynSolType::Tuple(inner_types))) - } else { - eyre::bail!( - "Could not find any definition in contract \"{contract_name}\" for type: {custom_type:?}" - ) - } - } else { - // Check if the custom type is a variable or function within the REPL contract before - // anything. If it is, we can stop here. - if let Ok(res) = Self::infer_custom_type(intermediate, custom_type, Some("REPL".into())) - { + // Function? + if let Some(fid) = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + let func = hir.function(fid); + if let res @ Some(_) = func_members(func, custom_type) { return Ok(res); } - // Check if the first element of the custom type is a known contract. If it is, begin - // our recursion on that contract's definitions. - let name = custom_type.last().unwrap(); - let contract = intermediate.intermediate_contracts.get(name); - if contract.is_some() { - let contract_name = custom_type.pop(); - return Self::infer_custom_type(intermediate, custom_type, contract_name); + if func.returns.is_empty() { + eyre::bail!( + "This call expression does not return any values to inspect. Insert as statement." + ) } - // See [`Type::infer_var_expr`] - let name = custom_type.last().unwrap(); - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - return Self::infer_var_expr(expr, Some(intermediate), custom_type); + let sm = func.state_mutability; + if !matches!(sm, StateMutability::View | StateMutability::Pure) { + eyre::bail!("This function mutates state. Insert as a statement.") } - // The first element of our custom type was neither a variable or a function within the - // REPL contract, move on to globally available types gracefully. - Ok(None) + let ret_id = func.returns[0]; + let ret_var = hir.variable(ret_id); + return Ok(solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) + .or_else(|| hir_ty_to_dyn(gcx, &ret_var.ty))); } - } - /// Infers the type from a variable's type - fn infer_var_expr( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - custom_type: &mut Vec, - ) -> Result> { - // Resolve local (in `run` function) or global (in the `REPL` or other contract) variable - let res = match &expr { - // Custom variable handling - pt::Expression::Variable(ident) => { - let name = &ident.name; - - if let Some(intermediate) = intermediate { - // expression in `run` - if let Some(expr) = intermediate.repl_contract_expressions.get(name) { - Self::infer_var_expr(expr, Some(intermediate), custom_type) - } else if intermediate.intermediate_contracts.contains_key(name) { - if custom_type.len() > 1 { - // There is still some recursing left to do: jump into the contract. - custom_type.pop(); - Self::infer_custom_type(intermediate, custom_type, Some(name.clone())) - } else { - // We have no types left to recurse: return the address of the contract. - Ok(Some(DynSolType::Address)) - } - } else { - Err(eyre::eyre!("Could not infer variable type")) - } - } else { - Ok(None) - } - } - other_expr => Ok(Self::ethabi(other_expr, intermediate)), - }; - // re-run everything with the resolved variable in case we're accessing a builtin member - // for example array or bytes length etc - match res { - Ok(Some(ty)) => { - let box_ty = Box::new(Self::Builtin(ty.clone())); - let access = Self::Access(box_ty, custom_type.drain(..).next().unwrap_or_default()); - if let Some(mapped) = access.map_special().try_as_ethabi(intermediate) { - Ok(Some(mapped)) - } else { - Ok(Some(ty)) + // Variable? + if let Some(vid) = contract + .variables() + .find(|&v| hir.variable(v).name.as_ref().map(|n| n.as_str() == cur).unwrap_or(false)) + { + if let Some(ty) = solar_ty_to_dyn(gcx, gcx.type_of_item(vid.into())) { + custom_type.pop(); + if custom_type.is_empty() { + return Ok(Some(ty)); } + let next_member = custom_type.drain(..).next().unwrap_or(Symbol::DUMMY); + return Ok(dyn_member(&ty, next_member.as_str()).or(Some(ty))); } - res => res, - } - } - - /// Attempt to convert this type into a [DynSolType] - /// - /// ### Takes - /// An immutable reference to an [IntermediateOutput] - /// - /// ### Returns - /// Optionally, a [DynSolType] - fn try_as_ethabi(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - Self::Tuple(types) => Some(DynSolType::Tuple(types_to_parameters(types, intermediate))), - Self::Array(inner) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::Array(Box::new(inner))), - }, - Self::FixedArray(inner, size) => match *inner { - ty @ Self::Custom(_) => ty.try_as_ethabi(intermediate), - _ => inner - .try_as_ethabi(intermediate) - .map(|inner| DynSolType::FixedArray(Box::new(inner), size)), - }, - ty @ Self::ArrayIndex(_, _) => ty.into_array_index(intermediate), - Self::Function(ty, _, _) => ty.try_as_ethabi(intermediate), - // should have been mapped to `Custom` in previous steps - Self::Access(_, _) => None, - Self::Custom(mut types) => { - // Cover any local non-state-modifying function call expressions - intermediate.and_then(|intermediate| { - Self::infer_custom_type(intermediate, &mut types, None).ok().flatten() - }) - } + let var = hir.variable(vid); + return infer_var_ty(gcx, &var.ty, custom_type); } - } - - /// Equivalent to `Type::from_expression` + `Type::map_special` + `Type::try_as_ethabi` - fn ethabi( - expr: &pt::Expression, - intermediate: Option<&IntermediateOutput>, - ) -> Option { - Self::from_expression(expr) - .map(Self::map_special) - .and_then(|ty| ty.try_as_ethabi(intermediate)) - } - /// Get the return type of a function call expression. - fn get_function_return_type<'a>( - contract_expr: Option<&'a pt::Expression>, - intermediate: &IntermediateOutput, - ) -> Option<(&'a pt::Expression, DynSolType)> { - let function_call = match contract_expr? { - pt::Expression::FunctionCall(_, function_call, _) => function_call, - _ => return None, - }; - let (contract_name, function_name) = match function_call.as_ref() { - pt::Expression::MemberAccess(_, contract_name, function_name) => { - (contract_name, function_name) + // Struct? + if let Some(sid) = contract.items.iter().find_map(|i| { + if let ItemId::Struct(sid) = i + && hir.strukt(*sid).name.as_str() == cur + { + Some(*sid) + } else { + None } - _ => return None, - }; - let contract_name = match contract_name.as_ref() { - pt::Expression::Variable(contract_name) => contract_name.to_owned(), - _ => return None, - }; - - let pt::Expression::Variable(contract_name) = - intermediate.repl_contract_expressions.get(&contract_name.name)? - else { - return None; - }; - - let contract = intermediate - .intermediate_contracts - .get(&contract_name.name)? - .function_definitions - .get(&function_name.name)?; - let return_parameter = contract.as_ref().returns.first()?.to_owned().1?; - Self::ethabi(&return_parameter.ty, Some(intermediate)).map(|p| (contract_expr.unwrap(), p)) - } - - /// Inverts Int to Uint and vice-versa. - fn invert_int(self) -> Self { - match self { - Self::Builtin(DynSolType::Uint(n)) => Self::Builtin(DynSolType::Int(n)), - Self::Builtin(DynSolType::Int(n)) => Self::Builtin(DynSolType::Uint(n)), - x => x, + }) { + let inner = gcx + .struct_field_types(sid) + .iter() + .map(|&t| { + solar_ty_to_dyn(gcx, t) + .ok_or_else(|| eyre::eyre!("Struct `{cur}` has invalid fields")) + }) + .collect::>>()?; + return Ok(Some(DynSolType::Tuple(inner))); } - } - /// Returns the `DynSolType` contained by `Type::Builtin` - #[inline] - fn into_builtin(self) -> Option { - match self { - Self::Builtin(ty) => Some(ty), - _ => None, - } + eyre::bail!( + "Could not find any definition in contract \"{}\" for type: {custom_type:?}", + contract.name.as_str() + ) } - /// Returns the resulting `DynSolType` of indexing self - fn into_array_index(self, intermediate: Option<&IntermediateOutput>) -> Option { - match self { - Self::Array(inner) | Self::FixedArray(inner, _) | Self::ArrayIndex(inner, _) => { - match inner.try_as_ethabi(intermediate) { - Some(DynSolType::Array(inner) | DynSolType::FixedArray(inner, _)) => { - Some(*inner) - } - Some(DynSolType::Bytes | DynSolType::String | DynSolType::FixedBytes(_)) => { - Some(DynSolType::FixedBytes(1)) - } - ty => ty, - } - } - _ => None, - } + let repl_id = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == "REPL").then_some(cid)); + if let Some(repl_id) = repl_id + && let Ok(res) = infer_custom_type(gcx, custom_type, Some(repl_id)) + { + return Ok(res); } - /// Returns whether this type is dynamic - #[inline] - const fn is_dynamic(&self) -> bool { - match self { - // TODO: Note, this is not entirely correct. Fixed arrays of non-dynamic types are - // not dynamic, nor are tuples of non-dynamic types. - Self::Builtin(DynSolType::Bytes | DynSolType::String | DynSolType::Array(_)) => true, - Self::Array(_) => true, - _ => false, - } + let last_name = *custom_type.last().unwrap(); + let last = last_name.as_str(); + let contract_match = gcx + .hir + .contracts_enumerated() + .find_map(|(cid, c)| (c.name.as_str() == last).then_some(cid)); + if let Some(cid) = contract_match { + custom_type.pop(); + return infer_custom_type(gcx, custom_type, Some(cid)); } - /// Returns whether this type is an array - #[inline] - const fn is_array(&self) -> bool { - matches!( - self, - Self::Array(_) - | Self::FixedArray(_, _) - | Self::Builtin(DynSolType::Array(_) | DynSolType::FixedArray(_, _)) - ) - } + Ok(None) +} - /// Returns whether this type is a dynamic array (can call push, pop) - #[inline] - const fn is_dynamic_array(&self) -> bool { - matches!(self, Self::Array(_) | Self::Builtin(DynSolType::Array(_))) +/// Infers the type from a variable's HIR type, optionally accessing a named member. +fn infer_var_ty( + gcx: Gcx<'_>, + ty: &HirType<'_>, + custom_type: &mut Vec, +) -> Result> { + let Some(ty) = hir_ty_to_dyn(gcx, ty) else { return Ok(None) }; + let next_member = custom_type.drain(..).next(); + if let Some(m) = next_member { + Ok(dyn_member(&ty, m.as_str()).or(Some(ty))) + } else { + Ok(Some(ty)) } +} - const fn is_fixed_bytes(&self) -> bool { - matches!(self, Self::Builtin(DynSolType::FixedBytes(_))) - } +/// Get the return type of a contract method call `receiver.method()`. +fn get_function_return_type(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option { + let ExprKind::Call(callee, _, _) = &expr.kind else { return None }; + let ExprKind::Member(obj, fn_ident) = &callee.kind else { return None }; + let ExprKind::Ident(reses) = &obj.kind else { return None }; + let res = reses.first()?; + let var_id = match res { + Res::Item(ItemId::Variable(vid)) => *vid, + _ => return None, + }; + let var_ty = gcx.type_of_item(var_id.into()).peel_refs(); + let cid = match var_ty.kind { + TyKind::Contract(cid) => cid, + _ => return None, + }; + + let hir = &gcx.hir; + let contract = hir.contract(cid); + let fid = contract + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some(fn_ident.as_str()))?; + let func = hir.function(fid); + let ret_id = *func.returns.first()?; + solar_ty_to_dyn(gcx, gcx.type_of_item(ret_id.into())) } -/// Returns Some if the custom type is a function member access +/// Returns Some if the custom type is a function member access. /// /// Ref: #[inline] -fn func_members(func: &pt::FunctionDefinition, custom_type: &[String]) -> Option { - if !matches!(func.ty, pt::FunctionTy::Function) { +fn func_members(func: &Function<'_>, custom_type: &[Symbol]) -> Option { + if !matches!(func.kind, FunctionKind::Function) { return None; } - - let vis = func.attributes.iter().find_map(|attr| match attr { - pt::FunctionAttribute::Visibility(vis) => Some(vis), - _ => None, - }); - match vis { - Some(pt::Visibility::External(_) | pt::Visibility::Public(_)) => { - match custom_type.first().unwrap().as_str() { - "address" => Some(DynSolType::Address), - "selector" => Some(DynSolType::FixedBytes(4)), - _ => None, - } - } + if !matches!(func.visibility, Visibility::External | Visibility::Public) { + return None; + } + match custom_type.first().unwrap().as_str() { + "address" => Some(DynSolType::Address), + "selector" => Some(DynSolType::FixedBytes(4)), _ => None, } } -/// Whether execution should continue after inspecting this expression +/// Whether execution should continue after inspecting this expression. #[inline] -fn should_continue(expr: &pt::Expression) -> bool { - match expr { - // assignments - pt::Expression::PreDecrement(_, _) | // -- - pt::Expression::PostDecrement(_, _) | // -- - pt::Expression::PreIncrement(_, _) | // ++ - pt::Expression::PostIncrement(_, _) | // ++ - pt::Expression::Assign(_, _, _) | // = ... - pt::Expression::AssignAdd(_, _, _) | // += ... - pt::Expression::AssignSubtract(_, _, _) | // -= ... - pt::Expression::AssignMultiply(_, _, _) | // *= ... - pt::Expression::AssignDivide(_, _, _) | // /= ... - pt::Expression::AssignModulo(_, _, _) | // %= ... - pt::Expression::AssignAnd(_, _, _) | // &= ... - pt::Expression::AssignOr(_, _, _) | // |= ... - pt::Expression::AssignXor(_, _, _) | // ^= ... - pt::Expression::AssignShiftLeft(_, _, _) | // <<= ... - pt::Expression::AssignShiftRight(_, _, _) // >>= ... - => { - true - } - +fn should_continue(expr: &Expr<'_>) -> bool { + match &expr.kind { + // assignments and compound assignments + ExprKind::Assign(_, _, _) => true, + // ++/-- pre/post operations + ExprKind::Unary(op, _) => matches!( + op.kind, + UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec + ), // Array.pop() - pt::Expression::FunctionCall(_, lhs, _) => { - match lhs.as_ref() { - pt::Expression::MemberAccess(_, _inner, access) => access.name == "pop", - _ => false - } - } - - _ => false + ExprKind::Call(callee, _, _) => match &callee.kind { + ExprKind::Member(_, ident) => ident.as_str() == "pop", + _ => false, + }, + _ => false, } } -fn map_parameters(params: &[(pt::Loc, Option)]) -> Vec> { - params - .iter() - .map(|(_, param)| param.as_ref().and_then(|param| Type::from_expression(¶m.ty))) - .collect() +/// Parses an [`Expr`] number/hex literal into a `U256`. Returns `None` if the expression +/// is not a numeric literal. +/// +/// SubDenominations are already applied to numeric literals in solar's HIR. +const fn parse_number_literal(expr: &Expr<'_>) -> Option { + match &expr.kind { + ExprKind::Lit(lit) => match &lit.kind { + LitKind::Number(n) => Some(*n), + _ => None, + }, + _ => None, + } } -fn types_to_parameters( - types: Vec>, - intermediate: Option<&IntermediateOutput>, -) -> Vec { - types.into_iter().filter_map(|ty| ty.and_then(|ty| ty.try_as_ethabi(intermediate))).collect() +/// Maps a solar [`ElementaryType`] to a [`DynSolType`]. +const fn elementary_to_dyn(et: ElementaryType) -> Option { + Some(match et { + ElementaryType::Address(_) => DynSolType::Address, + ElementaryType::Bool => DynSolType::Bool, + ElementaryType::String => DynSolType::String, + ElementaryType::Bytes => DynSolType::Bytes, + ElementaryType::Int(size) => DynSolType::Int(size.bits() as usize), + ElementaryType::UInt(size) => DynSolType::Uint(size.bits() as usize), + ElementaryType::FixedBytes(size) => DynSolType::FixedBytes(size.bytes() as usize), + // Fixed-point numbers are not yet representable as DynSolType. + ElementaryType::Fixed(_, _) | ElementaryType::UFixed(_, _) => return None, + }) } -fn parse_number_literal(expr: &pt::Expression) -> Option { - match expr { - pt::Expression::NumberLiteral(_, num, exp, unit) => { - let num = num.parse::().unwrap_or(U256::ZERO); - let exp = exp.parse().unwrap_or(0u32); - if exp > 77 { - None +/// Maps a solar [`Ty`] to a [`DynSolType`]. +fn solar_ty_to_dyn<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> Option { + match ty.kind { + TyKind::Elementary(et) => elementary_to_dyn(et), + TyKind::Ref(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Array(elem, n) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + let size: usize = n.try_into().ok()?; + Some(DynSolType::FixedArray(Box::new(inner), size)) + } + TyKind::DynArray(elem) | TyKind::Slice(elem) => { + let inner = solar_ty_to_dyn(gcx, elem)?; + Some(DynSolType::Array(Box::new(inner))) + } + TyKind::Tuple(tys) => { + Some(DynSolType::Tuple(tys.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect())) + } + TyKind::Mapping(_, _) => None, + TyKind::Struct(sid) => Some(DynSolType::Tuple( + gcx.struct_field_types(sid).iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + TyKind::Enum(_) => Some(DynSolType::Uint(8)), + TyKind::Udvt(inner, _) => solar_ty_to_dyn(gcx, inner), + TyKind::Contract(_) => Some(DynSolType::Address), + // For a function-pointer type we return the ABI type of what the call *produces*, not a + // representation of the pointer itself. This is intentional: chisel inspects values, so + // the interesting type is the returned value. A zero-return function pointer has no + // inspectable value, so we return `None`. + TyKind::FnPtr(f) => match f.returns.len() { + 0 => None, + 1 => solar_ty_to_dyn(gcx, f.returns[0]), + _ => Some(DynSolType::Tuple( + f.returns.iter().filter_map(|t| solar_ty_to_dyn(gcx, *t)).collect(), + )), + }, + TyKind::Type(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::Meta(inner) => solar_ty_to_dyn(gcx, inner), + TyKind::IntLiteral(neg, size) => { + let bits = (size.bits() as usize).max(8); + // Round up to the nearest multiple of 8 bits, capped at 256. + let bits = bits.div_ceil(8) * 8; + let bits = bits.min(256); + if neg { + Some(DynSolType::Int(bits.max(8))) } else { - let exp = U256::from(10usize.pow(exp)); - let unit_mul = unit_multiplier(unit).ok()?; - Some(num * exp * unit_mul) + Some(DynSolType::Uint(bits.max(8))) } } - pt::Expression::HexNumberLiteral(_, num, unit) => { - let unit_mul = unit_multiplier(unit).ok()?; - num.parse::().map(|num| num * unit_mul).ok() + TyKind::StringLiteral(valid_utf8, _) => { + if valid_utf8 { + Some(DynSolType::String) + } else { + Some(DynSolType::Bytes) + } } - // TODO: Rational numbers - pt::Expression::RationalNumberLiteral(..) => None, + TyKind::Module(_) + | TyKind::BuiltinModule(_) + | TyKind::Error(_, _) + | TyKind::Event(_, _) + | TyKind::Err(_) => None, _ => None, } } -#[inline] -fn unit_multiplier(unit: &Option) -> Result { - if let Some(unit) = unit { - let mul = match unit.name.as_str() { - "seconds" => 1, - "minutes" => 60, - "hours" => 60 * 60, - "days" => 60 * 60 * 24, - "weeks" => 60 * 60 * 24 * 7, - "wei" => 1, - "gwei" => 10_usize.pow(9), - "ether" => 10_usize.pow(18), - other => eyre::bail!("unknown unit: {other}"), - }; - Ok(U256::from(mul)) - } else { - Ok(U256::from(1)) - } -} - #[cfg(test)] mod tests { use super::*; use foundry_compilers::{error::SolcError, solc::Solc}; + use solar::sema::Compiler; use std::sync::Mutex; #[test] @@ -1558,46 +1455,66 @@ mod tests { DynSolType::FixedArray(Box::new(ty), len) } - fn parse(s: &mut SessionSource, input: &str, clear: bool) -> IntermediateOutput { + /// Lowers the given snippet appended to the REPL contract via solar's HIR pipeline (without + /// invoking solc) and returns the resulting `DynSolType` of the last expression statement in + /// the run() body. + /// + /// Tests bypass `SessionSource::build` (which routes through foundry-compilers + solc) so that + /// inputs which are syntactically valid but semantically rejected by solc (e.g. + /// `abi.decode(bytes, (uint8[13]))` or `a[0:3]` on a memory array) can still exercise the + /// HIR-based type-inference engine. + fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { if clear { s.clear(); } + // Always declare a sample enum so `Enum1` is available for `type(Enum1)` tests. *s = s.clone_with_new_line("enum Enum1 { A }".into()).unwrap().0; let input = format!("{};", input.trim_end().trim_end_matches(';')); - let (mut _s, _) = s.clone_with_new_line(input).unwrap(); - *s = _s.clone(); - let s = &mut _s; - - if let Err(e) = s.parse() { - let source = s.to_repl_source(); - panic!("{e}\n\ncould not parse input:\n{source}") - } - s.generate_intermediate_output().expect("could not generate intermediate output") - } - - fn expr(stmts: &[pt::Statement]) -> pt::Expression { - match stmts.last().expect("no statements") { - pt::Statement::Expression(_, e) => e.clone(), - s => panic!("Not an expression: {s:?}"), - } - } - - fn get_type( - s: &mut SessionSource, - input: &str, - clear: bool, - ) -> (Option, IntermediateOutput) { - let intermediate = parse(s, input, clear); - let run_func_body = intermediate.run_func_body().expect("no run func body"); - let expr = expr(run_func_body); - (Type::from_expression(&expr).map(Type::map_special), intermediate) - } + let (new_source, _) = s.clone_with_new_line(input).unwrap(); + *s = new_source.clone(); + + let src = new_source.to_repl_source(); + let sess = + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(); + let mut compiler = Compiler::new(sess); + + compiler.enter_mut(|c| -> Option { + // Stage 1: parse + lower (mutable access required). + let lowered = { + let mut pcx = c.parse(); + let file = c + .sess() + .source_map() + .new_source_file( + std::path::PathBuf::from(new_source.file_name.clone()), + src.clone(), + ) + .ok()?; + pcx.add_file(file); + pcx.parse(); + matches!(c.lower_asts(), Ok(ControlFlow::Continue(()))) + }; + if !lowered { + return None; + } - fn get_type_ethabi(s: &mut SessionSource, input: &str, clear: bool) -> Option { - let (ty, intermediate) = get_type(s, input, clear); - ty.and_then(|ty| ty.try_as_ethabi(Some(&intermediate))) + // Stage 2: walk HIR (immutable access). + let gcx = c.gcx(); + let hir = &gcx.hir; + let repl = hir.contracts().find(|c| c.name.as_str() == "REPL")?; + let run_fid = repl + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run"))?; + let body = hir.function(run_fid).body?; + let last = body.last()?; + let expr = match last.kind { + StmtKind::Expr(e) => e, + _ => return None, + }; + expr_to_dyn(gcx, expr, true) + }) } fn generic_type_test<'a, T, I>(s: &mut SessionSource, input: I) diff --git a/crates/chisel/src/source.rs b/crates/chisel/src/source.rs index 7eedb31923439..90c0bad874622 100644 --- a/crates/chisel/src/source.rs +++ b/crates/chisel/src/source.rs @@ -5,8 +5,6 @@ //! execution helpers. use eyre::Result; -use forge_doc::solang_ext::{CodeLocationExt, SafeUnwrap}; -use foundry_common::fs; use foundry_compilers::{ Artifact, ProjectCompileOutput, artifacts::{ConfigurableContractArtifact, Source, Sources}, @@ -17,9 +15,16 @@ use foundry_config::{Config, SolcReq}; use foundry_evm::{backend::Backend, core::bytecode::InstIter, opts::EvmOpts}; use semver::Version; use serde::{Deserialize, Serialize}; -use solang_parser::pt; -use solar::interface::diagnostics::EmittedDiagnostics; -use std::{cell::OnceCell, collections::HashMap, fmt, path::PathBuf}; +use solar::{ + ast::{ItemKind, StmtKind as AstStmtKind, yul}, + interface::{Span, diagnostics::EmittedDiagnostics}, + sema::{ + CompilerRef, + hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind}, + ty::Gcx, + }, +}; +use std::{cell::OnceCell, fmt}; use walkdir::WalkDir; /// The minimum Solidity version of the `Vm` interface. @@ -31,41 +36,8 @@ static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol"); /// [`SessionSource`] build output. pub struct GeneratedOutput { output: ProjectCompileOutput, - pub(crate) intermediate: IntermediateOutput, -} - -pub struct GeneratedOutputRef<'a> { - output: &'a ProjectCompileOutput, - // compiler: &'b solar::sema::CompilerRef<'c>, - pub(crate) intermediate: &'a IntermediateOutput, -} - -/// Intermediate output for the compiled [SessionSource] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IntermediateOutput { - /// All expressions within the REPL contract's run function and top level scope. - pub repl_contract_expressions: HashMap, - /// Intermediate contracts - pub intermediate_contracts: IntermediateContracts, -} - -/// A refined intermediate parse tree for a contract that enables easy lookups -/// of definitions. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IntermediateContract { - /// All function definitions within the contract - pub function_definitions: HashMap>, - /// All event definitions within the contract - pub event_definitions: HashMap>, - /// All struct definitions within the contract - pub struct_definitions: HashMap>, - /// All variable definitions within the top level scope of the contract - pub variable_definitions: HashMap>, } -/// A defined type for a map of contract names to [IntermediateContract]s -type IntermediateContracts = HashMap; - impl fmt::Debug for GeneratedOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("GeneratedOutput").finish_non_exhaustive() @@ -73,158 +45,25 @@ impl fmt::Debug for GeneratedOutput { } impl GeneratedOutput { - pub fn enter(&self, f: impl FnOnce(GeneratedOutputRef<'_>) -> T + Send) -> T { - // TODO(dani): once intermediate is removed - // self.output - // .parser() - // .solc() - // .compiler() - // .enter(|compiler| f(GeneratedOutputRef { output: &self.output, compiler })) - f(GeneratedOutputRef { output: &self.output, intermediate: &self.intermediate }) - } -} - -impl GeneratedOutputRef<'_> { - pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> { - self.output.find_first("REPL") - } -} - -impl std::ops::Deref for GeneratedOutput { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - &self.intermediate - } -} -impl std::ops::Deref for GeneratedOutputRef<'_> { - type Target = IntermediateOutput; - fn deref(&self) -> &Self::Target { - self.intermediate + /// Enters the solar compiler context, providing access to the HIR and `Gcx`. + pub fn enter( + &self, + f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send, + ) -> R { + self.output + .parser() + .solc() + .compiler() + .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c })) } } -impl IntermediateOutput { - pub fn get_event(&self, input: &str) -> Option<&pt::EventDefinition> { - self.intermediate_contracts - .get("REPL") - .and_then(|contract| contract.event_definitions.get(input).map(std::ops::Deref::deref)) - } - - pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { - let deployed_bytecode = contract - .get_deployed_bytecode() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - let deployed_bytecode_bytes = deployed_bytecode - .bytes() - .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?; - - let run_func_statements = self.run_func_body()?; - - // Record loc of first yul block return statement (if any). - // This is used to decide which is the final statement within the `run()` method. - // see . - let last_yul_return = run_func_statements.iter().find_map(|statement| { - if let pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } = statement - && let Some(statement) = block.statements.last() - && let pt::YulStatement::FunctionCall(yul_call) = statement - && yul_call.id.name == "return" - { - return Some(statement.loc()); - } - None - }); - - // Find the last statement within the "run()" method and get the program - // counter via the source map. - let Some(final_statement) = run_func_statements.last() else { return Ok(None) }; - - // If the final statement is some type of block (assembly, unchecked, or regular), - // we need to find the final statement within that block. Otherwise, default to - // the source loc of the final statement of the `run()` function's block. - // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. - let mut source_loc = match final_statement { - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(statement) = last_statement { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the asm block. Because we use saturating sub to get the second - // to last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - pt::Statement::Block { loc: _, unchecked: _, statements } => { - if let Some(statement) = statements.last() { - statement.loc() - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - run_func_statements - .get(run_func_statements.len().saturating_sub(2)) - .unwrap() - .loc() - } - } - _ => final_statement.loc(), - }; - - // Consider yul return statement as final statement (if it's loc is lower) . - if let Some(yul_return) = last_yul_return - && yul_return.end() < source_loc.start() - { - source_loc = yul_return; - } - - // Map the source location of the final statement of the `run()` function to its - // corresponding runtime program counter - let final_pc = { - let offset = source_loc.start() as u32; - let length = (source_loc.end() - source_loc.start()) as u32; - trace!(%offset, %length, "find pc"); - contract - .get_source_map_deployed() - .unwrap() - .unwrap() - .into_iter() - .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) - .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, pc)| pc) - .max() - }; - trace!(?final_pc); - Ok(final_pc) - } - - pub fn run_func_body(&self) -> Result<&Vec> { - match self - .intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find REPL intermediate contract!"))? - .function_definitions - .get("run") - .ok_or_else(|| eyre::eyre!("Could not find run function definition in REPL contract!"))? - .body - .as_ref() - .ok_or_else(|| eyre::eyre!("Could not find run function body!"))? - { - pt::Statement::Block { statements, .. } => Ok(statements), - _ => eyre::bail!("Could not find statements within run function body!"), - } - } +/// A scoped reference to a [`GeneratedOutput`] together with an entered solar compiler. +pub struct GeneratedOutputRef<'a, 'b, 'gcx> { + output: &'a ProjectCompileOutput, + pub(crate) compiler: &'b CompilerRef<'gcx>, } -// TODO(dani): further migration blocked on upstream work -#[cfg(false)] impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { pub fn gcx(&self) -> Gcx<'gcx> { self.compiler.gcx() @@ -234,8 +73,35 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { self.output.find_first("REPL") } - pub fn get_event(&self, input: &str) -> Option { - self.gcx().hir.events_enumerated().find(|(_, e)| e.name.as_str() == input).map(|(id, _)| id) + /// Looks up the REPL contract in the HIR. + pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> { + self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + } + + /// Returns the body block of the REPL `run()` function. + pub fn run_func_body(&self) -> Block<'gcx> { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); + let f = c + .functions() + .find(|&f| hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) + .expect("`run()` function not found in REPL contract"); + hir.function(f).body.expect("`run()` function does not have a body") + } + + /// Returns the [`EventId`] of an event named `input` in the REPL contract, if any. + pub fn get_event(&self, input: &str) -> Option { + let hir = &self.gcx().hir; + let c = self.repl_contract_hir()?; + c.items.iter().find_map(|id| { + if let ItemId::Event(eid) = id + && hir.event(*eid).name.as_str() == input + { + Some(*eid) + } else { + None + } + }) } pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result> { @@ -252,52 +118,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Record loc of first yul block return statement (if any). // This is used to decide which is the final statement within the `run()` method. // see . - let last_yul_return_span: Option = run_body.iter().find_map(|stmt| { - // TODO(dani): Yul is not yet lowered to HIR. - let _ = stmt; - /* - if let hir::StmtKind::Assembly { block, .. } = stmt { - if let Some(stmt) = block.last() { - if let pt::YulStatement::FunctionCall(yul_call) = stmt { - if yul_call.id.name == "return" { - return Some(stmt.loc()) - } - } - } - } - */ - None - }); + // + // Yul is not yet lowered to HIR (assembly statements appear as `StmtKind::Err`), + // so we walk the AST of the REPL source to find a top-level `return(...)` call + // inside any `assembly { ... }` block in `run()`. + let last_yul_return_span: Option = self.first_yul_return_span(); // Find the last statement within the "run()" method and get the program // counter via the source map. let Some(last_stmt) = run_body.last() else { return Ok(None) }; - // If the final statement is some type of block (assembly, unchecked, or regular), + // If the final statement is some type of block (unchecked or regular), // we need to find the final statement within that block. Otherwise, default to // the source loc of the final statement of the `run()` function's block. // - // There is some code duplication within the arms due to the difference between - // the [pt::Statement] type and the [pt::YulStatement] types. + // Inline assembly blocks (lowered to `StmtKind::Err` in HIR in the pinned solar + // version) are handled separately via `trailing_assembly_last_stmt_span`, which + // walks the AST to recover the last meaningful Yul statement. let source_stmt = match &last_stmt.kind { - // TODO(dani): Yul is not yet lowered to HIR. - /* - pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - // Select last non variable declaration statement, see . - let last_statement = block.statements.iter().rev().find(|statement| { - !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) - }); - if let Some(stmt) = last_statement { - stmt - } else { - // In the case where the block is empty, attempt to grab the statement - // before the block. Because we use saturating sub to get the second to - // last index, this can always be safely unwrapped. - &run_body[run_body.len().saturating_sub(2)] - } - } - */ - hir::StmtKind::UncheckedBlock(stmts) | hir::StmtKind::Block(stmts) => { + HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => { if let Some(stmt) = stmts.last() { stmt } else { @@ -309,9 +148,25 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { } _ => last_stmt, }; - let mut source_span = self.stmt_span_without_semicolon(source_stmt); + // If the trailing statement is an assembly block, prefer the last meaningful + // (non-`let`) Yul statement's span as the source location for `final_pc`. + // See . + // + // Two guards are required: + // 1. `StmtKind::Err`, assembly lowers to an error node in the current pinned solar + // version; this ensures we don't apply the AST fallback to properly-lowered stmts. + // 2. `trailing_assembly_last_stmt_span` returning `Some`, verifies via the AST that the + // failing HIR node actually corresponds to an assembly block (not some other lowering + // failure), and supplies the concrete span to use. + let mut source_span = if matches!(last_stmt.kind, HirStmtKind::Err(_)) + && let Some(span) = self.trailing_assembly_last_stmt_span() + { + span + } else { + self.stmt_span_without_semicolon(source_stmt) + }; - // Consider yul return statement as final statement (if it's loc is lower) . + // Consider yul return statement as final statement (if it's loc is lower). if let Some(yul_return_span) = last_yul_return_span && yul_return_span.hi() < source_span.lo() { @@ -320,26 +175,32 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { // Map the source location of the final statement of the `run()` function to its // corresponding runtime program counter - let (_sf, range) = self.compiler.sess().source_map().span_to_source(source_span).unwrap(); - dbg!(source_span, &range, &_sf.src[range.clone()]); + let result = self + .compiler + .sess() + .source_map() + .span_to_source(source_span) + .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?; + let range = result.data; let offset = range.start as u32; let length = range.len() as u32; - let final_pc = deployed_bytecode - .source_map() + trace!(%offset, %length, "find pc"); + let final_pc = contract + .get_source_map_deployed() .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))?? .into_iter() - .zip(InstructionIter::new(deployed_bytecode_bytes)) + .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc)) .filter(|(s, _)| s.offset() == offset && s.length() == length) - .map(|(_, i)| i.pc) - .max() - .unwrap_or_default(); - Ok(Some(final_pc)) + .map(|(_, pc)| pc) + .max(); + trace!(?final_pc); + Ok(final_pc) } /// Statements' ranges in the solc source map do not include the semicolon. - fn stmt_span_without_semicolon(&self, stmt: &hir::Stmt<'_>) -> Span { + fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span { match stmt.kind { - hir::StmtKind::DeclSingle(id) => { + HirStmtKind::DeclSingle(id) => { let decl = self.gcx().hir.variable(id); if let Some(expr) = decl.initializer { stmt.span.with_hi(expr.span.hi()) @@ -347,23 +208,65 @@ impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> { stmt.span } } - hir::StmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), - hir::StmtKind::Expr(expr) => expr.span, + HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()), + HirStmtKind::Expr(expr) => expr.span, _ => stmt.span, } } - fn run_func_body(&self) -> hir::Block<'_> { - let c = self.repl_contract_hir().expect("REPL contract not found in HIR"); - let f = c - .functions() - .find(|&f| self.gcx().hir.function(f).name.as_ref().map(|n| n.as_str()) == Some("run")) - .expect("`run()` function not found in REPL contract"); - self.gcx().hir.function(f).body.expect("`run()` function does not have a body") + /// Returns the AST `run()` body of the REPL contract, if any. + /// + /// Yul/assembly is not yet lowered to HIR in the pinned solar version, so we + /// keep around the AST to be able to inspect inline assembly blocks. + fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> { + let contract = self.repl_contract_hir()?; + let source = self.gcx().sources.get(contract.source)?; + let ast = source.ast.as_ref()?; + + let contract_ast = ast.items.iter().find_map(|i| match &i.kind { + ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c), + _ => None, + })?; + contract_ast.body.iter().find_map(|i| match &i.kind { + ItemKind::Function(f) if f.header.name.is_some_and(|n| n.as_str() == "run") => { + f.body.as_ref() + } + _ => None, + }) } - fn repl_contract_hir(&self) -> Option<&hir::Contract<'_>> { - self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL") + /// Returns the span of the first top-level `return(...)` call inside any + /// `assembly { ... }` block in the REPL `run()` function, if any. + fn first_yul_return_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + for stmt in run_body.stmts.iter() { + let AstStmtKind::Assembly(asm) = &stmt.kind else { continue }; + for ystmt in asm.block.stmts.iter() { + if let yul::StmtKind::Expr(e) = &ystmt.kind + && let yul::ExprKind::Call(call) = &e.kind + && call.name.as_str() == "return" + { + return Some(ystmt.span); + } + } + } + None + } + + /// If the last statement of the REPL `run()` function is an `assembly { ... }` block, + /// returns the span of its last non-`let` (i.e. non-VarDecl) Yul statement. + /// + /// This mirrors the legacy behavior used to pick a meaningful end-of-function PC when + /// the trailing statement is inline assembly. + fn trailing_assembly_last_stmt_span(&self) -> Option { + let run_body = self.repl_run_ast_body()?; + let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None }; + asm.block + .stmts + .iter() + .rev() + .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _))) + .map(|s| s.span) } } @@ -585,8 +488,7 @@ impl SessionSource { return Ok(output); } let output = self.compile()?; - let intermediate = self.generate_intermediate_output()?; - let output = GeneratedOutput { output, intermediate }; + let output = GeneratedOutput { output }; Ok(self.output.get_or_init(|| output)) } @@ -603,12 +505,11 @@ impl SessionSource { eyre::bail!("{output}"); } - // TODO(dani): re-enable - if cfg!(false) { - output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { - let _ = c.lower_asts(); - }); - } + // Drive HIR lowering and analysis so that subsequent `enter` queries can use them. + output.parser_mut().solc_mut().compiler_mut().enter_mut(|c| { + let _ = c.lower_asts(); + let _ = c.analysis(); + }); Ok(output) } @@ -633,53 +534,6 @@ impl SessionSource { sources } - /// Generate intermediate contracts for all contract definitions in the compilation source. - /// - /// ### Returns - /// - /// Optionally, a map of contract names to a vec of [IntermediateContract]s. - pub fn generate_intermediate_contracts(&self) -> Result> { - let mut res_map = HashMap::default(); - let parsed_map = self.get_sources(); - for source in parsed_map.values() { - Self::get_intermediate_contract(&source.content, &mut res_map); - } - Ok(res_map) - } - - /// Generate intermediate output for the REPL contract - pub fn generate_intermediate_output(&self) -> Result { - // Parse generate intermediate contracts - let intermediate_contracts = self.generate_intermediate_contracts()?; - - // Construct variable definitions - let variable_definitions = intermediate_contracts - .get("REPL") - .ok_or_else(|| eyre::eyre!("Could not find intermediate REPL contract!"))? - .variable_definitions - .clone() - .into_iter() - .map(|(k, v)| (k, v.ty)) - .collect::>(); - // Construct intermediate output - let mut intermediate_output = IntermediateOutput { - repl_contract_expressions: variable_definitions, - intermediate_contracts, - }; - - // Add all statements within the run function to the repl_contract_expressions map - for (key, val) in intermediate_output - .run_func_body()? - .clone() - .iter() - .flat_map(Self::get_statement_definitions) - { - intermediate_output.repl_contract_expressions.insert(key, val); - } - - Ok(intermediate_output) - } - /// Construct the REPL source. pub fn to_repl_source(&self) -> String { let Self { @@ -742,108 +596,6 @@ contract {contract_name} {{ }); sess.dcx.emitted_errors().unwrap() } - - /// Gets the [IntermediateContract] for a Solidity source string and inserts it into the - /// passed `res_map`. In addition, recurses on any imported files as well. - /// - /// ### Takes - /// - `content` - A Solidity source string - /// - `res_map` - A mutable reference to a map of contract names to [IntermediateContract]s - pub fn get_intermediate_contract( - content: &str, - res_map: &mut HashMap, - ) { - if let Ok((pt::SourceUnit(source_unit_parts), _)) = solang_parser::parse(content, 0) { - let func_defs = source_unit_parts - .into_iter() - .filter_map(|sup| match sup { - pt::SourceUnitPart::ImportDirective(i) => match i { - pt::Import::Plain(s, _) - | pt::Import::Rename(s, _, _) - | pt::Import::GlobalSymbol(s, _, _) => { - let s = match s { - pt::ImportPath::Filename(s) => s.string, - pt::ImportPath::Path(p) => p.to_string(), - }; - let path = PathBuf::from(s); - - match fs::read_to_string(path) { - Ok(source) => { - Self::get_intermediate_contract(&source, res_map); - None - } - Err(_) => None, - } - } - }, - pt::SourceUnitPart::ContractDefinition(cd) => { - let mut intermediate = IntermediateContract::default(); - - cd.parts.into_iter().for_each(|part| match part { - pt::ContractPart::FunctionDefinition(def) => { - // Only match normal function definitions here. - if matches!(def.ty, pt::FunctionTy::Function) { - intermediate - .function_definitions - .insert(def.name.clone().unwrap().name, def); - } - } - pt::ContractPart::EventDefinition(def) => { - let event_name = def.name.safe_unwrap().name.clone(); - intermediate.event_definitions.insert(event_name, def); - } - pt::ContractPart::StructDefinition(def) => { - let struct_name = def.name.safe_unwrap().name.clone(); - intermediate.struct_definitions.insert(struct_name, def); - } - pt::ContractPart::VariableDefinition(def) => { - let var_name = def.name.safe_unwrap().name.clone(); - intermediate.variable_definitions.insert(var_name, def); - } - _ => {} - }); - Some((cd.name.safe_unwrap().name.clone(), intermediate)) - } - _ => None, - }) - .collect::>(); - res_map.extend(func_defs); - } - } - - /// Helper to deconstruct a statement - /// - /// ### Takes - /// - /// A reference to a [pt::Statement] - /// - /// ### Returns - /// - /// A vector containing tuples of the inner expressions' names, types, and storage locations. - pub fn get_statement_definitions(statement: &pt::Statement) -> Vec<(String, pt::Expression)> { - match statement { - pt::Statement::VariableDefinition(_, def, _) => { - vec![(def.name.safe_unwrap().name.clone(), def.ty.clone())] - } - pt::Statement::Expression(_, pt::Expression::Assign(_, left, _)) => { - if let pt::Expression::List(_, list) = left.as_ref() { - list.iter() - .filter_map(|(_, param)| { - param.as_ref().and_then(|param| { - param - .name - .as_ref() - .map(|name| (name.name.clone(), param.ty.clone())) - }) - }) - .collect() - } else { - Vec::default() - } - } - _ => Vec::default(), - } - } } /// A Parse Tree Fragment diff --git a/crates/chisel/tests/it/repl/mod.rs b/crates/chisel/tests/it/repl/mod.rs index 704d30405eed9..338b7d2043809 100644 --- a/crates/chisel/tests/it/repl/mod.rs +++ b/crates/chisel/tests/it/repl/mod.rs @@ -153,6 +153,26 @@ assembly { repl.expect("[0x00:0x20]"); }); +// Assembly as the final statement with a return — exercises the path where both +// `first_yul_return_span` and `trailing_assembly_last_stmt_span` resolve to the same `return(...)` +// span (no subsequent Solidity statement after the assembly block). +repl_test!(assembly_return_final, |repl| { + repl.sendln("uint x = 0xbeef;"); + repl.sendln("assembly { mstore(0x0, sload(0)) return(0x0, 0x20) }"); + repl.sendln("!md"); + repl.expect("[0x00:0x20]"); +}); + +// Assembly block without a `return(...)` call as an intermediate statement, exercises +// `first_yul_return_span` returning `None` while a subsequent Solidity statement is still evaluated +// correctly. +repl_test!(assembly_no_return_intermediate, |repl| { + repl.sendln("uint x = 1;"); + repl.sendln("assembly { x := add(x, 1) }"); + repl.sendln("x"); + repl.expect("Decimal: 2"); +}); + // Issue #5051, #8978: Test EVM version normalization. repl_test!(flaky_evm_version_normalization, "--use 0.7.6 --evm-version london", |repl| { repl.sendln("uint x;\nx"); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 37340f5f4cc3c..606b8291819e4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -52,6 +52,7 @@ rayon.workspace = true regex = { workspace = true, default-features = false } serde_json.workspace = true serde.workspace = true +toml.workspace = true strsim = "0.11" strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["macros"] } @@ -70,7 +71,13 @@ tempfile.workspace = true tikv-jemallocator = { workspace = true, optional = true } [features] +default = ["optimism"] tracy = ["dep:tracing-tracy"] tracy-allocator = ["tracy"] jemalloc = ["dep:tikv-jemallocator"] mimalloc = ["dep:mimalloc"] +optimism = [ + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-evm/optimism", +] diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index 87f14e2039606..4fc437c7232f8 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -307,6 +307,17 @@ mod tests { assert_eq!(val, &Value::from(1000u64)); } + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + use clap::CommandFactory; + + let command = EvmArgs::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + #[test] fn can_parse_chain_id() { let args = EvmArgs { diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 8c37860446683..f846da5002354 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -66,8 +66,20 @@ impl figment::Provider for RpcOpts { impl RpcOpts { /// Returns the RPC endpoint. pub fn url<'a>(&'a self, config: Option<&'a Config>) -> Result>> { + self.url_with_env(config, std::env::var("ETH_RPC_URL").ok()) + } + + fn url_with_env<'a>( + &'a self, + config: Option<&'a Config>, + env_url: Option, + ) -> Result>> { if self.flashbots { Ok(Some(Cow::Borrowed(FLASHBOTS_URL))) + } else if let Some(url) = self.common.rpc_url.as_deref() { + Ok(Some(Cow::Borrowed(url))) + } else if let Some(url) = env_url { + Ok(Some(Cow::Owned(url))) } else { self.common.url(config) } @@ -85,8 +97,10 @@ impl RpcOpts { pub fn dict(&self) -> Dict { let mut dict = self.common.dict(); - if self.flashbots { - dict.insert("eth_rpc_url".into(), FLASHBOTS_URL.into()); + // `self.url(None)` already accounts for `flashbots` and the `ETH_RPC_URL` env var, + // so a single insert here covers both. + if let Ok(Some(url)) = self.url(None) { + dict.insert("eth_rpc_url".into(), url.into_owned().into()); } if let Ok(Some(jwt)) = self.jwt(None) { dict.insert("eth_rpc_jwt".into(), jwt.into_owned().into()); @@ -199,6 +213,7 @@ impl figment::Provider for EthereumOpts { #[cfg(test)] mod tests { use super::*; + use clap::CommandFactory; #[test] fn parse_etherscan_opts() { @@ -223,4 +238,41 @@ mod tests { let id: u64 = chain_id.deserialize().expect("chain_id should deserialize as u64"); assert_eq!(id, 9745); } + + #[test] + fn rpc_url_arg_does_not_read_eth_rpc_url_env() { + let command = RpcOpts::command(); + let rpc_url = + command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg"); + + assert!(rpc_url.get_env().is_none()); + } + + #[test] + fn rpc_url_resolves_eth_rpc_url_env() { + let args = RpcOpts::default(); + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8545"); + } + + #[test] + fn explicit_rpc_url_takes_precedence_over_eth_rpc_url_env() { + let args = RpcOpts { + common: RpcCommonOpts { + rpc_url: Some("http://127.0.0.1:8546".to_string()), + ..Default::default() + }, + ..Default::default() + }; + let url = args + .url_with_env(None, Some("http://127.0.0.1:8545".to_string())) + .expect("url") + .expect("url"); + + assert_eq!(url.as_ref(), "http://127.0.0.1:8546"); + } } diff --git a/crates/cli/src/opts/rpc_common.rs b/crates/cli/src/opts/rpc_common.rs index 05b98582fa88f..6a5fe5ed4e9e4 100644 --- a/crates/cli/src/opts/rpc_common.rs +++ b/crates/cli/src/opts/rpc_common.rs @@ -17,10 +17,15 @@ use std::borrow::Cow; /// This struct holds fields that both [`super::RpcOpts`] (cast) and /// [`super::EvmArgs`] (forge/script) need, eliminating duplication and /// making the two structs composable. +/// +/// Note: `ETH_RPC_URL` is intentionally **not** bound here as a clap env +/// fallback; otherwise it would be inherited by `EvmArgs` and silently +/// fork all `forge test` runs. Cast resolves `ETH_RPC_URL` explicitly +/// at the call site (see [`super::RpcOpts::url`]). #[derive(Clone, Debug, Default, Serialize, Parser)] pub struct RpcCommonOpts { /// The RPC endpoint. - #[arg(short, long, visible_alias = "fork-url", env = "ETH_RPC_URL")] + #[arg(short, long, visible_alias = "fork-url", value_name = "URL")] #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")] pub rpc_url: Option, diff --git a/crates/cli/src/opts/tempo.rs b/crates/cli/src/opts/tempo.rs index 8c2a12e661e18..88119f163b325 100644 --- a/crates/cli/src/opts/tempo.rs +++ b/crates/cli/src/opts/tempo.rs @@ -1,16 +1,26 @@ use alloy_network::{Network, TransactionBuilder}; use alloy_primitives::{Address, ruint::aliases::U256}; -use alloy_signer::Signature; +use alloy_signer::{Signature, Signer}; use clap::Parser; -use foundry_common::FoundryTransactionBuilder; -use std::{num::NonZeroU64, str::FromStr}; +use eyre::Result; +use foundry_common::{ + FoundryTransactionBuilder, + tempo::{TempoSponsor, resolve_tempo_sponsor_signer}, +}; +use std::{ + num::NonZeroU64, + path::PathBuf, + str::FromStr, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::utils::parse_fee_token_address; -/// CLI options for Tempo transactions. +/// CLI options common to Tempo transactions across commands. #[derive(Clone, Debug, Default, Parser)] #[command(next_help_heading = "Tempo")] -pub struct TempoOpts { +pub struct TempoCommonOpts { /// Fee token address for Tempo transactions. /// /// When set, builds a Tempo (type 0x76) transaction that pays gas fees @@ -21,6 +31,40 @@ pub struct TempoOpts { #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] pub fee_token: Option
, + /// Opt into TIP-1009 expiring-nonce mode with a validity window. + /// + /// Convenience flag that combines `--tempo.expiring-nonce` with a relative + /// `--tempo.valid-before`. Sets nonce_key = U256::MAX, nonce = 0, and valid_before = now + + /// seconds. + /// + /// Maximum value is 30 seconds. The transaction must be mined before the deadline or it + /// becomes permanently invalid, giving safe retry semantics: retries produce a fresh tx hash + /// and the old tx can never land late. + #[arg(long = "tempo.expires", value_name = "SECONDS", value_parser = parse_expires_seconds)] + pub expires: Option, +} + +impl TempoCommonOpts { + /// Returns `true` if any Tempo-specific option is set. + pub const fn is_tempo(&self) -> bool { + self.fee_token.is_some() || self.expires.is_some() + } + + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + let secs = self.expires?; + let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards"); + Some(now.as_secs() + secs) + } +} + +/// CLI options for Tempo transactions. +#[derive(Clone, Debug, Default, Parser)] +#[command(next_help_heading = "Tempo")] +pub struct TempoOpts { + #[command(flatten)] + pub common: TempoCommonOpts, + /// Nonce key for Tempo parallelizable nonces. /// /// When set, builds a Tempo (type 0x76) transaction with the specified nonce key, @@ -28,21 +72,69 @@ pub struct TempoOpts { /// to be executed in parallel. If not set, the protocol nonce key (0) will be used. /// /// For more information see . - #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY")] + #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY", conflicts_with = "lane")] pub nonce_key: Option, + /// Named nonce lane for Tempo parallelizable nonces. + /// + /// Resolves a friendly lane name (e.g. `deploy`, `payments`) to a `nonce_key` via a + /// shared lanes file (default: `tempo.lanes.toml` at the project root). The lanes file + /// is a TOML map of `name = ` entries, e.g.: + /// + /// ```toml + /// deploy = 1 + /// ops = 2 + /// payments = 3 + /// ``` + /// + /// Mutually exclusive with `--tempo.nonce-key`. + #[arg(long = "tempo.lane", value_name = "NAME")] + pub lane: Option, + + /// Path to the Tempo lanes file used by `--tempo.lane`. + /// + /// Defaults to `tempo.lanes.toml` at the project root. + #[arg(long = "tempo.lanes-file", value_name = "PATH")] + pub lanes_file: Option, + + /// Sponsor (fee payer) address for Tempo sponsored transactions. + #[arg(long = "tempo.sponsor", value_name = "ADDRESS")] + pub sponsor: Option
, + + /// Sign Tempo sponsor digests in-band with the given signer URI. + /// + /// Supported forms include `env://VAR`, `keystore://PATH`, `account://NAME`, + /// `ledger://`, `trezor://`, `aws://`, `gcp://`, `turnkey://`, and + /// `private-key://KEY`. + #[arg( + long = "tempo.sponsor-signer", + value_name = "SIGNER", + requires = "sponsor", + conflicts_with = "sponsor_sig" + )] + pub sponsor_signer: Option, + /// Sponsor (fee payer) signature for Tempo sponsored transactions. /// /// The sponsor signs the `fee_payer_signature_hash` to commit to paying gas fees /// on behalf of the sender. Provide as a hex-encoded signature. - #[arg(long = "tempo.sponsor-signature", value_parser = parse_signature)] - pub sponsor_signature: Option, + #[arg( + long = "tempo.sponsor-sig", + alias = "tempo.sponsor-signature", + value_parser = parse_signature, + requires = "sponsor", + conflicts_with = "sponsor_signer" + )] + pub sponsor_sig: Option, /// Print the sponsor signature hash and exit. /// /// Computes the `fee_payer_signature_hash` for the transaction so that a sponsor /// knows what hash to sign. The transaction is not sent. - #[arg(long = "tempo.print-sponsor-hash")] + #[arg( + long = "tempo.print-sponsor-hash", + conflicts_with_all = &["sponsor", "sponsor_signer", "sponsor_sig"] + )] pub print_sponsor_hash: bool, /// Access key ID for Tempo Keychain signature transactions. @@ -56,14 +148,14 @@ pub struct TempoOpts { /// /// Sets nonce to 0 and nonce_key to U256::MAX, enabling time-bounded transaction /// validity via `--tempo.valid-before` and `--tempo.valid-after`. - #[arg(long = "tempo.expiring-nonce", requires = "valid_before")] + #[arg(long = "tempo.expiring-nonce", requires = "valid_before", conflicts_with = "expires")] pub expiring_nonce: bool, /// Upper bound timestamp for Tempo expiring nonce transactions. /// /// The transaction is only valid before this unix timestamp. /// Requires `--tempo.expiring-nonce`. - #[arg(long = "tempo.valid-before")] + #[arg(long = "tempo.valid-before", conflicts_with = "expires")] pub valid_before: Option, /// Lower bound timestamp for Tempo expiring nonce transactions. @@ -77,9 +169,12 @@ pub struct TempoOpts { impl TempoOpts { /// Returns `true` if any Tempo-specific option is set. pub const fn is_tempo(&self) -> bool { - self.fee_token.is_some() + self.common.is_tempo() || self.nonce_key.is_some() - || self.sponsor_signature.is_some() + || self.lane.is_some() + || self.sponsor.is_some() + || self.sponsor_signer.is_some() + || self.sponsor_sig.is_some() || self.print_sponsor_hash || self.key_id.is_some() || self.expiring_nonce @@ -87,6 +182,58 @@ impl TempoOpts { || self.valid_after.is_some() } + /// Returns the absolute `valid_before` unix timestamp derived from `--tempo.expires`, if set. + pub fn expires_at(&self) -> Option { + self.common.expires_at() + } + + /// Resolves `--tempo.expires` into concrete expiring-nonce fields. + /// + /// This computes the relative deadline once so later calls to [`Self::apply`] reuse the same + /// `valid_before` timestamp instead of deriving a fresh one. + pub fn resolve_expires(&mut self) -> Option { + let ts = self.expires_at()?; + self.expiring_nonce = true; + self.valid_before = Some(ts); + self.common.expires = None; + Some(ts) + } + + /// Returns `true` if a sponsor signature should be attached before submission. + pub const fn has_sponsor_submission(&self) -> bool { + self.sponsor.is_some() || self.sponsor_signer.is_some() || self.sponsor_sig.is_some() + } + + /// Resolves sponsor CLI options into a reusable sponsor config for transaction submission. + pub async fn sponsor_config(&self) -> Result> { + let Some(sponsor) = self.sponsor else { + return Ok(None); + }; + + let signer = if let Some(spec) = &self.sponsor_signer { + Some(Arc::new(Box::pin(resolve_tempo_sponsor_signer(spec)).await?)) + } else { + None + }; + + if let Some(signer) = &signer { + let signer_address = signer.address(); + if signer_address != sponsor { + eyre::bail!( + "Tempo sponsor signer address {signer_address} does not match --tempo.sponsor {sponsor}" + ); + } + } + + if signer.is_none() && self.sponsor_sig.is_none() { + eyre::bail!( + "--tempo.sponsor requires either --tempo.sponsor-signer or --tempo.sponsor-sig" + ); + } + + Ok(Some(TempoSponsor::new(sponsor, signer, self.sponsor_sig))) + } + /// Applies Tempo-specific options to a transaction request. /// /// All setters are no-ops for non-Tempo networks, so this is safe to call unconditionally. @@ -94,8 +241,9 @@ impl TempoOpts { where N::TransactionRequest: FoundryTransactionBuilder, { - // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX - if self.expiring_nonce { + // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX. + // --tempo.expires is a convenience alias that also sets valid_before = now + duration. + if self.expiring_nonce || self.common.expires.is_some() { tx.set_nonce(0); tx.set_nonce_key(U256::MAX); } else { @@ -107,11 +255,14 @@ impl TempoOpts { } } - if let Some(fee_token) = self.fee_token { + if let Some(fee_token) = self.common.fee_token { tx.set_fee_token(fee_token); } - if let Some(valid_before) = self.valid_before + // --tempo.expires sets valid_before relative to now; --tempo.valid-before takes a raw + // unix timestamp. The two flags are mutually exclusive (enforced by clap). + let effective_valid_before = self.expires_at().or(self.valid_before); + if let Some(valid_before) = effective_valid_before && let Some(v) = NonZeroU64::new(valid_before) { tx.set_valid_before(v); @@ -131,8 +282,7 @@ impl TempoOpts { // gas estimation so that `--tempo.print-sponsor-hash` and // `--tempo.sponsor-signature` produce identical gas estimates. Callers // should call `set_fee_payer_signature` on the built tx request. - if (self.sponsor_signature.is_some() || self.print_sponsor_hash) && tx.nonce_key().is_none() - { + if (self.has_sponsor_submission() || self.print_sponsor_hash) && tx.nonce_key().is_none() { tx.set_nonce_key(U256::ZERO); } } @@ -142,11 +292,83 @@ fn parse_signature(s: &str) -> Result { Signature::from_str(s).map_err(|e| format!("invalid signature: {e}")) } +/// Parses a seconds value for `--tempo.expires`, capped at the protocol maximum of 30 seconds. +fn parse_expires_seconds(s: &str) -> Result { + let secs: u64 = s + .parse() + .map_err(|_| format!("invalid value '{s}': expected an integer number of seconds"))?; + if secs > 30 { + return Err(format!("expires must be at most 30 seconds (got {secs})")); + } + Ok(secs) +} + #[cfg(test)] mod tests { use super::*; use alloy_primitives::address; + #[test] + fn parses_lane_arg() { + let opts = TempoOpts::try_parse_from(["", "--tempo.lane", "deploy"]).unwrap(); + assert_eq!(opts.lane.as_deref(), Some("deploy")); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn lane_conflicts_with_nonce_key() { + let err = + TempoOpts::try_parse_from(["", "--tempo.lane", "deploy", "--tempo.nonce-key", "1"]) + .unwrap_err(); + assert!( + err.to_string().contains("cannot be used with"), + "expected clap conflict error, got: {err}", + ); + } + + #[test] + fn parse_expires_flag() { + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "30"]).unwrap(); + assert_eq!(opts.common.expires, Some(30)); + + let opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + assert_eq!(opts.common.expires, Some(10)); + + // exceeds 30s maximum + assert!(TempoOpts::try_parse_from(["", "--tempo.expires", "31"]).is_err()); + + // conflicts with --tempo.expiring-nonce + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.expires", + "30", + "--tempo.expiring-nonce", + "--tempo.valid-before", + "999" + ]) + .is_err() + ); + } + + #[test] + fn resolve_expires_materializes_valid_before() { + let before = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + let mut opts = TempoOpts::try_parse_from(["", "--tempo.expires", "10"]).unwrap(); + + let resolved = opts.resolve_expires().unwrap(); + let after = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards").as_secs(); + + assert!(resolved >= before + 10); + assert!(resolved <= after + 10); + assert!(opts.expiring_nonce); + assert_eq!(opts.valid_before, Some(resolved)); + assert_eq!(opts.common.expires, None); + assert_eq!(opts.expires_at(), None); + } + #[test] fn parse_fee_token_id() { let opts = TempoOpts::try_parse_from([ @@ -155,13 +377,69 @@ mod tests { "0x20C0000000000000000000000000000000000002", ]) .unwrap(); - assert_eq!(opts.fee_token, Some(address!("0x20C0000000000000000000000000000000000002")),); + assert_eq!( + opts.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000002")), + ); // AlphaUSD token ID is 1u64 let opts_with_id = TempoOpts::try_parse_from(["", "--tempo.fee-token", "1"]).unwrap(); assert_eq!( - opts_with_id.fee_token, + opts_with_id.common.fee_token, Some(address!("0x20C0000000000000000000000000000000000001")), ); } + + #[test] + fn parse_sponsor_signer() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert_eq!(opts.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + assert!(opts.sponsor_sig.is_none()); + assert!(opts.is_tempo()); + assert!(opts.has_sponsor_submission()); + } + + #[test] + fn sponsor_signer_requires_sponsor() { + assert!( + TempoOpts::try_parse_from(["", "--tempo.sponsor-signer", "env://SPONSOR"]).is_err() + ); + } + + #[test] + fn parse_sponsor_signature_alias() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signature", + "0x0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b", + ]) + .unwrap(); + + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + assert!(opts.sponsor_sig.is_some()); + } + + #[test] + fn print_sponsor_hash_conflicts_with_sponsor_submission() { + assert!( + TempoOpts::try_parse_from([ + "", + "--tempo.print-sponsor-hash", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + ]) + .is_err() + ); + } } diff --git a/crates/cli/src/utils/tempo.rs b/crates/cli/src/utils/tempo.rs index 647f52d316a6c..4b5715b9ebe08 100644 --- a/crates/cli/src/utils/tempo.rs +++ b/crates/cli/src/utils/tempo.rs @@ -1,8 +1,44 @@ -use std::str::FromStr; +//! Tempo utilities: fee token parsing and named nonce lanes (2D nonces). +//! +//! A "lane" is a friendly alias for a Tempo `nonce_key` (a [`U256`]). Lanes are defined in a +//! shared TOML file (default `tempo.lanes.toml` at the project root) so a team can reserve +//! independent sequential nonce streams for parallel scripts without coordinating on raw +//! `U256` selectors. +//! +//! Example `tempo.lanes.toml`: +//! +//! ```toml +//! deploy = 1 +//! ops = 2 +//! payments = 3 +//! ``` +//! +//! ```bash +//! cast erc20 transfer ... --tempo.lane payments +//! ``` -use alloy_primitives::Address; +use crate::opts::TempoOpts; +use alloy_primitives::{Address, U256}; +use eyre::{Result, eyre}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + str::FromStr, +}; use tempo_primitives::TempoAddressExt; +/// Default name of the lanes file at the project root. +pub const DEFAULT_LANES_FILE: &str = "tempo.lanes.toml"; + +/// Result of resolving a `--tempo.lane ` argument against a lanes file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedLane { + /// The lane name as provided on the CLI. + pub name: String, + /// The `nonce_key` the lane resolved to. + pub nonce_key: U256, +} + /// Parses a fee token address. pub fn parse_fee_token_address(address_or_id: &str) -> eyre::Result
{ Address::from_str(address_or_id).or_else(|_| Ok(token_id_to_address(address_or_id.parse()?))) @@ -14,3 +50,156 @@ fn token_id_to_address(token_id: u64) -> Address { address_bytes[12..20].copy_from_slice(&token_id.to_be_bytes()); Address::from(address_bytes) } + +/// Loads a TOML lanes file from `path`. +/// +/// Each top-level key is a lane name, and the value is the `nonce_key` (an integer or a +/// decimal/hex string parsed as [`U256`]). +pub fn load_lanes(path: &Path) -> Result> { + let contents = std::fs::read_to_string(path) + .map_err(|e| eyre!("failed to read tempo lanes file {}: {}", path.display(), e))?; + parse_lanes(&contents) + .map_err(|e| eyre!("failed to parse tempo lanes file {}: {}", path.display(), e)) +} + +fn parse_lanes(contents: &str) -> Result> { + let raw: BTreeMap = toml::from_str(contents)?; + let mut out = BTreeMap::new(); + for (name, value) in raw { + let nonce_key = match value { + toml::Value::Integer(n) => { + if n < 0 { + return Err(eyre!("invalid nonce_key for lane '{name}': must be non-negative")); + } + U256::from(n as u64) + } + toml::Value::String(s) => U256::from_str(s.trim()) + .map_err(|e| eyre!("invalid nonce_key for lane '{name}': {e}"))?, + other => { + return Err(eyre!( + "invalid nonce_key for lane '{name}': expected integer or string, got {}", + other.type_str(), + )); + } + }; + out.insert(name, nonce_key); + } + Ok(out) +} + +/// Resolves `opts.lane` against a lanes file and writes the resulting `nonce_key` to +/// `opts.nonce_key`. Returns the resolved lane (or `None` if no `--tempo.lane` was set). +/// +/// `root` is the project root used to locate the default lanes file +/// (`/tempo.lanes.toml`) when `--tempo.lanes-file` was not provided. +pub fn resolve_lane(opts: &mut TempoOpts, root: &Path) -> Result> { + let Some(lane_name) = opts.lane.clone() else { return Ok(None) }; + + let path: PathBuf = opts.lanes_file.clone().unwrap_or_else(|| root.join(DEFAULT_LANES_FILE)); + + if !path.exists() { + return Err(eyre!( + "tempo lanes file not found at {}\n\ + create it with `name = ` entries, e.g.:\n \ + deploy = 1\n \ + ops = 2\n \ + payments = 3", + path.display(), + )); + } + + let lanes = load_lanes(&path)?; + + let nonce_key = lanes.get(&lane_name).copied().ok_or_else(|| { + let mut known: Vec<&str> = lanes.keys().map(String::as_str).collect(); + known.sort_unstable(); + eyre!( + "lane '{lane_name}' not found in {} (known lanes: {})", + path.display(), + if known.is_empty() { "".to_string() } else { known.join(", ") }, + ) + })?; + + opts.nonce_key = Some(nonce_key); + Ok(Some(ResolvedLane { name: lane_name, nonce_key })) +} + +/// Prints `lane: (nonce_key=, nonce=)` to stderr (so it doesn't pollute +/// stdout for commands like `cast mktx` whose stdout is meant to be piped), giving +/// visibility into which 2D nonce lane was used. +pub fn maybe_print_resolved_lane(resolved: Option<&ResolvedLane>, nonce: u64) -> Result<()> { + if let Some(lane) = resolved { + sh_eprintln!("lane: {} (nonce_key={}, nonce={})", lane.name, lane.nonce_key, nonce)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_int_and_string_lane_values() { + let toml = r#" +deploy = 1 +ops = 2 +payments = "3" +big = "115792089237316195423570985008687907853269984665640564039457584007913129639935" +"#; + let lanes = parse_lanes(toml).unwrap(); + assert_eq!(lanes.get("deploy"), Some(&U256::from(1u64))); + assert_eq!(lanes.get("ops"), Some(&U256::from(2u64))); + assert_eq!(lanes.get("payments"), Some(&U256::from(3u64))); + assert_eq!(lanes.get("big"), Some(&U256::MAX)); + } + + #[test] + fn parse_lanes_rejects_invalid_string() { + let toml = "broken = \"not-a-number\""; + let err = parse_lanes(toml).unwrap_err(); + assert!(err.to_string().contains("invalid nonce_key for lane 'broken'")); + } + + #[test] + fn resolve_lane_sets_nonce_key_and_returns_resolved() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 7\npayments = 42\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let resolved = resolve_lane(&mut opts, dir.path()).unwrap().unwrap(); + assert_eq!(resolved.name, "payments"); + assert_eq!(resolved.nonce_key, U256::from(42u64)); + assert_eq!(opts.nonce_key, Some(U256::from(42u64))); + } + + #[test] + fn resolve_lane_returns_none_when_no_lane() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts::default(); + let resolved = resolve_lane(&mut opts, dir.path()).unwrap(); + assert!(resolved.is_none()); + assert!(opts.nonce_key.is_none()); + } + + #[test] + fn resolve_lane_errors_when_file_missing() { + let dir = tempfile::tempdir().unwrap(); + let mut opts = TempoOpts { lane: Some("deploy".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + assert!(err.to_string().contains("tempo lanes file not found")); + } + + #[test] + fn resolve_lane_errors_when_lane_unknown() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(DEFAULT_LANES_FILE); + std::fs::write(&path, "deploy = 1\nops = 2\n").unwrap(); + + let mut opts = TempoOpts { lane: Some("payments".to_string()), ..Default::default() }; + let err = resolve_lane(&mut opts, dir.path()).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("lane 'payments' not found")); + assert!(msg.contains("deploy, ops")); + } +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 337c51f52d2c2..6921faabcb102 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -35,7 +35,7 @@ alloy-signer.workspace = true alloy-pubsub.workspace = true alloy-rpc-client.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "engine"] } -alloy-rpc-types-engine = { workspace = true, features = ["jwt"] } +alloy-rpc-types-engine = { workspace = true, features = ["jwt-aws-lc-rs"] } alloy-sol-types.workspace = true alloy-transport-ipc.workspace = true alloy-transport-ws.workspace = true @@ -44,8 +44,8 @@ alloy-transport.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true -op-alloy-network.workspace = true -op-alloy-rpc-types.workspace = true +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } revm.workspace = true @@ -87,6 +87,10 @@ mpp.workspace = true foundry-wallets = { workspace = true, features = ["browser", "tempo"] } tokio-tungstenite.workspace = true futures.workspace = true +alloy-signer-local.workspace = true +base64.workspace = true +sha2 = "0.10" +tempfile.workspace = true [build-dependencies] chrono.workspace = true @@ -96,4 +100,12 @@ vergen = { workspace = true, features = ["build", "emit_and_set"] } foundry-evm-hardforks.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } axum = { workspace = true } -tempfile.workspace = true +k256 = { workspace = true } + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "foundry-common-fmt/optimism", +] diff --git a/crates/common/build.rs b/crates/common/build.rs index d89e23be850f4..9afa01b5757ef 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -15,16 +15,13 @@ fn main() -> Result<(), Box> { let sha_short = &sha[..10]; let tag_name = try_env_var("TAG_NAME").unwrap_or_else(|| String::from("dev")); - let is_nightly = tag_name.contains("nightly"); - let version_suffix = if is_nightly { "nightly" } else { &tag_name }; + let version = release_version(&env_var("CARGO_PKG_VERSION"), &tag_name); + let is_nightly = tag_name.starts_with("nightly"); if is_nightly { println!("cargo:rustc-env=FOUNDRY_IS_NIGHTLY_VERSION=true"); } - let pkg_version = env_var("CARGO_PKG_VERSION"); - let version = format!("{pkg_version}-{version_suffix}"); - // `PROFILE` captures only release or debug. Get the actual name from the out directory. let out_dir = PathBuf::from(env_var("OUT_DIR")); let profile = out_dir.components().rev().nth(3).unwrap().as_os_str().to_str().unwrap(); @@ -87,6 +84,19 @@ fn env_var(name: &str) -> String { try_env_var(name).unwrap() } +fn release_version(pkg_version: &str, tag_name: &str) -> String { + if let Some(version) = tag_name.strip_prefix('v') { + return version.to_owned(); + } + + // Normalize `nightly-` to `nightly` so tarball and Docker nightly + // artifacts produce the same version string. The commit identifier is + // already included in the SemVer build metadata (after `+`). + let normalized = if tag_name.starts_with("nightly-") { "nightly" } else { tag_name }; + + format!("{pkg_version}-{normalized}") +} + fn try_env_var(name: &str) -> Option { println!("cargo:rerun-if-env-changed={name}"); std::env::var(name).ok() diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 2c8e16bccdcc6..179c71048da5b 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -20,10 +20,10 @@ eyre.workspace = true # ui alloy-consensus.workspace = true -op-alloy-consensus.workspace = true +op-alloy-consensus = { workspace = true, optional = true } alloy-network.workspace = true alloy-rpc-types = { workspace = true, features = ["eth"] } -op-alloy-rpc-types.workspace = true +op-alloy-rpc-types = { workspace = true, optional = true } alloy-serde.workspace = true serde.workspace = true serde_json.workspace = true @@ -38,3 +38,7 @@ tempo-alloy.workspace = true [dev-dependencies] foundry-macros.workspace = true similar-asserts.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:op-alloy-consensus", "dep:op-alloy-rpc-types"] diff --git a/crates/common/fmt/src/ui.rs b/crates/common/fmt/src/ui.rs index e883810dcda34..2087a85236154 100644 --- a/crates/common/fmt/src/ui.rs +++ b/crates/common/fmt/src/ui.rs @@ -18,6 +18,7 @@ use alloy_rpc_types::{ AccessListItem, Block, BlockTransactions, Header, Log, Transaction, TransactionReceipt, }; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{OpTxEnvelope, TxDeposit, TxPostExec}; use revm::context_interface::transaction::SignedAuthorization; use serde::Deserialize; @@ -448,6 +449,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxDeposit { fn pretty(&self) -> String { format!( @@ -472,6 +474,7 @@ input {}", } } +#[cfg(feature = "optimism")] impl UIfmt for TxPostExec { fn pretty(&self) -> String { format!( @@ -606,6 +609,7 @@ type {:#x} } } +#[cfg(feature = "optimism")] impl UIfmt for OpTxEnvelope { fn pretty(&self) -> String { match self { @@ -651,6 +655,7 @@ effectiveGasPrice {} } } +#[cfg(feature = "optimism")] impl UIfmt for op_alloy_rpc_types::Transaction { fn pretty(&self) -> String { format!( @@ -786,6 +791,7 @@ impl UIfmtSignatureExt for AnyTxEnvelope { } } +#[cfg(feature = "optimism")] impl UIfmtSignatureExt for OpTxEnvelope { fn signature_pretty(&self) -> Option<(String, String, String)> { self.signature().map(|sig| { @@ -1135,6 +1141,7 @@ mod tests { assert_eq!(b.pretty(), b32.pretty()); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx() { let s = r#" @@ -1186,6 +1193,7 @@ yParity 1 ); } + #[cfg(feature = "optimism")] #[test] fn can_pretty_print_optimism_tx_through_any() { let s = r#" diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 895b16b3b4532..95c7d4083f34e 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -383,16 +383,14 @@ impl ContractsByArtifact { &self, id: &str, ) -> Result>> { - let contracts = self - .iter() - .filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id) - .collect::>(); - - if contracts.len() > 1 { + let mut iter = + self.iter().filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id); + let first = iter.next(); + if first.is_some() && iter.next().is_some() { eyre::bail!("{id} has more than one implementation."); } - Ok(contracts.first().copied()) + Ok(first) } /// Finds abi by name or source path @@ -411,7 +409,7 @@ impl ContractsByArtifact { let mut funcs = BTreeMap::new(); let mut events = BTreeMap::new(); let mut errors_abi = JsonAbi::new(); - for (_name, contract) in self.iter() { + for contract in self.values() { for func in contract.abi.functions() { funcs.insert(func.selector(), func.clone()); } diff --git a/crates/common/src/provider/mpp/keys.rs b/crates/common/src/provider/mpp/keys.rs index 65640c48ab841..fa0fc80ed3d03 100644 --- a/crates/common/src/provider/mpp/keys.rs +++ b/crates/common/src/provider/mpp/keys.rs @@ -7,6 +7,7 @@ use crate::tempo::{TEMPO_PRIVATE_KEY_ENV, WalletType, read_tempo_keys_file}; use alloy_primitives::Address; +use std::env; use tracing::debug; /// Options for MPP key discovery filtering. @@ -55,7 +56,7 @@ pub fn discover_mpp_key() -> Option { /// target chain and the required currency. pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 1. Check TEMPO_PRIVATE_KEY env var (no keychain metadata available) - if let Ok(key) = std::env::var(TEMPO_PRIVATE_KEY_ENV) { + if let Ok(key) = env::var(TEMPO_PRIVATE_KEY_ENV) { let key = key.trim().to_string(); if !key.is_empty() { debug!("using MPP key from {TEMPO_PRIVATE_KEY_ENV} env var"); @@ -73,11 +74,17 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { // 2. Read $TEMPO_HOME/wallet/keys.toml (default: ~/.tempo/wallet/keys.toml) let keys_file = read_tempo_keys_file()?; + // `expiry == 0` means "no expiry" on the wire. + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // Pick primary key using the same deterministic order as // `Keystore::primary_key()` in tempo-common: // passkey > first entry with inline key > first entry // Only entries with a usable inline key can provide a signing key. - // Filter by chain_id and currency when provided. + // Filter by chain_id, currency, and freshness when provided. let candidates: Vec<_> = keys_file .keys .iter() @@ -86,6 +93,7 @@ pub fn discover_mpp_config(opts: DiscoverOptions) -> Option { opts.currency .is_none_or(|cur| k.limits.is_empty() || k.limits.iter().any(|l| l.currency == cur)) }) + .filter(|k| k.expiry.is_none_or(|e| e == 0 || e > now)) .collect(); let primary = candidates @@ -135,6 +143,7 @@ mod tests { #[test] fn discover_from_tempo_home_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( r#" @@ -160,6 +169,7 @@ chain_id = 4217 #[test] fn discover_env_var_takes_priority_over_keys_toml() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let file_key = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; let env_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; let toml_content = format!( @@ -187,6 +197,7 @@ key = "{file_key}" #[test] fn discover_returns_none_when_no_keys() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let (dir, _) = setup_keys_toml(""); unsafe { @@ -202,6 +213,7 @@ key = "{file_key}" #[test] fn discover_skips_entries_without_inline_key() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let toml_content = format!( r#" @@ -344,6 +356,7 @@ key = "0xthe_key" #[test] fn discover_filters_by_chain_id() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); let mainnet_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let testnet_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; let toml_content = format!( @@ -416,6 +429,62 @@ chain_id = 4217 unsafe { std::env::remove_var("TEMPO_HOME") }; } + #[test] + fn discover_filters_expired_entries() { + // Expired entries must not be selected, so the next 402 re-triggers + // the device-code flow instead of returning a stale key. + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let expired_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let fresh_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + let toml_content = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 + +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000002" +key = "{fresh_key}" +chain_id = 4217 +expiry = 0 +"# + ); + let (dir, _) = setup_keys_toml(&toml_content); + unsafe { + std::env::set_var("TEMPO_HOME", dir.path()); + std::env::remove_var("TEMPO_PRIVATE_KEY"); + } + + // Even though the expired entry comes first, discovery skips it. + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert_eq!(config.as_ref().unwrap().key, fresh_key); + + // With only the expired entry present, discovery returns None so the + // 402 path can run `ensure_access_key` again. + let only_expired = format!( + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "{expired_key}" +chain_id = 4217 +expiry = 1 +"# + ); + let (dir2, _) = setup_keys_toml(&only_expired); + unsafe { std::env::set_var("TEMPO_HOME", dir2.path()) }; + let config = + discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() }); + assert!(config.is_none(), "expired-only keys.toml must not yield a usable key"); + + unsafe { std::env::remove_var("TEMPO_HOME") }; + } + #[test] fn parse_keys_toml_unknown_fields_ignored() { let toml_str = r#" diff --git a/crates/common/src/provider/mpp/session.rs b/crates/common/src/provider/mpp/session.rs index 334166b844613..c3e87f8cf42b5 100644 --- a/crates/common/src/provider/mpp/session.rs +++ b/crates/common/src/provider/mpp/session.rs @@ -175,6 +175,16 @@ impl SessionProvider { self } + /// Address that funds payments for this provider. + pub fn funding_wallet_address(&self) -> Address { + self.signing_mode.from_address(self.signer.address()) + } + + /// Chain ID from the selected wallet key, when known. + pub const fn key_chain_id(&self) -> Option { + self.key_chain_id + } + /// Set the chain ID and currencies from the key entry used to initialize /// this provider. Used to reject challenges for incompatible chains/currencies. /// When `chain_id` is `None` (e.g. env var key), chain filtering is skipped. diff --git a/crates/common/src/provider/mpp/transport.rs b/crates/common/src/provider/mpp/transport.rs index 67354dc2bd60d..9e3b16dedd59e 100644 --- a/crates/common/src/provider/mpp/transport.rs +++ b/crates/common/src/provider/mpp/transport.rs @@ -4,6 +4,7 @@ //! handling via the MPP protocol. When the RPC endpoint returns a 402 response, //! this transport automatically pays the challenge and retries the request. +use alloy_chains::Chain; use alloy_json_rpc::{RequestPacket, ResponsePacket}; use alloy_transport::{TransportError, TransportErrorKind, TransportFut, TransportResult}; use mpp::{ @@ -16,12 +17,17 @@ use mpp::{ use reqwest::{StatusCode, header::HeaderMap}; use std::{ collections::HashMap, - fmt, - sync::{Mutex, OnceLock}, + env, fmt, io, + io::IsTerminal, + process::{Command, Stdio}, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicBool, Ordering}, + }, task, time::Duration, }; -use tokio::sync::OwnedMutexGuard; +use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard}; use tower::Service; use tracing::{Instrument, debug, debug_span, trace}; use url::Url; @@ -39,7 +45,27 @@ const MPP_RETRY_TIMEOUT: Duration = Duration::from_secs(120); /// Resolve the deposit amount from `MPP_DEPOSIT` env var or the default. fn default_deposit() -> u128 { - std::env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) + env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_DEPOSIT) +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct FundingContext { + wallet_address: Option, + token: Option, + chain_id: Option, +} + +impl FundingContext { + fn token_line(&self) -> String { + self.token + .as_ref() + .map(|token| format!("Requested payment token: {token}\n\n")) + .unwrap_or_default() + } + + fn network(&self) -> Option { + self.chain_id.filter(|chain| chain.is_tempo()).map(|chain| chain.to_string()) + } } fn format_http_diagnostics(headers: &HeaderMap) -> String { @@ -60,12 +86,173 @@ fn format_http_diagnostics(headers: &HeaderMap) -> String { } } +fn tempo_wallet_fund_help(ctx: &FundingContext) -> String { + let mut command = "tempo wallet fund".to_string(); + if let Some(address) = ctx.wallet_address { + command.push_str(&format!(" --address {address}")); + } + if let Some(network) = ctx.network() { + command.push_str(&format!(" --network {network}")); + } + + let mut no_browser = command.clone(); + no_browser.push_str(" --no-browser"); + + format!( + "\n\nTempo wallet payment could not be funded for this paid RPC request.\n\n{}\ + Fund the wallet, then rerun the command:\n {command}\n\n\ + If this CLI is running on a remote or headless host, use:\n {no_browser}", + ctx.token_line() + ) +} + +/// Decide whether the interactive `tempo wallet fund` flow may be launched. +/// +/// Policy (library-safe): +/// - never run inside CI +/// - never run unless both stdin and stderr are real terminals +/// - `FOUNDRY_MPP_NO_AUTO_FUND` is honored as an opt-out; it must not bypass CI/TTY guards in +/// shared transport code that may be embedded inside long-running RPC daemons. +fn interactive_tempo_fund_allowed( + no_auto_fund: Option<&str>, + in_ci: bool, + stdin_is_terminal: bool, + stderr_is_terminal: bool, +) -> bool { + if no_auto_fund.is_some_and(|v| { + !(v == "0" || v.eq_ignore_ascii_case("false") || v.eq_ignore_ascii_case("off")) + }) { + return false; + } + + if in_ci { + return false; + } + + stdin_is_terminal && stderr_is_terminal +} + +fn can_run_interactive_tempo_fund() -> bool { + if cfg!(test) { + return false; + } + + interactive_tempo_fund_allowed( + std::env::var("FOUNDRY_MPP_NO_AUTO_FUND").ok().as_deref(), + std::env::var_os("CI").is_some(), + std::io::stdin().is_terminal(), + std::io::stderr().is_terminal(), + ) +} + +fn tempo_bin() -> String { + std::env::var("TEMPO_BIN").unwrap_or_else(|_| "tempo".to_string()) +} + +async fn run_interactive_tempo_fund(ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + + let tempo = tempo_bin(); + let mut args = vec!["wallet".to_string(), "fund".to_string()]; + if let Some(address) = ctx.wallet_address { + args.push("--address".to_string()); + args.push(address.to_string()); + } + if let Some(network) = ctx.network() { + args.push("--network".to_string()); + args.push(network); + } + + tracing::warn!( + token = ?ctx.token, + chain_id = ?ctx.chain_id, + "MPP payment could not be funded; opening `tempo wallet fund`" + ); + + let status = tokio::task::spawn_blocking(move || { + Command::new(tempo) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + }) + .await + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to join tempo wallet fund process: {e}" + ))) + })? + .map_err(|e| { + TransportErrorKind::custom(std::io::Error::other(format!( + "failed to run `tempo wallet fund`: {e}{}", + tempo_wallet_fund_help(ctx) + ))) + })?; + + if status.success() { + Ok(true) + } else { + Err(TransportErrorKind::custom(std::io::Error::other(format!( + "`tempo wallet fund` exited with status {status}{}", + tempo_wallet_fund_help(ctx) + )))) + } +} + +/// Single-attempt guard around [`run_interactive_tempo_fund`]. +/// +/// Ensures that for one logical request we launch `tempo wallet fund` at most +/// once, regardless of how many recovery paths (`do_request`, `pay_and_retry`, +/// `handle_response_or_retry_after_fund`, ...) attempt it. +async fn maybe_auto_fund(used: &AtomicBool, ctx: &FundingContext) -> TransportResult { + if !can_run_interactive_tempo_fund() { + return Ok(false); + } + if used.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { + return Ok(false); + } + run_interactive_tempo_fund(ctx).await +} + +/// Returns true iff a 402 response carries a structured insufficient-balance +/// problem (RFC 9457 `PaymentErrorDetails`). +/// +/// We deliberately do **not** match on free-text body content or on generic +/// `verification-failed` problem types, as those have many non-funding causes +/// (bad signature, replay, expired challenge, clock skew, key provisioning, +/// malformed auth, ...). +fn should_suggest_tempo_fund(status: StatusCode, body: &[u8]) -> bool { + if status != StatusCode::PAYMENT_REQUIRED { + return false; + } + let Ok(problem) = serde_json::from_slice::(body) else { + return false; + }; + problem.problem_type.ends_with("/insufficient-balance") +} + +fn format_mpp_payment_failure( + error: impl fmt::Display, + ctx: &FundingContext, + suggest_fund: bool, +) -> String { + let message = error.to_string(); + if suggest_fund { + format!("MPP payment failed: {message}{}", tempo_wallet_fund_help(ctx)) + } else { + format!("MPP payment failed: {message}") + } +} + /// Process-wide payment serialization locks, keyed by origin URL. /// /// Created eagerly so the lock exists before the first provider init, /// preventing concurrent first-402 races. -static GLOBAL_PAY_LOCKS: OnceLock>>>> = - OnceLock::new(); +static GLOBAL_PAY_LOCKS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); /// Production transport: lazily discovers MPP keys from the Tempo wallet on /// first 402 response. @@ -75,24 +262,21 @@ pub type LazyMppHttpTransport = MppHttpTransport; /// Tempo wallet configuration on first use. #[derive(Clone, Debug)] pub struct LazySessionProvider { - inner: std::sync::Arc>>, + inner: Arc>>, /// Eagerly-created, process-wide payment serialization lock for this origin. - pay_lock: std::sync::Arc>, + pay_lock: Arc>, origin: String, } impl LazySessionProvider { pub(super) fn new(origin: String) -> Self { - let pay_lock = { - let global = GLOBAL_PAY_LOCKS.get_or_init(|| Mutex::new(HashMap::new())); - global - .lock() - .unwrap() - .entry(origin.clone()) - .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(()))) - .clone() - }; - Self { inner: std::sync::Arc::new(Mutex::new(None)), pay_lock, origin } + let pay_lock = GLOBAL_PAY_LOCKS + .lock() + .unwrap() + .entry(origin.clone()) + .or_insert_with(|| Arc::new(AsyncMutex::new(()))) + .clone(); + Self { inner: Arc::new(Mutex::new(None)), pay_lock, origin } } fn set_key_provisioned(&self, provisioned: bool) { @@ -125,6 +309,14 @@ impl LazySessionProvider { } } + /// Drop the cached `SessionProvider` so the next `get_or_init` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry, so a long-lived transport doesn't keep paying with + /// the superseded key. + fn invalidate(&self) { + *self.inner.lock().unwrap() = None; + } + pub(super) fn get_or_init(&self, opts: DiscoverOptions) -> TransportResult { let mut guard = self.inner.lock().unwrap(); if let Some(ref provider) = *guard { @@ -132,18 +324,20 @@ impl LazySessionProvider { } let config = discover_mpp_config(opts).ok_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "RPC endpoint returned HTTP 402 Payment Required. \ This endpoint requires payment via the Machine Payments Protocol (MPP).\n\n\ - To configure MPP, install the Tempo wallet CLI and create a key:\n\ - \n curl -sSL https://tempo.xyz/install.sh | bash\ - \n tempo wallet login\ + Authorize an access key against your Tempo wallet:\n\ + \n cast tempo login\ + \n\nIn headless environments, pass `--no-browser` to print the authorization \ + URL instead of launching a browser:\n\ + \n cast tempo login --no-browser\ \n\nSee https://docs.tempo.xyz for more information.", )) })?; let signer: mpp::PrivateKeySigner = config.key.parse().map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("invalid MPP key: {e}"))) + TransportErrorKind::custom(io::Error::other(format!("invalid MPP key: {e}"))) })?; let signing_mode = if let Some(wallet) = config.wallet_address { @@ -152,7 +346,7 @@ impl LazySessionProvider { .as_ref() .map(|hex_str| { crate::tempo::decode_key_authorization(hex_str).map(Box::new).map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "invalid MPP key_authorization: {e}" ))) }) @@ -223,6 +417,17 @@ where P::Provider: Send + Sync + 'static, { async fn do_request(self, req: RequestPacket) -> TransportResult { + // Per-request guard: launch `tempo wallet fund` at most once for one + // logical request, regardless of how many recovery paths attempt it. + let auto_fund_used = AtomicBool::new(false); + self.do_request_inner(req, &auto_fund_used).await + } + + async fn do_request_inner( + self, + req: RequestPacket, + auto_fund_used: &AtomicBool, + ) -> TransportResult { let body = serde_json::to_vec(&req).map_err(TransportErrorKind::custom)?; let headers = req.headers(); @@ -246,15 +451,53 @@ where // held until the retry response is fully handled. let _pay_guard = self.provider.lock_pay().await; - let (resolved, challenge) = Self::select_challenge(&resp, &self.provider)?; + // No local key for any offered challenge → run device-code flow, + // invalidate the cached provider, and fetch a fresh 402 (the original + // may have expired during the browser/passkey flow). + let (resolved, challenge) = + if let Some(chain_id) = tempo_chain_needing_auth(&self.url, &resp) { + debug!(chain_id, "launching wallet.tempo authorization"); + let cfg = crate::tempo::EnsureAccessKeyConfig::from_env(chain_id); + crate::tempo::ensure_access_key(cfg).await.map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "tempo access key authorization failed: {e}" + ))) + })?; + self.provider.invalidate_cached_provider(); + self.fetch_fresh_challenge(&headers, &body).await? + } else { + Self::select_challenge(&resp, &self.provider)? + }; + let funding_ctx = self.provider.funding_context(&challenge); debug!(id = %challenge.id, method = %challenge.method, intent = %challenge.intent, "received MPP 402 challenge, paying"); - let credential = resolved.pay(&challenge).await.map_err(|e| { - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let credential = match resolved.pay(&challenge).await { + Ok(credential) => credential, + Err(e) => { + // Only the explicit `InsufficientBalance` variant is treated as + // a fundable error. Any other failure must surface unchanged so + // we don't mask payment/protocol issues behind a fund prompt. + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + resolved.pay(&challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + self.provider.rollback_pending(); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { + self.provider.rollback_pending(); TransportErrorKind::custom(std::io::Error::other(format!( "failed to format MPP credential: {e}" ))) @@ -286,9 +529,20 @@ where self.provider.commit_topup_and_track_voucher(); let resolved = self.provider.resolve()?; - let voucher_resp = self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(voucher_resp).await; + let voucher_resp = + self.pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used).await?; + + // Route the voucher response through the funding-aware handler so + // a final 402 here also gets the fund retry / contextual help. + let result = self + .handle_response_or_retry_after_fund( + voucher_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -304,7 +558,7 @@ where self.provider.rollback_pending(); self.provider.clear_channels(); - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "MPP channel not found on server (410 Gone). \ The server may have restarted or the channel was closed externally.\n\ Local channel state has been cleared. Re-run to open a new channel.", @@ -333,10 +587,19 @@ where debug!("MPP voucher stale, retrying with fresh voucher"); let resolved = self.provider.resolve()?; if resolved.supports(challenge.method.as_str(), challenge.intent.as_str()) { - let final_resp = - self.pay_and_retry(&challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.flush_pending(); } else { @@ -372,10 +635,19 @@ where let (resolved, fresh_challenge) = self.fetch_fresh_challenge(&headers, &body).await?; - let final_resp = - self.pay_and_retry(&fresh_challenge, &resolved, &headers, &body).await?; - - let result = Self::handle_response(final_resp).await; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; if result.is_ok() { self.provider.set_key_provisioned(true); self.provider.flush_pending(); @@ -386,9 +658,40 @@ where } self.provider.rollback_pending(); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) + && maybe_auto_fund(auto_fund_used, &funding_ctx).await? + { + let (resolved, fresh_challenge) = + self.fetch_fresh_challenge(&headers, &body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, &headers, &body, auto_fund_used) + .await?; + + let result = self + .handle_response_or_retry_after_fund( + final_resp, + &headers, + &body, + &funding_ctx, + auto_fund_used, + ) + .await; + if result.is_ok() { + self.provider.set_key_provisioned(true); + self.provider.flush_pending(); + } else { + self.provider.rollback_pending(); + } + return result; + } + + let mut error_text = format!("{retry_text}{diagnostics}"); + if should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &retry_body) { + error_text.push_str(&tempo_wallet_fund_help(&funding_ctx)); + } return Err(TransportErrorKind::http_error( StatusCode::PAYMENT_REQUIRED.as_u16(), - format!("{retry_text}{diagnostics}"), + error_text, )); } @@ -409,15 +712,32 @@ where provider: &P::Provider, headers: &reqwest::header::HeaderMap, body: &[u8], + auto_fund_used: &AtomicBool, ) -> TransportResult { - let credential = provider.pay(challenge).await.map_err(|e| { - self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!("MPP payment failed: {e}"))) - })?; + let funding_ctx = self.provider.funding_context(challenge); + let credential = match provider.pay(challenge).await { + Ok(credential) => credential, + Err(e) => { + self.provider.rollback_pending(); + let is_insufficient = matches!(e, mpp::MppError::InsufficientBalance(_)); + if is_insufficient && maybe_auto_fund(auto_fund_used, &funding_ctx).await? { + provider.pay(challenge).await.map_err(|e2| { + let suggest = matches!(e2, mpp::MppError::InsufficientBalance(_)); + TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e2, &funding_ctx, suggest), + )) + })? + } else { + return Err(TransportErrorKind::custom(std::io::Error::other( + format_mpp_payment_failure(e, &funding_ctx, is_insufficient), + ))); + } + } + }; let auth_header = format_authorization(&credential).map_err(|e| { self.provider.rollback_pending(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "failed to format MPP credential: {e}" ))) })?; @@ -437,6 +757,41 @@ where }) } + async fn handle_response_or_retry_after_fund( + &self, + resp: reqwest::Response, + headers: &reqwest::header::HeaderMap, + body: &[u8], + funding_ctx: &FundingContext, + auto_fund_used: &AtomicBool, + ) -> TransportResult { + if resp.status() != StatusCode::PAYMENT_REQUIRED { + return Self::handle_response_with_funding(resp, Some(funding_ctx)).await; + } + + let diagnostics = format_http_diagnostics(resp.headers()); + let status = resp.status(); + let resp_body = resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if should_suggest_tempo_fund(status, &resp_body) + && maybe_auto_fund(auto_fund_used, funding_ctx).await? + { + self.provider.rollback_pending(); + + let (resolved, fresh_challenge) = self.fetch_fresh_challenge(headers, body).await?; + let final_resp = self + .pay_and_retry(&fresh_challenge, &resolved, headers, body, auto_fund_used) + .await?; + return Self::handle_response_with_funding(final_resp, Some(funding_ctx)).await; + } + + let mut error_text = format!("{}{diagnostics}", String::from_utf8_lossy(&resp_body)); + if should_suggest_tempo_fund(status, &resp_body) { + error_text.push_str(&tempo_wallet_fund_help(funding_ctx)); + } + Err(TransportErrorKind::http_error(status.as_u16(), error_text)) + } + /// Fetch a fresh 402 challenge from the server (unauthenticated request). /// /// Returns `Ok(Some((provider, challenge)))` if the server returns a 402 @@ -462,7 +817,7 @@ where // Non-402 → return whatever the server sent (could be success or error). let result = Self::handle_response(fresh_resp).await; return Err(result.err().unwrap_or_else(|| { - TransportErrorKind::custom(std::io::Error::other( + TransportErrorKind::custom(io::Error::other( "unexpected success on unauthenticated fresh probe", )) })); @@ -477,25 +832,14 @@ where resp: &reqwest::Response, provider: &P, ) -> TransportResult<(P::Provider, mpp::protocol::core::PaymentChallenge)> { - let www_auth_values: Vec<&str> = resp - .headers() - .get_all(WWW_AUTHENTICATE_HEADER) - .iter() - .filter_map(|v| v.to_str().ok()) - .collect(); - - if www_auth_values.is_empty() { - return Err(TransportErrorKind::custom(std::io::Error::other(format!( + let challenges = parse_challenges(resp); + if challenges.is_empty() && resp.headers().get(WWW_AUTHENTICATE_HEADER).is_none() { + return Err(TransportErrorKind::custom(io::Error::other(format!( "402 response missing WWW-Authenticate header{}", format_http_diagnostics(resp.headers()) )))); } - let challenges: Vec<_> = parse_www_authenticate_all(www_auth_values) - .into_iter() - .filter_map(|r| r.ok()) - .collect(); - let mut last_resolve_err: Option = None; let resolved_pair = challenges.iter().find_map(|c| { let (chain_id, currency) = extract_challenge_chain_and_currency(c); @@ -515,7 +859,7 @@ where } let offered: Vec<_> = challenges.iter().map(|c| format!("{}.{}", c.method, c.intent)).collect(); - TransportErrorKind::custom(std::io::Error::other(format!( + TransportErrorKind::custom(io::Error::other(format!( "no supported MPP challenge; server offered [{}]", offered.join(", "), ))) @@ -523,6 +867,17 @@ where } async fn handle_response(resp: reqwest::Response) -> TransportResult { + Self::handle_response_with_funding(resp, None).await + } + + /// Like [`Self::handle_response`] but, when an unsuccessful 402 looks like a + /// fundable error, appends actionable `tempo wallet fund` help that uses + /// the per-request `FundingContext` (so the suggested command includes + /// `--address` and `--network` when known). + async fn handle_response_with_funding( + resp: reqwest::Response, + funding_ctx: Option<&FundingContext>, + ) -> TransportResult { let status = resp.status(); debug!(%status, "received response from MPP transport"); let diagnostics = format_http_diagnostics(resp.headers()); @@ -536,10 +891,19 @@ where } if !status.is_success() { - return Err(TransportErrorKind::http_error( - status.as_u16(), - format!("{}{diagnostics}", String::from_utf8_lossy(&body)), - )); + let mut body_text = format!("{}{diagnostics}", String::from_utf8_lossy(&body)); + if should_suggest_tempo_fund(status, &body) { + let default_ctx; + let ctx = match funding_ctx { + Some(c) => c, + None => { + default_ctx = FundingContext::default(); + &default_ctx + } + }; + body_text.push_str(&tempo_wallet_fund_help(ctx)); + } + return Err(TransportErrorKind::http_error(status.as_u16(), body_text)); } serde_json::from_slice(&body) @@ -547,6 +911,57 @@ where } } +/// Returns `Some(chain_id)` when a 402 response should trigger the +/// `wallet.tempo.xyz` device-code authorization flow. +/// +/// Conditions: known Tempo endpoint, interactive (TTY, not `CI`), and no +/// offered Tempo challenge resolves against a local key on `(chain, currency)`. +/// The picked chain matches the first unresolved challenge — same iteration +/// order [`MppHttpTransport::select_challenge`] uses. +fn tempo_chain_needing_auth(url: &Url, resp: &reqwest::Response) -> Option { + if !io::stderr().is_terminal() || env::var_os("CI").is_some() { + return None; + } + pick_chain_needing_auth(url, &parse_challenges(resp)) +} + +/// Extract all parseable MPP challenges from a 402 response's `WWW-Authenticate` headers. +fn parse_challenges(resp: &reqwest::Response) -> Vec { + let values: Vec<&str> = resp + .headers() + .get_all(WWW_AUTHENTICATE_HEADER) + .iter() + .filter_map(|v| v.to_str().ok()) + .collect(); + parse_www_authenticate_all(values).into_iter().filter_map(|r| r.ok()).collect() +} + +/// Inner logic of [`tempo_chain_needing_auth`], factored out for testing. +fn pick_chain_needing_auth( + url: &Url, + challenges: &[mpp::protocol::core::PaymentChallenge], +) -> Option { + if !crate::tempo::is_known_tempo_endpoint(url) { + return None; + } + + let tempo_challenges: Vec<_> = + challenges.iter().filter(|c| c.method.as_str() == "tempo").collect(); + + // If any challenge already resolves with a local key, no auth needed. + let any_resolvable = tempo_challenges.iter().any(|c| { + let (chain_id, currency) = extract_challenge_chain_and_currency(c); + let currency = currency.and_then(|s| s.parse().ok()); + super::keys::discover_mpp_config(super::keys::DiscoverOptions { chain_id, currency }) + .is_some() + }); + if any_resolvable { + return None; + } + + tempo_challenges.iter().find_map(|c| extract_challenge_chain_and_currency(c).0) +} + /// Extract `(chainId, currency)` from a parsed MPP challenge. pub(super) fn extract_challenge_chain_and_currency( c: &mpp::protocol::core::PaymentChallenge, @@ -576,10 +991,28 @@ pub(crate) trait ResolveProvider { fn flush_pending(&self) {} fn rollback_pending(&self) {} fn commit_topup_and_track_voucher(&self) {} + /// Drop any cached payment provider so the next `resolve_for` re-runs + /// discovery. Called after the device-code flow writes a fresh + /// `keys.toml` entry. + fn invalidate_cached_provider(&self) {} + fn funding_wallet_address(&self) -> Option { + None + } + fn funding_chain_id(&self) -> Option { + None + } + fn funding_context(&self, challenge: &mpp::protocol::core::PaymentChallenge) -> FundingContext { + let (challenge_chain_id, token) = extract_challenge_chain_and_currency(challenge); + FundingContext { + wallet_address: self.funding_wallet_address(), + token, + chain_id: challenge_chain_id.or_else(|| self.funding_chain_id()).map(Chain::from_id), + } + } /// Acquire the payment serialization lock. The returned guard must be held /// across the entire 402 → pay → retry → response cycle to prevent /// concurrent channel opens and colliding expiring-nonce transactions. - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn lock_pay(&self) -> impl Future>> + Send { async { None } } } @@ -599,7 +1032,7 @@ impl ResolveProvider for LazySessionProvider { // regardless of opts. Re-check that the provider's key is compatible // with this challenge's chain/currency. if !provider.matches_challenge(opts.chain_id, opts.currency) { - return Err(TransportErrorKind::custom(std::io::Error::other( + return Err(TransportErrorKind::custom(io::Error::other( "cached provider does not match challenge chain/currency", ))); } @@ -623,7 +1056,16 @@ impl ResolveProvider for LazySessionProvider { fn commit_topup_and_track_voucher(&self) { Self::commit_topup_and_track_voucher(self) } - fn lock_pay(&self) -> impl std::future::Future>> + Send { + fn invalidate_cached_provider(&self) { + Self::invalidate(self) + } + fn funding_wallet_address(&self) -> Option { + self.inner.lock().unwrap().as_ref().map(|p| p.funding_wallet_address()) + } + fn funding_chain_id(&self) -> Option { + self.inner.lock().unwrap().as_ref().and_then(|p| p.key_chain_id()) + } + fn lock_pay(&self) -> impl Future>> + Send { let lock = self.pay_lock.clone(); async move { Some(lock.lock_owned().await) } } @@ -685,7 +1127,7 @@ mod tests { fn pay( &self, challenge: &PaymentChallenge, - ) -> impl std::future::Future> + Send { + ) -> impl Future> + Send { let echo = challenge.to_echo(); async move { Ok(PaymentCredential::with_source( @@ -697,6 +1139,21 @@ mod tests { } } + #[derive(Clone, Debug)] + struct InsufficientBalanceProvider; + + impl PaymentProvider for InsufficientBalanceProvider { + fn supports(&self, method: &str, intent: &str) -> bool { + method == "tempo" && (intent == "session" || intent == "charge") + } + + async fn pay(&self, _challenge: &PaymentChallenge) -> Result { + Err(MppError::InsufficientBalance(Some( + "wallet has 0 pathUSD but needs 100000".to_string(), + ))) + } + } + fn test_challenge() -> (PaymentChallenge, String) { let request = Base64UrlJson::from_value(&serde_json::json!({ "amount": "1000", @@ -853,8 +1310,238 @@ mod tests { handle.abort(); } + #[tokio::test] + async fn test_mpp_transport_payment_failure_suggests_tempo_wallet_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move || { + let www_auth = www_auth.clone(); + async move { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required", + ) + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + InsufficientBalanceProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + #[tokio::test] + async fn test_mpp_transport_retry_402_insufficient_balance_suggests_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail( + "Insufficient pathUSD balance: have 0, need 100000", + ), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("InsufficientBalanceError"), "got: {msg}"); + assert!(msg.contains("Tempo wallet payment could not be funded"), "got: {msg}"); + assert!(msg.contains("tempo wallet fund"), "got: {msg}"); + assert!(msg.contains("--no-browser"), "got: {msg}"); + assert!(msg.contains("Requested payment token: 0x20c0"), "got: {msg}"); + + handle.abort(); + } + + /// Generic `verification-failed` has many non-funding causes (bad signature, + /// replay, expired challenge, clock skew, ...). The transport must surface + /// the original error verbatim and must NOT add a "fund your wallet" hint. + #[tokio::test] + async fn test_mpp_transport_final_402_verification_failed_does_not_suggest_fund() { + let (_, www_auth) = test_challenge(); + + let app = axum::Router::new().route( + "/", + post(move |req: axum::http::Request| { + let www_auth = www_auth.clone(); + async move { + if req.headers().get("authorization").is_some() { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("content-type", "application/problem+json")], + serde_json::to_string( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(), + ) + .into_response() + } else { + ( + AxumStatusCode::PAYMENT_REQUIRED, + [("www-authenticate", www_auth)], + "Payment Required".to_string(), + ) + .into_response() + } + } + }), + ); + + let (base_url, handle) = spawn_server(app).await; + let mut transport = MppHttpTransport::new( + reqwest::Client::new(), + Url::parse(&base_url).unwrap(), + MockPaymentProvider, + ); + + let err = tower::Service::call(&mut transport, test_request()).await.unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Verification Failed"), "got: {msg}"); + assert!( + !msg.contains("Tempo wallet payment could not be funded"), + "verification-failed must not be classified as fundable; got: {msg}" + ); + + handle.abort(); + } + + // --- Classifier unit tests -------------------------------------------- + + #[test] + fn classifier_only_triggers_on_explicit_insufficient_balance_problem() { + // explicit insufficient-balance → true + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_title("InsufficientBalanceError") + .with_detail("Insufficient pathUSD balance"), + ) + .unwrap(); + assert!(should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_verification_failed() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::core("verification-failed") + .with_title("Verification Failed") + .with_detail("Payment verification failed."), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, &body)); + } + + #[test] + fn classifier_does_not_trigger_on_unrelated_text_with_balance_words() { + // Free-text 402 body that just happens to mention the word "balance" + // must NOT trigger the fund suggestion (no structured problem details). + let body = + b"402 Payment Required: server could not balance ledger entries; insufficient inputs."; + assert!(!should_suggest_tempo_fund(StatusCode::PAYMENT_REQUIRED, body)); + } + + #[test] + fn classifier_does_not_trigger_outside_402() { + let body = serde_json::to_vec( + &mpp::error::PaymentErrorDetails::session("insufficient-balance") + .with_detail("Insufficient balance"), + ) + .unwrap(); + assert!(!should_suggest_tempo_fund(StatusCode::INTERNAL_SERVER_ERROR, &body)); + assert!(!should_suggest_tempo_fund(StatusCode::OK, &body)); + } + + #[test] + fn fund_help_includes_address_and_network_for_known_chain() { + let ctx = FundingContext { + wallet_address: Some("0x000000000000000000000000000000000000dEaD".parse().unwrap()), + token: Some("0x20c0".to_string()), + chain_id: Some(Chain::from_id(42431)), + }; + let help = tempo_wallet_fund_help(&ctx); + assert!(help.contains("--address 0x"), "missing --address: {help}"); + assert!(help.contains("--network tempo-moderato"), "missing --network: {help}"); + assert!(help.contains("--no-browser"), "missing --no-browser: {help}"); + assert!(help.contains("Requested payment token: 0x20c0"), "missing token: {help}"); + + let mainnet = FundingContext { chain_id: Some(Chain::from_id(4217)), ..ctx }; + let help2 = tempo_wallet_fund_help(&mainnet); + assert!(help2.contains("--network tempo"), "missing tempo network: {help2}"); + } + + #[test] + fn auto_fund_policy_blocks_in_ci_and_non_tty() { + assert!(!interactive_tempo_fund_allowed(Some("1"), true, true, true), "must not run in CI"); + assert!( + interactive_tempo_fund_allowed(Some("0"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=0 must not disable" + ); + assert!( + interactive_tempo_fund_allowed(Some("false"), false, true, true), + "FOUNDRY_MPP_NO_AUTO_FUND=false must not disable" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, false, true), + "stdin must be a terminal" + ); + assert!( + !interactive_tempo_fund_allowed(None, false, true, false), + "stderr must be a terminal" + ); + assert!(!interactive_tempo_fund_allowed(Some("1"), false, true, true)); + assert!(!interactive_tempo_fund_allowed(Some("true"), false, true, true)); + assert!(interactive_tempo_fund_allowed(None, false, true, true)); + } + #[tokio::test] async fn test_plain_http_402_shows_mpp_setup_instructions() { + let _g = crate::tempo::test_env_mutex().lock().await; let (_, www_auth) = test_challenge(); let app = axum::Router::new().route( @@ -920,6 +1607,32 @@ mod tests { ); } + /// `invalidate_cached_provider` clears the cache so the next + /// `get_or_init` re-runs discovery — the path `do_request` takes after + /// `ensure_access_key` writes a fresh `keys.toml` entry. + #[tokio::test] + async fn lazy_session_provider_invalidate_clears_cache() { + let _g = crate::tempo::test_env_mutex().lock().await; + // TEMPO_PRIVATE_KEY lets discovery succeed without a keys.toml. + let key_hex = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + unsafe { + std::env::set_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV, key_hex); + std::env::remove_var(crate::tempo::TEMPO_HOME_ENV); + } + + let lazy = LazySessionProvider::new("https://rpc.example.com".into()); + let _ = lazy.get_or_init(Default::default()).expect("discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected provider to be cached"); + + ResolveProvider::invalidate_cached_provider(&lazy); + assert!(lazy.inner.lock().unwrap().is_none(), "expected cache to be cleared"); + + let _ = lazy.get_or_init(Default::default()).expect("re-discovery succeeds"); + assert!(lazy.inner.lock().unwrap().is_some(), "expected re-init to repopulate cache"); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV) }; + } + #[test] fn challenge_chain_and_currency_extraction() { let extract = |headers: Vec<&str>| -> Vec<(Option, Option)> { @@ -955,4 +1668,73 @@ mod tests { ); assert_eq!(extract(vec![&no_details]), vec![(None, Some("0x20c0".into()))]); } + + /// Auth must trigger when a key matches the chain but not the currency. + #[test] + fn pick_chain_needing_auth_currency_aware() { + let _g = crate::tempo::test_env_mutex().blocking_lock(); + let dir = tempfile::tempdir().unwrap(); + let wallet = dir.path().join("wallet"); + std::fs::create_dir_all(&wallet).unwrap(); + std::fs::write( + wallet.join("keys.toml"), + r#" +[[keys]] +wallet_type = "passkey" +wallet_address = "0x0000000000000000000000000000000000000001" +key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +chain_id = 4217 + +[[keys.limits]] +currency = "0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +limit = "1000" +"#, + ) + .unwrap(); + unsafe { + std::env::set_var(crate::tempo::TEMPO_HOME_ENV, dir.path()); + std::env::remove_var(crate::tempo::TEMPO_PRIVATE_KEY_ENV); + } + + let url = Url::parse("https://rpc.mpp.tempo.xyz").unwrap(); + let mk = |currency: &str| -> PaymentChallenge { + PaymentChallenge { + id: "x".into(), + realm: "api".into(), + method: MethodName::new("tempo"), + intent: IntentName::new("charge"), + request: Base64UrlJson::from_value(&serde_json::json!({ + "amount": "1", + "currency": currency, + "recipient": "0xabc", + "methodDetails": { "chainId": 4217 } + })) + .unwrap(), + expires: None, + description: None, + digest: None, + opaque: None, + } + }; + + // Currency mismatch → auth needed. + let mismatched = mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + assert_eq!(pick_chain_needing_auth(&url, &[mismatched]), Some(4217)); + + // Currency match → no auth. + let matched = mk("0x20c0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + assert_eq!(pick_chain_needing_auth(&url, &[matched]), None); + + // Non-Tempo host → never triggers, even without a key. + let stripe_url = Url::parse("https://api.stripe.com").unwrap(); + assert_eq!( + pick_chain_needing_auth( + &stripe_url, + &[mk("0x20c0bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")] + ), + None, + ); + + unsafe { std::env::remove_var(crate::tempo::TEMPO_HOME_ENV) }; + } } diff --git a/crates/common/src/provider/mpp/ws.rs b/crates/common/src/provider/mpp/ws.rs index f631d0b08a2fc..69aef7d4f4cbc 100644 --- a/crates/common/src/provider/mpp/ws.rs +++ b/crates/common/src/provider/mpp/ws.rs @@ -378,6 +378,8 @@ mod tests { /// MPP server sends challenge → client pays → server sends receipt. #[tokio::test] async fn test_ws_mpp_challenge_credential_receipt() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); @@ -452,6 +454,8 @@ mod tests { /// MPP server sends challenge, client pays, server closes → rollback. #[tokio::test] async fn test_ws_mpp_rollback_on_post_pay_close() { + // Serialize with other tests that mutate TEMPO_PRIVATE_KEY / TEMPO_HOME. + let _g = crate::tempo::test_env_mutex().lock().await; let challenge = test_challenge(); let challenge_json = serde_json::to_value(&challenge).unwrap(); diff --git a/crates/common/src/provider/runtime_transport.rs b/crates/common/src/provider/runtime_transport.rs index 7db1ebd1b3f91..f59a2efa75b8e 100644 --- a/crates/common/src/provider/runtime_transport.rs +++ b/crates/common/src/provider/runtime_transport.rs @@ -36,7 +36,11 @@ fn is_known_mpp_endpoint(url: &Url) -> bool { /// Only meant to be used internally by [RuntimeTransport]. #[derive(Clone, Debug)] pub enum InnerTransport { - /// HTTP transport with lazy MPP 402 handling + /// HTTP transport with lazy MPP 402 handling. + /// + /// For known Tempo endpoints, the MPP layer additionally runs the + /// `wallet.tempo.xyz` device-code flow on a 402 when no local access key + /// is configured (see [`crate::tempo::ensure_access_key`]). Http(LazyMppHttpTransport), /// WebSocket transport Ws(PubSubFrontend), diff --git a/crates/common/src/tempo/auth.rs b/crates/common/src/tempo/auth.rs new file mode 100644 index 0000000000000..d79306cfb74f2 --- /dev/null +++ b/crates/common/src/tempo/auth.rs @@ -0,0 +1,494 @@ +//! Tempo wallet device-code authorization flow. +//! +//! Implements the CLI side of the tempoxyz/accounts `cli-auth` device-code +//! protocol: generates a local secp256k1 access key, creates a PKCE-protected +//! device code, opens `wallet.tempo.xyz/cli-auth?code=` in the browser, +//! polls until the user authorizes the key on their passkey wallet, and writes +//! the resulting `keyAuthorization` to `~/.tempo/wallet/keys.toml`. + +use crate::tempo::{ + KeyEntry, KeyType, StoredTokenLimit, WalletType, decode_key_authorization, upsert_key_entry, +}; +use alloy_primitives::{Address, B256, hex}; +use alloy_signer_local::PrivateKeySigner; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use eyre::Result; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +#[cfg(any(unix, windows))] +use std::process::Command; +use std::{ + env, + sync::LazyLock, + time::{Duration, Instant}, +}; +use tempo_primitives::transaction::{SignatureType, SignedKeyAuthorization}; +use tokio::sync::Mutex; + +/// Default device-code service URL (production wallet.tempo.xyz). +const DEFAULT_CLI_AUTH_URL: &str = "https://wallet.tempo.xyz/cli-auth"; + +/// Returns `true` if `url`'s host is `tempo.xyz` or a subdomain of it. +pub(crate) fn is_known_tempo_endpoint(url: &url::Url) -> bool { + url.host_str().is_some_and(|host| host == "tempo.xyz" || host.ends_with(".tempo.xyz")) +} + +/// Env var to override the device-code service URL (for tests / staging). +const TEMPO_CLI_AUTH_URL_ENV: &str = "TEMPO_CLI_AUTH_URL"; + +const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(2); +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); + +/// Per-process serialization of concurrent `ensure_access_key` calls. +/// +/// Prevents two `cast` invocations in the same process from racing two browser +/// popups for the same chain. +static AUTH_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +/// Configuration for [`ensure_access_key`]. +#[derive(Clone, Debug)] +pub struct EnsureAccessKeyConfig { + /// Chain ID the access key is being authorized for. + pub chain_id: u64, + /// Device-code service base URL. Defaults to [`DEFAULT_CLI_AUTH_URL`]. + pub(crate) service_url: String, + /// Poll interval. + pub(crate) poll_interval: Duration, + /// Total timeout for the authorization flow. + pub(crate) timeout: Duration, + /// If `true`, print the authorization URL to stderr instead of opening a + /// browser. + pub no_browser: bool, +} + +impl EnsureAccessKeyConfig { + /// Build a config from the environment for the given chain. + /// + /// `no_browser` defaults to `true` under `CI`; callers (e.g. `cast tempo + /// login --no-browser`) may override it. + pub fn from_env(chain_id: u64) -> Self { + Self { + chain_id, + service_url: env::var(TEMPO_CLI_AUTH_URL_ENV) + .unwrap_or_else(|_| DEFAULT_CLI_AUTH_URL.to_string()), + poll_interval: DEFAULT_POLL_INTERVAL, + timeout: DEFAULT_TIMEOUT, + no_browser: env::var_os("CI").is_some(), + } + } +} + +/// Open `url` via the OS default browser handler. On platforms without a known +/// opener, this is a no-op (the URL is still printed by [`ensure_access_key`]). +fn open_browser(_url: &str) { + #[cfg(target_os = "macos")] + let _ = Command::new("open").arg(_url).spawn(); + #[cfg(target_os = "windows")] + let _ = Command::new("cmd").args(["/c", "start", "", _url]).spawn(); + #[cfg(all(unix, not(target_os = "macos")))] + let _ = Command::new("xdg-open").arg(_url).spawn(); +} + +/// Result of [`ensure_access_key`]. +#[derive(Debug, Clone)] +pub struct AccessKeyOutcome { + pub wallet_address: Address, + pub key_address: Address, + pub chain_id: u64, +} + +/// Run the device-code flow, persist the resulting key to `keys.toml`, and +/// return the new entry's identifying fields. +pub async fn ensure_access_key(cfg: EnsureAccessKeyConfig) -> Result { + let _guard = AUTH_LOCK.lock().await; + + let signer = PrivateKeySigner::random(); + let key_address = signer.address(); + // The server requires uncompressed SEC1 (65-byte `0x04 || X || Y`); the + // default `to_sec1_bytes()` would emit the compressed 33-byte form. + let pub_key_hex = format!( + "0x{}", + hex::encode(signer.credential().verifying_key().to_encoded_point(false).as_bytes()), + ); + + let code_verifier = random_code_verifier(); + let client = reqwest::Client::builder().timeout(Duration::from_secs(30)).build()?; + let service = cfg.service_url.trim_end_matches('/'); + + let create_req = CreateCodeRequest { + chain_id: cfg.chain_id, + code_challenge: sha256_b64url(&code_verifier), + key_type: "secp256k1", + pub_key: pub_key_hex, + }; + let code = create_code_with_retry(&client, service, &create_req, cfg.timeout).await?; + + let browser_url = format!("{service}?code={code}"); + if cfg.no_browser { + let _ = crate::sh_eprintln!("Open this URL to authorize: {browser_url}"); + } else { + let _ = crate::sh_eprintln!( + "Opening wallet.tempo to authorize an access key…\n {browser_url}" + ); + open_browser(&browser_url); + } + + let poll = PollRequest { code_verifier }; + let started = Instant::now(); + loop { + // Retry transient network/5xx/429 failures within `cfg.timeout`. + let send_res = client.post(format!("{service}/poll/{code}")).json(&poll).send().await; + + let resp = match send_res { + Ok(r) => r, + Err(e) if is_transient_error(&e) && started.elapsed() < cfg.timeout => { + tracing::debug!(error = %e, "transient error polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + Err(e) => return Err(e.into()), + }; + + let status = resp.status(); + if !status.is_success() { + if is_transient_status(status) && started.elapsed() < cfg.timeout { + tracing::debug!(%status, "transient HTTP status polling device code, retrying"); + tokio::time::sleep(cfg.poll_interval).await; + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code poll failed ({status}): {body}"); + } + + let body: PollResponse = resp.json().await?; + match body { + PollResponse::Pending => { + if started.elapsed() > cfg.timeout { + eyre::bail!("timed out waiting for wallet authorization (code {code})"); + } + tokio::time::sleep(cfg.poll_interval).await; + } + PollResponse::Expired => { + eyre::bail!("device code {code} expired before authorization"); + } + PollResponse::Authorized { account_address, key_authorization } => { + let hex_str = key_authorization.ok_or_else(|| { + eyre::eyre!("wallet authorized response missing key_authorization") + })?; + let signed: SignedKeyAuthorization = decode_key_authorization(&hex_str)?; + // Reject mismatches before persisting — an unusable keys.toml + // entry would silently break the next 402 retry. + if signed.authorization.key_id != key_address { + eyre::bail!( + "wallet authorized key {} but the locally generated key is {}", + signed.authorization.key_id, + key_address, + ); + } + if signed.authorization.chain_id != cfg.chain_id { + eyre::bail!( + "wallet authorized chain {} but {} was requested", + signed.authorization.chain_id, + cfg.chain_id, + ); + } + if signed.authorization.key_type != SignatureType::Secp256k1 { + eyre::bail!( + "wallet returned keyType {:?} but secp256k1 was requested", + signed.authorization.key_type, + ); + } + let chain_id = signed.authorization.chain_id; + let key_authorization = + if hex_str.starts_with("0x") { hex_str } else { format!("0x{hex_str}") }; + let entry = KeyEntry { + wallet_type: WalletType::Passkey, + wallet_address: account_address, + chain_id, + key_type: match signed.authorization.key_type { + SignatureType::P256 => KeyType::P256, + SignatureType::WebAuthn => KeyType::WebAuthn, + _ => KeyType::Secp256k1, + }, + key_address: Some(key_address), + key: Some(format!("0x{}", hex::encode(signer.to_bytes()))), + key_authorization: Some(key_authorization), + expiry: signed.authorization.expiry.map(|n| n.get()), + limits: signed + .authorization + .limits + .unwrap_or_default() + .into_iter() + .map(|l| StoredTokenLimit { currency: l.token, limit: l.limit.to_string() }) + .collect(), + }; + upsert_key_entry(entry)?; + return Ok(AccessKeyOutcome { + wallet_address: account_address, + key_address, + chain_id, + }); + } + } + } +} + +fn is_transient_error(err: &reqwest::Error) -> bool { + err.is_timeout() || err.is_connect() || err.is_request() +} + +fn is_transient_status(status: reqwest::StatusCode) -> bool { + status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS +} + +/// POST `/code` with exponential backoff on transient errors, bounded by `timeout`. +async fn create_code_with_retry( + client: &reqwest::Client, + service: &str, + req: &CreateCodeRequest, + timeout: Duration, +) -> Result { + let started = Instant::now(); + let mut backoff = Duration::from_millis(500); + loop { + let send_res = client.post(format!("{service}/code")).json(req).send().await; + + match send_res { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + let CreateCodeResponse { code } = resp.json().await?; + return Ok(code); + } + if is_transient_status(status) && started.elapsed() < timeout { + tracing::debug!(%status, "transient HTTP status creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + continue; + } + let body = resp.text().await.unwrap_or_default(); + eyre::bail!("device-code create failed ({status}): {body}"); + } + Err(e) if is_transient_error(&e) && started.elapsed() < timeout => { + tracing::debug!(error = %e, "transient error creating device code, retrying"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(5)); + } + Err(e) => return Err(e.into()), + } + } +} + +fn random_code_verifier() -> String { + let bytes = B256::random(); + URL_SAFE_NO_PAD.encode(bytes.as_slice()) +} + +fn sha256_b64url(input: &str) -> String { + let digest = Sha256::digest(input.as_bytes()); + URL_SAFE_NO_PAD.encode(digest) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CreateCodeRequest { + /// `0x`-hex per the SDK schema (server accepts hex string or bigint, not a plain JSON number). + #[serde(serialize_with = "serialize_u64_hex")] + chain_id: u64, + code_challenge: String, + key_type: &'static str, + pub_key: String, +} + +fn serialize_u64_hex(v: &u64, s: S) -> std::result::Result { + s.serialize_str(&format!("0x{v:x}")) +} + +#[derive(Deserialize)] +struct CreateCodeResponse { + code: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PollRequest { + code_verifier: String, +} + +/// Matches `tempoxyz/wallet` poll response shape. +#[derive(Deserialize)] +#[serde(tag = "status", rename_all = "lowercase")] +enum PollResponse { + Pending, + Expired, + Authorized { + account_address: Address, + #[serde(default)] + key_authorization: Option, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tempo::{TEMPO_HOME_ENV, read_tempo_keys_file, test_env_mutex}; + use axum::{Json, Router, extract::State, routing::post}; + use std::sync::{Arc, Mutex}; + + #[test] + fn pkce_challenge_matches_sdk_format() { + // Vector from RFC 7636 §4.2. + let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + let challenge = sha256_b64url(verifier); + assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); + } + + /// Recover the EOA from a SEC1-encoded public key (compressed or + /// uncompressed). + fn address_from_sec1_hex(s: &str) -> Address { + let stripped = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(stripped).expect("valid hex"); + let vk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes).expect("valid SEC1 pubkey"); + Address::from_public_key(&vk) + } + + #[derive(Clone)] + struct MockState { + wallet: Arc>>, + /// Derived from the `pubKey` posted to `/code` so `/poll` can echo + /// back a matching `keyId`, like a real wallet would. + key_id: Arc>>, + /// Chain ID the mock `/poll` returns in `keyAuthorization`. + poll_chain_id: u64, + } + + async fn create_code_handler( + State(state): State, + Json(body): Json, + ) -> Json { + // Sanity: required fields present and chainId is a 0x-hex string, + // matching the SDK wire format the live server enforces. + let pub_key = body + .get("pubKey") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| panic!("pubKey missing: {body}")); + assert!(body.get("codeChallenge").is_some(), "codeChallenge missing: {body}"); + let chain_id = body.get("chainId").unwrap_or_else(|| panic!("chainId missing: {body}")); + let chain_str = chain_id + .as_str() + .unwrap_or_else(|| panic!("chainId must be string, got {chain_id}: {body}")); + assert!(chain_str.starts_with("0x"), "chainId must be 0x-hex, got {chain_str}"); + let wallet: Address = "0x0000000000000000000000000000000000000042".parse().unwrap(); + *state.wallet.lock().unwrap() = Some(wallet); + *state.key_id.lock().unwrap() = Some(address_from_sec1_hex(pub_key)); + Json(serde_json::json!({ "code": "ABCDEFGH" })) + } + + /// Build the RLP-hex `SignedKeyAuthorization` blob the live server returns + /// in the `key_authorization` field. + fn signed_key_auth_hex(chain_id: u64, key_id: Address, expiry: u64) -> String { + use alloy_rlp::Encodable; + use tempo_primitives::transaction::{KeyAuthorization, PrimitiveSignature}; + let auth = KeyAuthorization::unrestricted(chain_id, SignatureType::Secp256k1, key_id) + .with_expiry(expiry); + let sig: PrimitiveSignature = serde_json::from_value(serde_json::json!({ + "type": "secp256k1", "r": "0x0", "s": "0x0", "yParity": 0 + })) + .unwrap(); + let signed = auth.into_signed(sig); + let mut buf = Vec::new(); + signed.encode(&mut buf); + format!("0x{}", hex::encode(buf)) + } + + async fn poll_handler(State(state): State) -> Json { + let wallet = state.wallet.lock().unwrap().expect("create_code must be called first"); + let key_id = state.key_id.lock().unwrap().expect("create_code must be called first"); + Json(serde_json::json!({ + "status": "authorized", + "account_address": wallet, + "key_authorization": signed_key_auth_hex(state.poll_chain_id, key_id, 9_999_999_999), + })) + } + + /// Spawn a mock wallet.tempo server whose `/poll` echoes `poll_chain_id`. + async fn spawn_mock_wallet(poll_chain_id: u64) -> (String, tokio::task::JoinHandle<()>) { + let app = Router::new() + .route("/code", post(create_code_handler)) + .route("/poll/{code}", post(poll_handler)) + .with_state(MockState { + wallet: Arc::default(), + key_id: Arc::default(), + poll_chain_id, + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let handle = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + (format!("http://{addr}"), handle) + } + + fn test_cfg(service_url: String) -> EnsureAccessKeyConfig { + EnsureAccessKeyConfig { + chain_id: 4217, + service_url, + poll_interval: Duration::from_millis(10), + timeout: Duration::from_secs(2), + no_browser: true, + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_happy_path_writes_keys_toml() { + // SAFETY: serialized with other tests that mutate TEMPO_HOME. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(4217).await; + let outcome = ensure_access_key(test_cfg(service_url)).await.unwrap(); + + let expected_wallet: Address = + "0x0000000000000000000000000000000000000042".parse().unwrap(); + assert_eq!(outcome.chain_id, 4217); + assert_eq!(outcome.wallet_address, expected_wallet); + + let file = read_tempo_keys_file().expect("keys.toml written"); + assert_eq!(file.keys.len(), 1); + let entry = &file.keys[0]; + assert_eq!(entry.wallet_address, outcome.wallet_address); + assert_eq!(entry.key_address, Some(outcome.key_address)); + assert_eq!(entry.chain_id, 4217); + assert_eq!(entry.expiry, Some(9_999_999_999)); + let decoded: tempo_primitives::transaction::SignedKeyAuthorization = + crate::tempo::decode_key_authorization(entry.key_authorization.as_deref().unwrap()) + .expect("RLP roundtrip"); + assert_eq!(decoded.authorization.chain_id, 4217); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_rejects_wrong_chain_id() { + // Wallet returns chain 99999 but client requested 4217 → must reject + // and persist nothing, else discovery would later fail to find a key + // for the requested chain. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + let (service_url, server) = spawn_mock_wallet(99999).await; + let err = ensure_access_key(test_cfg(service_url)).await.unwrap_err(); + assert!( + err.to_string().contains("wallet authorized chain 99999 but 4217 was requested"), + "expected chain mismatch error, got: {err}" + ); + assert!(read_tempo_keys_file().is_none_or(|f| f.keys.is_empty())); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } +} diff --git a/crates/common/src/tempo/mod.rs b/crates/common/src/tempo/mod.rs index ec51dc607b5ab..ef8d0212bd453 100644 --- a/crates/common/src/tempo/mod.rs +++ b/crates/common/src/tempo/mod.rs @@ -1,8 +1,24 @@ //! Tempo network utilities. +pub mod auth; + +use crate::FoundryTransactionBuilder; +use alloy_network::Network; +use alloy_primitives::{Address, B256, Signature}; +use alloy_signer::Signer; +use eyre::{Context, Result}; +use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; +use std::sync::Arc; + mod keystore; + +pub(crate) use auth::is_known_tempo_endpoint; +pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key}; pub use keystore::*; +#[cfg(test)] +pub(crate) use keystore::test_env_mutex; + #[cfg(test)] mod tests; @@ -16,3 +32,173 @@ mod tests; /// /// See pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000; + +/// Gas sponsor configuration for Tempo fee-payer signatures. +#[derive(Clone, Debug)] +pub struct TempoSponsor { + sponsor: Address, + signer: Option>, + signature: Option, +} + +impl TempoSponsor { + pub const fn new( + sponsor: Address, + signer: Option>, + signature: Option, + ) -> Self { + Self { sponsor, signer, signature } + } + + pub const fn sponsor(&self) -> Address { + self.sponsor + } + + pub async fn attach_and_print( + &self, + tx: &mut N::TransactionRequest, + sender: Address, + ) -> Result + where + N::TransactionRequest: FoundryTransactionBuilder, + { + if self.sponsor == sender { + eyre::bail!( + "invalid Tempo sponsorship: sponsor {} must not equal transaction sender", + self.sponsor + ); + } + + let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| { + eyre::eyre!( + "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction" + ) + })?; + + let preview = TempoSponsorPreview { + sponsor: self.sponsor, + fee_token: tx.fee_token(), + valid_before: tx.valid_before().map(|v| v.get()), + valid_after: tx.valid_after().map(|v| v.get()), + digest, + }; + preview.print()?; + + let signature = if let Some(signature) = self.signature { + signature + } else if let Some(signer) = &self.signer { + signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")? + } else { + eyre::bail!("missing Tempo sponsor signature or signer") + }; + + let recovered = signature + .recover_address_from_prehash(&digest) + .context("failed to recover Tempo sponsor signature")?; + if recovered != self.sponsor { + eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor); + } + if recovered == sender { + eyre::bail!( + "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender" + ); + } + + tx.set_fee_payer_signature(signature); + Ok(preview) + } +} + +/// User-visible sponsor digest metadata for a single outgoing Tempo transaction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TempoSponsorPreview { + pub sponsor: Address, + pub fee_token: Option
, + pub valid_before: Option, + pub valid_after: Option, + pub digest: B256, +} + +impl TempoSponsorPreview { + pub fn print(&self) -> Result<()> { + crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?; + crate::sh_eprintln!( + "Tempo fee token: {}", + self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string()) + )?; + crate::sh_eprintln!( + "Tempo validity: after {}, before {}", + self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()), + self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string()) + )?; + crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?; + Ok(()) + } +} + +/// Resolves a `--tempo.sponsor-signer` URI into a Foundry wallet signer. +pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result { + let spec = spec.trim(); + let (scheme, value) = spec + .split_once("://") + .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value)) + .unwrap_or_else(|| (spec.to_ascii_lowercase(), "")); + + match scheme.as_str() { + "env" => { + if value.is_empty() { + eyre::bail!("env:// sponsor signer requires an environment variable name"); + } + let private_key = std::env::var(value) + .wrap_err_with(|| format!("{value} environment variable is required"))?; + foundry_wallets::utils::create_private_key_signer(&private_key) + } + "private-key" => { + if value.is_empty() { + eyre::bail!("private-key:// sponsor signer requires a private key"); + } + foundry_wallets::utils::create_private_key_signer(value) + } + "keystore" => { + if value.is_empty() { + eyre::bail!("keystore:// sponsor signer requires a keystore path"); + } + WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "account" => { + if value.is_empty() { + eyre::bail!("account:// sponsor signer requires an account name"); + } + WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() } + .signer() + .await + } + "ledger" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { ledger: true, raw, ..Default::default() }.signer().await + } + "trezor" => { + let raw = RawWalletOpts { + hd_path: (!value.is_empty()).then(|| value.to_string()), + ..Default::default() + }; + WalletOpts { trezor: true, raw, ..Default::default() }.signer().await + } + "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await, + "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await, + "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await, + "browser" => { + eyre::bail!( + "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer" + ) + } + _ => eyre::bail!( + "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY" + ), + } +} diff --git a/crates/common/src/transactions/builder.rs b/crates/common/src/transactions/builder.rs index de03cf3adc73e..aa4c971680d00 100644 --- a/crates/common/src/transactions/builder.rs +++ b/crates/common/src/transactions/builder.rs @@ -9,7 +9,9 @@ use alloy_primitives::{Address, B256, Signature, TxKind, U256}; use alloy_provider::Provider; use alloy_signer::Signer; use eyre::Result; +#[cfg(feature = "optimism")] use op_alloy_network::Optimism; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionRequest; use tempo_alloy::{TempoNetwork, provider::TempoProviderExt}; use tempo_primitives::{ @@ -244,6 +246,24 @@ pub trait FoundryTransactionBuilder: NetworkTransactionBuilder { /// on-chain as part of this transaction. fn set_key_authorization(&mut self, _key_authorization: SignedKeyAuthorization) {} + /// Embeds key authorization before gas estimation/signing if the access key is not yet + /// provisioned on-chain. + /// + /// This mirrors the mutation performed by [`Self::sign_with_access_key`], but makes the final + /// transaction body available before fee-payer sponsor digests are computed. + fn prepare_access_key_authorization<'a>( + &'a mut self, + _provider: &'a impl Provider, + _wallet_address: Address, + _key_address: Address, + _key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + async { Ok(()) } + } + /// Converts a CREATE transaction into an AA-compatible call entry. /// /// Tempo AA transactions use a `calls` list instead of `to`+`input`. Must be @@ -355,6 +375,7 @@ impl FoundryTransactionBuilder for ::Transact } } +#[cfg(feature = "optimism")] impl FoundryTransactionBuilder for OpTransactionRequest { fn reset_gas_limit(&mut self) { self.as_mut().gas = None; @@ -439,6 +460,35 @@ impl FoundryTransactionBuilder for ::Tran self.key_authorization = Some(key_authorization); } + fn prepare_access_key_authorization<'a>( + &'a mut self, + provider: &'a impl Provider, + wallet_address: Address, + key_address: Address, + key_authorization: Option<&'a SignedKeyAuthorization>, + ) -> impl Future> + Send + 'a + where + Self: Send, + { + let auth = key_authorization.cloned(); + + async move { + if let Some(auth) = auth { + let is_provisioned = provider + .get_keychain_key(wallet_address, key_address) + .await + .map(|info| info.keyId != Address::ZERO) + .unwrap_or(false); + + if !is_provisioned { + self.set_key_authorization(auth); + } + } + + Ok(()) + } + } + fn convert_create_to_call(&mut self) { if self.calls.is_empty() && self.inner.to.is_some_and(|to| to.is_create()) { let input = self.inner.input.input().cloned().unwrap_or_default(); @@ -473,7 +523,12 @@ impl FoundryTransactionBuilder for ::Tran let is_provisioned = provisioning_fut.await.map(|info| info.keyId != Address::ZERO).unwrap_or(false); - if !is_provisioned { + if !is_provisioned && self.key_authorization.is_none() { + if self.fee_payer_signature.is_some() { + eyre::bail!( + "cannot add Tempo key authorization after fee payer signature was attached" + ); + } self.set_key_authorization(auth); } } diff --git a/crates/common/src/transactions/receipt.rs b/crates/common/src/transactions/receipt.rs index 9ca6cb02b10ee..c2e34419248c4 100644 --- a/crates/common/src/transactions/receipt.rs +++ b/crates/common/src/transactions/receipt.rs @@ -7,6 +7,7 @@ use alloy_provider::{ use alloy_rpc_types::{BlockId, TransactionReceipt}; use eyre::Result; use foundry_common_fmt::{UIfmt, UIfmtReceiptExt, get_pretty_receipt_attr}; +#[cfg(feature = "optimism")] use op_alloy_rpc_types::OpTransactionReceipt; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionReceipt; @@ -23,6 +24,7 @@ impl FoundryReceiptResponse for TransactionReceipt { } } +#[cfg(feature = "optimism")] impl FoundryReceiptResponse for OpTransactionReceipt { fn set_contract_address(&mut self, contract_address: Address) { self.inner.contract_address = Some(contract_address); diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index bab59137b0130..8f63718e086cd 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -10,6 +10,10 @@ use std::path::PathBuf; pub struct FuzzConfig { /// The number of test cases that must execute for each property test pub runs: u32, + /// Optional 1-based fuzz run to execute. + pub run: Option, + /// Optional fuzz worker ID to pair with `run`. + pub worker: Option, /// Fails the fuzzed test if a revert occurs. pub fail_on_revert: bool, /// The maximum number of test case rejections allowed, @@ -37,6 +41,8 @@ impl Default for FuzzConfig { fn default() -> Self { Self { runs: 256, + run: None, + worker: None, fail_on_revert: true, max_test_rejects: 65536, seed: None, diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 270df14a6c291..000cefc26737a 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use crate::Config; use alloy_primitives::map::HashMap; use figment::{ @@ -5,6 +7,7 @@ use figment::{ value::{Dict, Map, Value}, }; use foundry_compilers::ProjectCompileOutput; +use foundry_evm_networks::NetworkVariant; use itertools::Itertools; mod natspec; @@ -123,6 +126,42 @@ impl InlineConfig { self.get_function(contract, function).is_some_and(|map| !map.is_empty()) } + /// Returns the configured [`NetworkVariant`] for a given test, checking function-level first + /// then contract-level. Returns `None` if no network annotation is present. + pub fn network_for( + &self, + profile: &Profile, + contract: &str, + function: &str, + ) -> Option { + let data = self.provide(contract, function).data().ok()?; + let dict = data.get(profile).or_else(|| data.get(&Profile::Default))?; + if let Some(Value::Dict(_, networks)) = dict.get("networks") + && let Some(Value::String(_, s)) = networks.get("network") + { + return s.parse().ok(); + } + None + } + + /// Returns all distinct [`NetworkVariant`]s referenced in any inline config annotation. + /// + /// This is used to determine whether a multi-network test pass is needed. + pub fn referenced_override_networks(&self, profile: &Profile) -> Vec { + let mut seen = BTreeSet::new(); + for (contract, function) in self.fn_level.keys() { + if let Some(v) = self.network_for(profile, contract, function) { + seen.insert(v); + } + } + for contract in self.contract_level.keys() { + if let Some(v) = self.network_for(profile, contract, "") { + seen.insert(v); + } + } + seen.into_iter().collect() + } + fn get_contract(&self, contract: &str) -> Option<&DataMap> { self.contract_level.get(contract) } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 1f5c35775f1dc..71eb2a96d7152 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -2883,19 +2883,35 @@ impl BasicConfig { /// /// This serializes to a table with the name of the profile pub fn to_string_pretty(&self) -> Result { - let mut value = toml::Value::try_from(self)?; + let mut profile_body = toml::Value::try_from(self)?; if let Some(ref network) = self.network - && let toml::Value::Table(ref mut table) = value + && let toml::Value::Table(ref mut table) = profile_body { - table.insert(network.clone(), toml::Value::Boolean(true)); + table.insert("network".to_string(), toml::Value::String(network.clone())); + } + + let mut profile_section = toml::value::Table::new(); + profile_section.insert(self.profile.to_string(), profile_body); + + let mut document = toml::value::Table::new(); + document.insert("profile".to_string(), toml::Value::Table(profile_section)); + + if self.network.as_deref() == Some("tempo") { + let mut endpoints = toml::value::Table::new(); + endpoints.insert( + "tempo".to_string(), + toml::Value::String("https://rpc.tempo.xyz/".to_string()), + ); + endpoints.insert( + "moderato".to_string(), + toml::Value::String("https://rpc.moderato.tempo.xyz/".to_string()), + ); + document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints)); } - let s = toml::to_string_pretty(&value)?; + + let body = toml::to_string_pretty(&toml::Value::Table(document))?; Ok(format!( - "\ -[profile.{}] -{s} -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n", - self.profile + "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n" )) } } @@ -6700,6 +6716,55 @@ mod tests { }); } + #[test] + fn no_unknown_key_warning_for_network_field() { + // Regression test: `network` is a flattened `Option` field of `NetworkConfigs`. It must + // not trigger an unknown-key warning, regardless of whether it is set. + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + network = "tempo" + "#, + )?; + + let cfg = Config::load().unwrap(); + assert!( + !cfg.warnings.iter().any( + |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network") + ), + "did not expect UnknownKey warning for `network`, got: {:?}", + cfg.warnings + ); + Ok(()) + }); + } + + #[test] + fn no_unknown_key_warning_for_legacy_tempo_alias() { + // Regression test: the legacy `tempo = true` alias must keep working without warnings. + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + tempo = true + "#, + )?; + + let cfg = Config::load().unwrap(); + assert!( + !cfg.warnings + .iter() + .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")), + "did not expect UnknownKey warning for `tempo`, got: {:?}", + cfg.warnings + ); + Ok(()) + }); + } + #[test] fn fails_on_ambiguous_version_in_compilation_restrictions() { figment::Jail::expect_with(|jail| { diff --git a/crates/config/src/providers/warnings.rs b/crates/config/src/providers/warnings.rs index 930066b29cf73..ff1d0b35def47 100644 --- a/crates/config/src/providers/warnings.rs +++ b/crates/config/src/providers/warnings.rs @@ -38,7 +38,10 @@ const DOC_KEYS: &[&str] = &["out", "title", "book", "homepage", "repository", "p const RESERVED_KEYS: &[&str] = &["extends"]; /// Keys kept for backward compatibility that should not trigger unknown key warnings. -const BACKWARD_COMPATIBLE_KEYS: &[&str] = &["solc_version"]; +/// +/// `tempo` and `optimism` are legacy aliases for `network = "tempo"` / `network = "optimism"` — +/// still accepted on input but no longer serialized in the default config. +const BACKWARD_COMPATIBLE_KEYS: &[&str] = &["solc_version", "tempo", "optimism"]; /// Generate warnings for unknown sections and deprecated keys pub struct WarningsProvider

{ diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index 3c8cad85bae10..cc3dabd32d4bf 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -29,3 +29,11 @@ ratatui = { version = "0.30", default-features = false, features = [ revm.workspace = true tracing.workspace = true serde.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 77020a88fe3cd..814beab402729 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -14,7 +14,6 @@ repository.workspace = true workspace = true [dependencies] -forge-fmt.workspace = true foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true @@ -29,8 +28,11 @@ mdbook-driver = { version = "0.5", default-features = false, features = ["search rayon.workspace = true serde_json.workspace = true serde.workspace = true -solang-parser.workspace = true thiserror.workspace = true toml.workspace = true tracing.workspace = true regex.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index 7ecada9d33e72..ae456e5de7e2c 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -1,6 +1,6 @@ use crate::{ AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor, - document::DocumentContent, helpers::merge_toml_table, solang_ext::Visitable, + document::DocumentContent, helpers::merge_toml_table, }; use alloy_primitives::map::HashMap; use eyre::{Context, Result}; @@ -129,41 +129,27 @@ impl DocBuilder { let gcx = compiler.gcx(); let documents = combined_sources .par_iter() - .enumerate() - .map(|(i, (path, from_library))| { + .map(|(path, from_library)| { let path = *path; let from_library = *from_library; let mut files = vec![]; // Read and parse source file - if let Some((_, ast)) = gcx.get_ast_source(path) - && let Some(source) = - forge_fmt::format_ast(gcx, ast, self.fmt.clone().into()) + if let Some((_, ast_source)) = gcx.get_ast_source(path) + && let Some(source_unit) = ast_source.ast.as_ref() { - let (mut source_unit, comments) = match solang_parser::parse(&source, i) { - Ok(res) => res, - Err(err) => { - if from_library { - // Ignore failures for library files - return Ok(files); - } - return Err(eyre::eyre!( - "Failed to parse Solidity code for {}\nDebug info: {:?}", - path.display(), - err - )); - } - }; + // Solar uses a global SourceMap: span BytePos values are global + // offsets, not per-file offsets. Subtract file.start_pos so that + // span-based indexing into the per-file source string is correct. + let source = ast_source.file.src.to_string(); + let file_start = ast_source.file.start_pos.to_usize(); - // Visit the parse tree - let mut doc = Parser::new(comments, source, self.fmt.tab_width); - source_unit - .visit(&mut doc) - .map_err(|err| eyre::eyre!("Failed to parse source: {err}"))?; + // Walk the solar AST directly + let doc = Parser::new(source, file_start, self.fmt.tab_width); + let all_items = doc.parse(source_unit); // Split the parsed items on top-level constants and rest. - let (items, consts): (Vec, Vec) = doc - .items() + let (items, consts): (Vec, Vec) = all_items .into_iter() .partition(|item| !matches!(item.source, ParseSource::Variable(_))); diff --git a/crates/doc/src/helpers.rs b/crates/doc/src/helpers.rs index 77f10329a88b8..7b36e2e257b71 100644 --- a/crates/doc/src/helpers.rs +++ b/crates/doc/src/helpers.rs @@ -1,25 +1,5 @@ -use itertools::Itertools; -use solang_parser::pt::FunctionDefinition; use toml::{Value, value::Table}; -/// Generates a function signature with parameter types (e.g., "functionName(type1,type2)"). -/// Returns the function name without parameters if the function has no parameters. -pub fn function_signature(func: &FunctionDefinition) -> String { - let func_name = func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.clone()); - if func.params.is_empty() { - return func_name; - } - - format!( - "{}({})", - func_name, - func.params - .iter() - .map(|p| p.1.as_ref().map(|p| p.ty.to_string()).unwrap_or_default()) - .join(",") - ) -} - /// Merge original toml table with the override. pub(crate) fn merge_toml_table(table: &mut Table, override_table: Table) { for (key, override_value) in override_table { @@ -46,74 +26,3 @@ pub(crate) fn merge_toml_table(table: &mut Table, override_table: Table) { }; } } - -#[cfg(test)] -mod tests { - use super::*; - use solang_parser::{ - parse, - pt::{ContractPart, SourceUnit, SourceUnitPart}, - }; - - #[test] - fn test_function_signature_no_params() { - let (source_unit, _) = parse( - r#" - contract Test { - function foo() public {} - } - "#, - 0, - ) - .unwrap(); - - let func = extract_function(&source_unit); - assert_eq!(function_signature(func), "foo"); - } - - #[test] - fn test_function_signature_with_params() { - let (source_unit, _) = parse( - r#" - contract Test { - function transfer(address to, uint256 amount) public {} - } - "#, - 0, - ) - .unwrap(); - - let func = extract_function(&source_unit); - assert_eq!(function_signature(func), "transfer(address,uint256)"); - } - - #[test] - fn test_function_signature_constructor() { - let (source_unit, _) = parse( - r#" - contract Test { - constructor(address owner) {} - } - "#, - 0, - ) - .unwrap(); - - let func = extract_function(&source_unit); - assert_eq!(function_signature(func), "constructor(address)"); - } - - /// Helper to extract the first function from a parsed source unit - fn extract_function(source_unit: &SourceUnit) -> &FunctionDefinition { - for part in &source_unit.0 { - if let SourceUnitPart::ContractDefinition(contract) = part { - for part in &contract.parts { - if let ContractPart::FunctionDefinition(func) = part { - return func; - } - } - } - } - panic!("No function found in source unit"); - } -} diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 847d58dfb1d8b..e50598cff83ac 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -22,6 +22,10 @@ mod helpers; mod parser; pub use parser::{ Comment, CommentTag, Comments, CommentsRef, ParseItem, ParseSource, Parser, error, + source::{ + BaseInfo, ContractKind, ContractSource, EnumSource, ErrorSource, EventSource, + FunctionSource, ParamInfo, StructSource, TypeSource, VariableAttr, VariableSource, + }, }; mod preprocessor; @@ -31,6 +35,3 @@ mod writer; pub use writer::{AsDoc, AsDocResult, BufWriter, Markdown}; pub use mdbook_driver; - -// old formatter dependencies -pub mod solang_ext; diff --git a/crates/doc/src/parser/comment.rs b/crates/doc/src/parser/comment.rs index 3e915b1b7f479..e70f47e174a81 100644 --- a/crates/doc/src/parser/comment.rs +++ b/crates/doc/src/parser/comment.rs @@ -1,6 +1,5 @@ use alloy_primitives::map::HashMap; use derive_more::{Deref, DerefMut, derive::Display}; -use solang_parser::doccomment::DocCommentTag; /// The natspec comment tag explaining the purpose of the comment. /// See: . @@ -70,10 +69,10 @@ impl Comment { Self { tag, value } } - /// Create new instance of [Comment] from [DocCommentTag] + /// Create new instance of [Comment] from a tag string and value, /// if it has a valid natspec tag. - pub fn from_doc_comment(value: DocCommentTag) -> Option { - CommentTag::from_str(&value.tag).map(|tag| Self { tag, value: value.value }) + pub fn from_tag_and_value(tag: &str, value: String) -> Option { + CommentTag::from_str(tag).map(|tag| Self { tag, value }) } /// Split the comment at first word. @@ -145,9 +144,70 @@ impl Comments { } } -impl From> for Comments { - fn from(value: Vec) -> Self { - Self(value.into_iter().filter_map(Comment::from_doc_comment).collect()) +impl Comments { + /// Parse natspec comments from raw doc comment lines. + /// + /// Each line should be the raw text content of a `///` or `/** */` doc comment + /// with the comment delimiters already stripped (as provided by solar's `DocComment::symbol`). + /// + /// Natspec tags start with `@` (e.g. `@notice`, `@dev`, `@param`). + /// Lines without a tag at the start are treated as continuations of the previous tag, + /// or as `@notice` if no previous tag exists. + pub fn from_doc_lines(lines: impl IntoIterator>) -> Self { + let mut comments = Vec::new(); + let mut current_tag: Option = None; + let mut current_value = String::new(); + + let flush = |tag: &Option, value: &str, out: &mut Vec| { + let value = value.trim(); + if value.is_empty() && tag.is_none() { + return; + } + let tag_str = tag.as_deref().unwrap_or("notice"); + // Filter out `@solidity` tags and empty tags + if tag_str.trim() == "solidity" || tag_str.trim().is_empty() { + return; + } + if let Some(c) = Comment::from_tag_and_value(tag_str, value.to_string()) { + out.push(c); + } + }; + + for raw_line in lines { + let raw = raw_line.as_ref(); + // For block comments, process each line individually + for line in raw.lines() { + let trimmed = line.trim().trim_start_matches('*').trim(); + + if let Some(rest) = trimmed.strip_prefix('@') { + // Flush previous + flush(¤t_tag, ¤t_value, &mut comments); + // Parse new tag + let (tag, value) = rest.split_once(char::is_whitespace).unwrap_or((rest, "")); + current_tag = Some(tag.to_string()); + current_value = value.trim().to_string(); + } else if !trimmed.is_empty() { + // Continuation of current tag + if current_value.is_empty() { + current_value = trimmed.to_string(); + } else { + current_value.push('\n'); + current_value.push_str(trimmed); + } + } + } + } + + // Flush last + flush(¤t_tag, ¤t_value, &mut comments); + + Self(comments) + } +} + +impl From> for Comments { + fn from(value: Vec) -> Self { + Self(value) } } diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index b50c0ccae9f4e..cca36d47301c8 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -1,12 +1,12 @@ -use crate::{Comments, helpers::function_signature, solang_ext::SafeUnwrap}; -use solang_parser::pt::{ - ContractDefinition, ContractTy, EnumDefinition, ErrorDefinition, EventDefinition, - FunctionDefinition, StructDefinition, TypeDefinition, VariableDefinition, +use crate::Comments; + +pub use super::source::{ + ContractSource, EnumSource, ErrorSource, EventSource, FunctionSource, ParseSource, + StructSource, VariableSource, }; -use std::ops::Range; /// The parsed item. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseItem { /// The parse tree source. pub source: ParseSource, @@ -18,12 +18,12 @@ pub struct ParseItem { pub code: String, } -/// Defines a method that filters [ParseItem]'s children and returns the source pt token of the +/// Defines a method that filters [ParseItem]'s children and returns the source data of the /// children matching the target variant as well as its comments. /// Returns [Option::None] if no children matching the variant are found. macro_rules! filter_children_fn { ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { - /// Filter children items for [ParseSource::$variant] variants. + /// Filter children items for matching variants. $vis fn $name(&self) -> Option> { let items = self.children.iter().filter_map(|item| match item.source { ParseSource::$variant(ref inner) => Some((inner, &item.comments, &item.code)), @@ -39,12 +39,11 @@ macro_rules! filter_children_fn { }; } -/// Defines a method that returns [ParseSource] inner element if it matches -/// the variant +/// Defines a method that returns the inner element if it matches the variant. macro_rules! as_inner_source { ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { - /// Return inner element if it matches $variant. - /// If the element doesn't match, returns [None] + /// Return inner element if it matches the variant. + /// If the element doesn't match, returns [None]. $vis fn $name(&self) -> Option<&$ret> { match self.source { ParseSource::$variant(ref inner) => Some(inner), @@ -77,38 +76,16 @@ impl ParseItem { self } - /// Set the source code of this [ParseItem]. - /// - /// The parameter should be the full source file where this parse item originated from. - pub fn with_code(mut self, source: &str, tab_width: usize) -> Self { - let mut code = source[self.source.range()].to_string(); - - // Special function case, add `;` at the end of definition. - if let ParseSource::Function(_) | ParseSource::Error(_) | ParseSource::Event(_) = - self.source - { - code.push(';'); - } - - // Remove extra indent from source lines. - let prefix = &" ".repeat(tab_width); - self.code = code - .lines() - .map(|line| line.strip_prefix(prefix).unwrap_or(line)) - .collect::>() - .join("\n"); + /// Set the code string for this [ParseItem]. + pub fn with_code(mut self, code: String) -> Self { + self.code = code; self } /// Format the item's filename. pub fn filename(&self) -> String { - let prefix = match self.source { - ParseSource::Contract(ref c) => match c.ty { - ContractTy::Contract(_) => "contract", - ContractTy::Abstract(_) => "abstract", - ContractTy::Interface(_) => "interface", - ContractTy::Library(_) => "library", - }, + let prefix = match &self.source { + ParseSource::Contract(c) => c.kind.as_str(), ParseSource::Function(_) => "function", ParseSource::Variable(_) => "variable", ParseSource::Event(_) => "event", @@ -121,77 +98,14 @@ impl ParseItem { format!("{prefix}.{ident}.md") } - filter_children_fn!(pub fn variables(&self, Variable) -> VariableDefinition); - filter_children_fn!(pub fn functions(&self, Function) -> FunctionDefinition); - filter_children_fn!(pub fn events(&self, Event) -> EventDefinition); - filter_children_fn!(pub fn errors(&self, Error) -> ErrorDefinition); - filter_children_fn!(pub fn structs(&self, Struct) -> StructDefinition); - filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); + filter_children_fn!(pub fn variables(&self, Variable) -> VariableSource); + filter_children_fn!(pub fn functions(&self, Function) -> FunctionSource); + filter_children_fn!(pub fn events(&self, Event) -> EventSource); + filter_children_fn!(pub fn errors(&self, Error) -> ErrorSource); + filter_children_fn!(pub fn structs(&self, Struct) -> StructSource); + filter_children_fn!(pub fn enums(&self, Enum) -> EnumSource); - as_inner_source!(pub fn as_contract(&self, Contract) -> ContractDefinition); - as_inner_source!(pub fn as_variable(&self, Variable) -> VariableDefinition); - as_inner_source!(pub fn as_function(&self, Function) -> FunctionDefinition); -} - -/// A wrapper type around pt token. -#[derive(Clone, Debug, PartialEq, Eq)] -#[allow(clippy::large_enum_variant)] -pub enum ParseSource { - /// Source contract definition. - Contract(Box), - /// Source function definition. - Function(FunctionDefinition), - /// Source variable definition. - Variable(VariableDefinition), - /// Source event definition. - Event(EventDefinition), - /// Source error definition. - Error(ErrorDefinition), - /// Source struct definition. - Struct(StructDefinition), - /// Source enum definition. - Enum(EnumDefinition), - /// Source type definition. - Type(TypeDefinition), -} - -impl ParseSource { - /// Get the identity of the source - pub fn ident(&self) -> String { - match self { - Self::Contract(contract) => contract.name.safe_unwrap().name.clone(), - Self::Variable(var) => var.name.safe_unwrap().name.clone(), - Self::Event(event) => event.name.safe_unwrap().name.clone(), - Self::Error(error) => error.name.safe_unwrap().name.clone(), - Self::Struct(structure) => structure.name.safe_unwrap().name.clone(), - Self::Enum(enumerable) => enumerable.name.safe_unwrap().name.clone(), - Self::Function(func) => { - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.clone()) - } - Self::Type(ty) => ty.name.name.clone(), - } - } - - /// Get the signature of the source (for functions, includes parameter types) - pub fn signature(&self) -> String { - match self { - Self::Function(func) => function_signature(func), - _ => self.ident(), - } - } - - /// Get the range of this item in the source file. - pub fn range(&self) -> Range { - match self { - Self::Contract(contract) => contract.loc, - Self::Variable(var) => var.loc, - Self::Event(event) => event.loc, - Self::Error(error) => error.loc, - Self::Struct(structure) => structure.loc, - Self::Enum(enumerable) => enumerable.loc, - Self::Function(func) => func.loc_prototype, - Self::Type(ty) => ty.loc, - } - .range() - } + as_inner_source!(pub fn as_contract(&self, Contract) -> ContractSource); + as_inner_source!(pub fn as_variable(&self, Variable) -> VariableSource); + as_inner_source!(pub fn as_function(&self, Function) -> FunctionSource); } diff --git a/crates/doc/src/parser/mod.rs b/crates/doc/src/parser/mod.rs index 76da5d032382d..124d2aa3ed576 100644 --- a/crates/doc/src/parser/mod.rs +++ b/crates/doc/src/parser/mod.rs @@ -1,19 +1,12 @@ //! The parser module. -use crate::solang_ext::{Visitable, Visitor}; -use itertools::Itertools; -use solang_parser::{ - doccomment::{DocComment, parse_doccomments}, - pt::{ - Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition, - FunctionDefinition, Identifier, Loc, SourceUnit, SourceUnitPart, StructDefinition, - TypeDefinition, VariableDefinition, - }, -}; +use solar::parse::ast; /// Parser error. pub mod error; -use error::{ParserError, ParserResult}; + +/// Owned source types. +pub mod source; /// Parser item. mod item; @@ -23,206 +16,274 @@ pub use item::{ParseItem, ParseSource}; mod comment; pub use comment::{Comment, CommentTag, Comments, CommentsRef}; -/// The documentation parser. This type implements a [Visitor] trait. +use source::*; + +/// The documentation parser. /// -/// While walking the parse tree, [Parser] will collect relevant source items and corresponding -/// doc comments. The resulting [ParseItem]s can be accessed by calling [Parser::items]. -#[derive(Debug, Default)] +/// Walks the solar AST and extracts [`ParseItem`]s with owned source data and doc comments. +#[derive(Debug)] pub struct Parser { - /// Initial comments from solang parser. - comments: Vec, - /// Parser context. - context: ParserContext, /// Parsed results. items: Vec, - /// Source file. + /// The source code string of the file being parsed. source: String, + /// The global byte offset of this file's first byte in solar's source map. + /// + /// Solar uses a global `SourceMap` where each file's `BytePos` values start at + /// `file.start_pos` rather than 0. All span `lo`/`hi` values must be offset by this + /// amount before indexing into `self.source`. + file_start: usize, /// Tab width used to format code. tab_width: usize, } -/// [Parser] context. -#[derive(Debug, Default)] -struct ParserContext { - /// Current visited parent. - parent: Option, - /// Current start pointer for parsing doc comments. - doc_start_loc: usize, -} - impl Parser { /// Create a new instance of [Parser]. - pub fn new(comments: Vec, source: String, tab_width: usize) -> Self { - Self { comments, source, tab_width, ..Default::default() } + /// + /// `file_start` is `ast_source.file.start_pos.to_usize()` — the offset of the file's + /// first byte in solar's global source map. Pass `0` in tests where you parse directly. + pub const fn new(source: String, file_start: usize, tab_width: usize) -> Self { + Self { items: Vec::new(), source, file_start, tab_width } } - /// Return the parsed items. Consumes the parser. - pub fn items(self) -> Vec { + /// Parse a solar source unit and return the parsed items. + pub fn parse(mut self, source_unit: &ast::SourceUnit<'_>) -> Vec { + for item in source_unit.items.iter() { + if let Some(parsed) = self.parse_item(item) { + self.items.push(parsed); + } + } self.items } - /// Visit the children elements with parent context. - /// This function memoizes the previous parent, sets the context - /// to a new one and invokes a visit function. The context will be reset - /// to the previous parent at the end of the function. - fn with_parent( - &mut self, - mut parent: ParseItem, - mut visit: impl FnMut(&mut Self) -> ParserResult<()>, - ) -> ParserResult { - let curr = self.context.parent.take(); - self.context.parent = Some(parent); - visit(self)?; - parent = self.context.parent.take().unwrap(); - self.context.parent = curr; - Ok(parent) - } - - /// Adds a child element to the parent item if it exists. - /// Otherwise the element will be added to a top-level items collection. - /// Moves the doc comment pointer to the end location of the child element. - fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) -> ParserResult<()> { - let child = self.new_item(source, loc.start())?; - if let Some(parent) = self.context.parent.as_mut() { - parent.children.push(child); - } else { - self.items.push(child); + /// Parse a single solar AST item into a [ParseItem]. + fn parse_item(&self, item: &ast::Item<'_>) -> Option { + let docs = Self::parse_docs(&item.docs); + let span = item.span; + + match &item.kind { + ast::ItemKind::Contract(contract) => { + let source = self.parse_contract(contract); + let code = self.extract_code(span); + let mut parse_item = + ParseItem::new(ParseSource::Contract(source)).with_comments(docs); + + // Parse children + let mut children = Vec::new(); + for child in contract.body.iter() { + if let Some(parsed) = self.parse_item(child) { + children.push(parsed); + } + } + parse_item.children = children; + parse_item.code = code; + Some(parse_item) + } + ast::ItemKind::Function(func) => { + let source = self.parse_function(func); + let code = self.extract_prototype_code(func); + Some( + ParseItem::new(ParseSource::Function(source)) + .with_comments(docs) + .with_code(code), + ) + } + ast::ItemKind::Variable(var) => { + let source = self.parse_variable(var); + let code = self.extract_code(span); + Some( + ParseItem::new(ParseSource::Variable(source)) + .with_comments(docs) + .with_code(code), + ) + } + ast::ItemKind::Event(event) => { + let source = self.parse_event(event); + let code = self.extract_code(span); + Some(ParseItem::new(ParseSource::Event(source)).with_comments(docs).with_code(code)) + } + ast::ItemKind::Error(err) => { + let source = self.parse_error(err); + let code = self.extract_code(span); + Some(ParseItem::new(ParseSource::Error(source)).with_comments(docs).with_code(code)) + } + ast::ItemKind::Struct(strukt) => { + let source = self.parse_struct(strukt); + let code = self.extract_code(span); + Some( + ParseItem::new(ParseSource::Struct(source)).with_comments(docs).with_code(code), + ) + } + ast::ItemKind::Enum(enm) => { + let source = self.parse_enum(enm); + let code = self.extract_code(span); + Some(ParseItem::new(ParseSource::Enum(source)).with_comments(docs).with_code(code)) + } + ast::ItemKind::Udvt(udvt) => { + let source = TypeSource { name: udvt.name.to_string() }; + let code = self.extract_code(span); + Some(ParseItem::new(ParseSource::Type(source)).with_comments(docs).with_code(code)) + } + // Skip pragmas, imports, using directives + _ => None, } - self.context.doc_start_loc = loc.end(); - Ok(()) } - /// Create new [ParseItem] with comments and formatted code. - fn new_item(&mut self, source: ParseSource, loc_start: usize) -> ParserResult { - let docs = self.parse_docs(loc_start)?; - Ok(ParseItem::new(source).with_comments(docs).with_code(&self.source, self.tab_width)) + fn parse_contract(&self, contract: &ast::ItemContract<'_>) -> ContractSource { + let kind = match contract.kind { + ast::ContractKind::Contract => ContractKind::Contract, + ast::ContractKind::AbstractContract => ContractKind::Abstract, + ast::ContractKind::Interface => ContractKind::Interface, + ast::ContractKind::Library => ContractKind::Library, + }; + + let bases = contract + .bases + .iter() + .map(|base| { + let full_name = base.name.to_string(); + let ident = base.name.last().name.to_string(); + BaseInfo { name: full_name, ident } + }) + .collect(); + + ContractSource { name: contract.name.to_string(), kind, bases } } - /// Parse the doc comments from the current start location. - fn parse_docs(&mut self, end: usize) -> ParserResult { - self.parse_docs_range(self.context.doc_start_loc, end) + fn parse_function(&self, func: &ast::ItemFunction<'_>) -> FunctionSource { + let name = func.header.name.map(|n| n.to_string()); + let kind = func.kind.to_string(); + let params = self.parse_var_defs(&func.header.parameters); + let returns = + func.header.returns.as_deref().map(|r| self.parse_var_defs(r)).unwrap_or_default(); + FunctionSource { name, kind, params, returns } } - /// Parse doc comments from the within specified range. - fn parse_docs_range(&mut self, start: usize, end: usize) -> ParserResult { - let mut res = vec![]; - for comment in parse_doccomments(&self.comments, start, end) { - match comment { - DocComment::Line { comment } => res.push(comment), - DocComment::Block { comments } => res.extend(comments), + fn parse_variable(&self, var: &ast::VariableDefinition<'_>) -> VariableSource { + let name = var.name.map(|n| n.to_string()).unwrap_or_default(); + + let mut attrs = Vec::new(); + if let Some(m) = var.mutability { + match m { + ast::VarMut::Constant => attrs.push(VariableAttr::Constant), + ast::VarMut::Immutable => attrs.push(VariableAttr::Immutable), } } - // Filter out `@solidity` and empty tags - // See https://docs.soliditylang.org/en/v0.8.17/assembly.html#memory-safety - let res = res - .into_iter() - .filter(|c| c.tag.trim() != "solidity" && !c.tag.trim().is_empty()) - .collect_vec(); - Ok(res.into()) + VariableSource { name, attrs } } -} -impl Visitor for Parser { - type Error = ParserError; - - fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> ParserResult<()> { - for source in &mut source_unit.0 { - match source { - SourceUnitPart::ContractDefinition(def) => { - // Create new contract parse item. - let contract = - self.new_item(ParseSource::Contract(def.clone()), def.loc.start())?; - - // Move the doc pointer to the contract location start. - self.context.doc_start_loc = def.loc.start(); - - // Parse child elements with current contract as parent - let contract = self.with_parent(contract, |doc| { - def.parts - .iter_mut() - .map(|d| d.visit(doc)) - .collect::>>()?; - Ok(()) - })?; - - // Move the doc pointer to the contract location end. - self.context.doc_start_loc = def.loc.end(); - - // Add contract to the parsed items. - self.items.push(contract); - } - SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, - SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, - SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?, - SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, - SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, - SourceUnitPart::VariableDefinition(var) => self.visit_var_definition(var)?, - SourceUnitPart::TypeDefinition(ty) => self.visit_type_definition(ty)?, - _ => {} - }; - } + fn parse_event(&self, event: &ast::ItemEvent<'_>) -> EventSource { + let fields = self.parse_var_defs(&event.parameters); + EventSource { name: event.name.to_string(), fields } + } - Ok(()) + fn parse_error(&self, err: &ast::ItemError<'_>) -> ErrorSource { + let fields = self.parse_var_defs(&err.parameters); + ErrorSource { name: err.name.to_string(), fields } } - fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc) + fn parse_struct(&self, strukt: &ast::ItemStruct<'_>) -> StructSource { + let fields = self.parse_var_defs(strukt.fields); + StructSource { name: strukt.name.to_string(), fields } } - fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc) + /// Parse a list of variable definitions into [ParamInfo]. + fn parse_var_defs(&self, vars: &[ast::VariableDefinition<'_>]) -> Vec { + vars.iter() + .map(|v| ParamInfo { name: v.name.map(|n| n.to_string()), ty: self.type_string(&v.ty) }) + .collect() } - fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> { - // If the function parameter doesn't have a name, try to set it with - // `@custom:name` tag if any was provided - let mut start_loc = func.loc.start(); - for (loc, param) in &mut func.params { - if let Some(param) = param - && param.name.is_none() - { - let docs = self.parse_docs_range(start_loc, loc.end())?; - let name_tag = docs.iter().find(|c| c.tag == CommentTag::Custom("name".to_owned())); - if let Some(name_tag) = name_tag - && let Some(name) = name_tag.value.trim().split(' ').next() - { - param.name = Some(Identifier { loc: Loc::Implicit, name: name.to_owned() }) - } - } - start_loc = loc.end(); + /// Extract the type as a string from the source code. + fn type_string(&self, ty: &ast::Type<'_>) -> String { + let lo = ty.span.lo().to_usize().saturating_sub(self.file_start); + let hi = ty.span.hi().to_usize().saturating_sub(self.file_start); + if lo < self.source.len() && hi <= self.source.len() && lo < hi { + self.source[lo..hi].to_string() + } else { + String::new() } + } - self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc) + fn parse_enum(&self, enm: &ast::ItemEnum<'_>) -> EnumSource { + let variants = enm.variants.iter().map(|v| v.to_string()).collect(); + EnumSource { name: enm.name.to_string(), variants } } - fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc) + /// Parse doc comments from solar's [ast::DocComments] into our [Comments] type. + fn parse_docs(docs: &ast::DocComments<'_>) -> Comments { + if docs.is_empty() { + return Comments::default(); + } + Comments::from_doc_lines(docs.iter().map(|d| d.symbol.as_str())) } - fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc) + /// Extract a code snippet from the source for the given span. + fn extract_code(&self, span: ast::Span) -> String { + let lo = span.lo().to_usize().saturating_sub(self.file_start); + let hi = span.hi().to_usize().saturating_sub(self.file_start); + if lo < self.source.len() && hi <= self.source.len() && lo < hi { + let code = &self.source[lo..hi]; + self.dedent(code) + } else { + String::new() + } } - fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc) + /// Extract only the function prototype (excluding the body) from the source. + fn extract_prototype_code(&self, func: &ast::ItemFunction<'_>) -> String { + let lo = func.header.span.lo().to_usize().saturating_sub(self.file_start); + let hi = func.header.span.hi().to_usize().saturating_sub(self.file_start); + if lo < self.source.len() && hi <= self.source.len() && lo < hi { + let mut code = self.source[lo..hi].to_string(); + code.push(';'); + self.dedent(&code) + } else { + String::new() + } } - fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Type(def.clone()), def.loc) + /// Remove one level of indentation from code. + fn dedent(&self, code: &str) -> String { + let prefix = &" ".repeat(self.tab_width); + code.lines() + .map(|line| line.strip_prefix(prefix).unwrap_or(line)) + .collect::>() + .join("\n") } } #[cfg(test)] mod tests { use super::*; - use solang_parser::parse; fn parse_source(src: &str) -> Vec { - let (mut source, comments) = parse(src, 0).expect("failed to parse source"); - let mut doc = Parser::new(comments, src.to_owned(), 4); - source.visit(&mut doc).expect("failed to visit source"); - doc.items() + use solar::parse::{ + Parser as SolarParser, + ast::{Arena, interface}, + interface::Session, + }; + + let sess = + Session::builder().with_silent_emitter(Some("test parse failed".to_string())).build(); + + sess.enter(|| -> Vec { + let arena = Arena::new(); + let mut parser = SolarParser::from_source_code( + &sess, + &arena, + interface::source_map::FileName::Custom("test".to_string()), + src.to_string(), + ) + .expect("failed to create parser"); + + let source_unit = parser.parse_file().map_err(|e| e.emit()).expect("failed to parse"); + + // file_start=0: when parsing directly, solar's BytePos values start at 0. + let doc = Parser::new(src.to_string(), 0, 4); + doc.parse(&source_unit) + }) } macro_rules! test_single_unit { @@ -339,23 +400,39 @@ mod tests { assert!(matches!(fallback.source, ParseSource::Function(_))); } + #[test] + fn overloaded_function_signatures() { + let items = parse_source( + r" + interface IFoo { + function process(address addr) external; + function process(address[] calldata addrs) external; + function process(address addr, uint256 value) external; + } + ", + ); + assert_eq!(items.len(), 1); + let contract = items.first().unwrap(); + assert_eq!(contract.children.len(), 3); + let sigs: Vec = contract.children.iter().map(|ch| ch.source.signature()).collect(); + assert_eq!(sigs[0], "process(address)", "first overload"); + assert_eq!(sigs[1], "process(address[])", "second overload (array)"); + assert_eq!(sigs[2], "process(address,uint256)", "third overload"); + } + #[test] fn contract_with_doc_comments() { let items = parse_source( r" pragma solidity ^0.8.19; - /// @name Test - /// no tag - ///@notice Cool contract - /// @ dev This is not a dev tag + /// @notice Cool contract /** * @dev line one * line 2 */ contract Test { - /** my function - i like whitespace - */ + /// my function + /// i like whitespace function test() {} } ", diff --git a/crates/doc/src/parser/source.rs b/crates/doc/src/parser/source.rs new file mode 100644 index 0000000000000..5a2fbddf43d5a --- /dev/null +++ b/crates/doc/src/parser/source.rs @@ -0,0 +1,172 @@ +//! Owned source types extracted from the parse tree for documentation generation. +//! +//! These types hold only the data needed by the doc writer and preprocessors, +//! avoiding lifetime dependencies on the parser's arena-allocated AST. + +/// Information about a parameter, struct field, event/error parameter, etc. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParamInfo { + /// The parameter name, if any. + pub name: Option, + /// The type rendered as a string (e.g. `"uint256"`, `"address"`). + pub ty: String, +} + +/// A base contract reference (e.g. `is IERC721, Ownable`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BaseInfo { + /// The full dotted name (e.g. `"IERC721"` or `"SomeLib.Base"`). + pub name: String, + /// The last identifier in the path, used for linking. + pub ident: String, +} + +/// The kind of a contract-like definition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ContractKind { + Contract, + Abstract, + Interface, + Library, +} + +impl ContractKind { + /// Returns the lowercase keyword string. + pub const fn as_str(&self) -> &'static str { + match self { + Self::Contract => "contract", + Self::Abstract => "abstract", + Self::Interface => "interface", + Self::Library => "library", + } + } +} + +/// Owned contract definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ContractSource { + pub name: String, + pub kind: ContractKind, + pub bases: Vec, +} + +/// Owned function definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FunctionSource { + /// The function name, or `None` for unnamed functions (fallback/receive). + pub name: Option, + /// The function kind as a display string (e.g. `"function"`, `"constructor"`, `"fallback"`). + pub kind: String, + /// Function parameters. + pub params: Vec, + /// Return parameters. + pub returns: Vec, +} + +impl FunctionSource { + /// Get the signature of the function, including parameter types. + pub fn signature(&self) -> String { + let name = self.name.as_deref().unwrap_or(&self.kind); + if self.params.is_empty() { + return name.to_string(); + } + format!( + "{}({})", + name, + self.params.iter().map(|p| p.ty.as_str()).collect::>().join(",") + ) + } +} + +/// Variable attribute relevant for doc generation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VariableAttr { + Constant, + Immutable, +} + +/// Owned variable definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VariableSource { + pub name: String, + pub attrs: Vec, +} + +/// Owned event definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EventSource { + pub name: String, + pub fields: Vec, +} + +/// Owned error definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ErrorSource { + pub name: String, + pub fields: Vec, +} + +/// Owned struct definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StructSource { + pub name: String, + pub fields: Vec, +} + +/// Owned enum definition data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EnumSource { + pub name: String, + pub variants: Vec, +} + +/// Owned type definition data (user-defined value types). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TypeSource { + pub name: String, +} + +/// A wrapper type around owned source data extracted from the parse tree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ParseSource { + /// Source contract definition. + Contract(ContractSource), + /// Source function definition. + Function(FunctionSource), + /// Source variable definition. + Variable(VariableSource), + /// Source event definition. + Event(EventSource), + /// Source error definition. + Error(ErrorSource), + /// Source struct definition. + Struct(StructSource), + /// Source enum definition. + Enum(EnumSource), + /// Source type definition. + Type(TypeSource), +} + +impl ParseSource { + /// Get the identity of the source. + pub fn ident(&self) -> String { + match self { + Self::Contract(c) => c.name.clone(), + Self::Variable(v) => v.name.clone(), + Self::Event(e) => e.name.clone(), + Self::Error(e) => e.name.clone(), + Self::Struct(s) => s.name.clone(), + Self::Enum(e) => e.name.clone(), + Self::Function(f) => f.name.clone().unwrap_or_else(|| f.kind.clone()), + Self::Type(t) => t.name.clone(), + } + } + + /// Get the signature of the source (for functions, includes parameter types). + pub fn signature(&self) -> String { + match self { + Self::Function(f) => f.signature(), + _ => self.ident(), + } + } +} diff --git a/crates/doc/src/preprocessor/contract_inheritance.rs b/crates/doc/src/preprocessor/contract_inheritance.rs index 1c2159131b01a..5df27c12a4e72 100644 --- a/crates/doc/src/preprocessor/contract_inheritance.rs +++ b/crates/doc/src/preprocessor/contract_inheritance.rs @@ -1,7 +1,5 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{ - Document, ParseSource, PreprocessorOutput, document::DocumentContent, solang_ext::SafeUnwrap, -}; +use crate::{Document, ParseSource, PreprocessorOutput, document::DocumentContent}; use alloy_primitives::map::HashMap; use std::path::PathBuf; @@ -11,7 +9,7 @@ pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inh /// The contract inheritance preprocessor. /// /// It matches the documents with inner [`ParseSource::Contract`](crate::ParseSource) elements, -/// iterates over their [Base](solang_parser::pt::Base)s and attempts +/// iterates over their base contracts and attempts /// to link them with the paths of the other contract documents. /// /// This preprocessor writes to [Document]'s context. @@ -34,8 +32,8 @@ impl Preprocessor for ContractInheritance { let mut links = HashMap::default(); // Attempt to match bases to other contracts - for base in &contract.base { - let base_ident = base.name.identifiers.last().unwrap().name.clone(); + for base in &contract.bases { + let base_ident = base.ident.clone(); if let Some(linked) = self.try_link_base(&base_ident, &documents) { links.insert(base_ident, linked); } @@ -60,7 +58,7 @@ impl ContractInheritance { } if let DocumentContent::Single(ref item) = candidate.content && let ParseSource::Contract(ref contract) = item.source - && base == contract.name.safe_unwrap().name + && base == contract.name { return Some(candidate.relative_output_path().to_path_buf()); } diff --git a/crates/doc/src/preprocessor/infer_hyperlinks.rs b/crates/doc/src/preprocessor/infer_hyperlinks.rs index 925a7cc8a2259..6fdda0e40d290 100644 --- a/crates/doc/src/preprocessor/infer_hyperlinks.rs +++ b/crates/doc/src/preprocessor/infer_hyperlinks.rs @@ -1,5 +1,5 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{Comments, Document, ParseItem, ParseSource, solang_ext::SafeUnwrap}; +use crate::{Comments, Document, ParseItem, ParseSource}; use regex::{Captures, Match, Regex}; use std::{ borrow::Cow, @@ -84,7 +84,7 @@ impl InferInlineHyperlinks { for item in items { match &item.source { ParseSource::Contract(contract) => { - let name = &contract.name.safe_unwrap().name; + let name = &contract.name; if name == link.identifier { if link.part.is_none() { return Some(InlineLinkTarget::borrowed( @@ -102,11 +102,8 @@ impl InferInlineHyperlinks { // have so we can match the correct one if let Some(id) = &fun.name { // Note: constructors don't have a name - if id.name == link.ref_name() { - return Some(InlineLinkTarget::borrowed( - &id.name, - target_path.to_path_buf(), - )); + if id == link.ref_name() { + return Some(InlineLinkTarget::borrowed(id, target_path.to_path_buf())); } } else if link.ref_name() == "constructor" { return Some(InlineLinkTarget::borrowed( @@ -117,7 +114,7 @@ impl InferInlineHyperlinks { } ParseSource::Variable(_) => {} ParseSource::Event(ev) => { - let ev_name = &ev.name.safe_unwrap().name; + let ev_name = &ev.name; if ev_name == link.ref_name() { return Some(InlineLinkTarget::borrowed( ev_name, @@ -126,7 +123,7 @@ impl InferInlineHyperlinks { } } ParseSource::Error(err) => { - let err_name = &err.name.safe_unwrap().name; + let err_name = &err.name; if err_name == link.ref_name() { return Some(InlineLinkTarget::borrowed( err_name, @@ -135,7 +132,7 @@ impl InferInlineHyperlinks { } } ParseSource::Struct(structdef) => { - let struct_name = &structdef.name.safe_unwrap().name; + let struct_name = &structdef.name; if struct_name == link.ref_name() { return Some(InlineLinkTarget::borrowed( struct_name, diff --git a/crates/doc/src/preprocessor/inheritdoc.rs b/crates/doc/src/preprocessor/inheritdoc.rs index cc18bdfbd83fb..15d190f83d49c 100644 --- a/crates/doc/src/preprocessor/inheritdoc.rs +++ b/crates/doc/src/preprocessor/inheritdoc.rs @@ -1,7 +1,6 @@ use super::{Preprocessor, PreprocessorId}; use crate::{ Comments, Document, ParseItem, ParseSource, PreprocessorOutput, document::DocumentContent, - solang_ext::SafeUnwrap, }; use alloy_primitives::map::HashMap; @@ -72,7 +71,7 @@ impl Inheritdoc { for candidate in documents { if let DocumentContent::Single(ref item) = candidate.content && let ParseSource::Contract(ref contract) = item.source - && base == contract.name.safe_unwrap().name + && base == contract.name { // Not matched for the contract because it's a noop // https://docs.soliditylang.org/en/v0.8.17/natspec-format.html#tags diff --git a/crates/doc/src/solang_ext/ast_eq.rs b/crates/doc/src/solang_ext/ast_eq.rs deleted file mode 100644 index 1252ac4eb32bf..0000000000000 --- a/crates/doc/src/solang_ext/ast_eq.rs +++ /dev/null @@ -1,708 +0,0 @@ -use alloy_primitives::{Address, I256, U256}; -use solang_parser::pt::*; -use std::str::FromStr; - -/// Helper to convert a string number into a comparable one -fn to_num(string: &str) -> I256 { - if string.is_empty() { - return I256::ZERO; - } - string.replace('_', "").trim().parse().unwrap() -} - -/// Helper to convert the fractional part of a number into a comparable one. -/// This will reverse the number so that 0's can be ignored -fn to_num_reversed(string: &str) -> U256 { - if string.is_empty() { - return U256::from(0); - } - string.replace('_', "").trim().chars().rev().collect::().parse().unwrap() -} - -/// Helper to filter [ParameterList] to omit empty -/// parameters -fn filter_params(list: &ParameterList) -> ParameterList { - list.iter().filter(|(_, param)| param.is_some()).cloned().collect::>() -} - -/// Check if two ParseTrees are equal ignoring location information or ordering if ordering does -/// not matter -pub trait AstEq { - fn ast_eq(&self, other: &Self) -> bool; -} - -impl AstEq for Loc { - fn ast_eq(&self, _other: &Self) -> bool { - true - } -} - -impl AstEq for IdentifierPath { - fn ast_eq(&self, other: &Self) -> bool { - self.identifiers.ast_eq(&other.identifiers) - } -} - -impl AstEq for SourceUnit { - fn ast_eq(&self, other: &Self) -> bool { - self.0.ast_eq(&other.0) - } -} - -impl AstEq for VariableDefinition { - fn ast_eq(&self, other: &Self) -> bool { - let sorted_attrs = |def: &Self| { - let mut attrs = def.attrs.clone(); - attrs.sort(); - attrs - }; - self.ty.ast_eq(&other.ty) - && self.name.ast_eq(&other.name) - && self.initializer.ast_eq(&other.initializer) - && sorted_attrs(self).ast_eq(&sorted_attrs(other)) - } -} - -impl AstEq for FunctionDefinition { - fn ast_eq(&self, other: &Self) -> bool { - // attributes - let sorted_attrs = |def: &Self| { - let mut attrs = def.attributes.clone(); - attrs.sort(); - attrs - }; - - // params - let left_params = filter_params(&self.params); - let right_params = filter_params(&other.params); - let left_returns = filter_params(&self.returns); - let right_returns = filter_params(&other.returns); - - self.ty.ast_eq(&other.ty) - && self.name.ast_eq(&other.name) - && left_params.ast_eq(&right_params) - && self.return_not_returns.ast_eq(&other.return_not_returns) - && left_returns.ast_eq(&right_returns) - && self.body.ast_eq(&other.body) - && sorted_attrs(self).ast_eq(&sorted_attrs(other)) - } -} - -impl AstEq for Base { - fn ast_eq(&self, other: &Self) -> bool { - self.name.ast_eq(&other.name) - && self.args.clone().unwrap_or_default().ast_eq(&other.args.clone().unwrap_or_default()) - } -} - -impl AstEq for Vec -where - T: AstEq, -{ - fn ast_eq(&self, other: &Self) -> bool { - if self.len() == other.len() { - self.iter().zip(other.iter()).all(|(left, right)| left.ast_eq(right)) - } else { - false - } - } -} - -impl AstEq for Option -where - T: AstEq, -{ - fn ast_eq(&self, other: &Self) -> bool { - match (self, other) { - (Some(left), Some(right)) => left.ast_eq(right), - (None, None) => true, - _ => false, - } - } -} - -impl AstEq for Box -where - T: AstEq, -{ - fn ast_eq(&self, other: &Self) -> bool { - T::ast_eq(self, other) - } -} - -impl AstEq for () { - fn ast_eq(&self, _other: &Self) -> bool { - true - } -} - -impl AstEq for &T -where - T: AstEq, -{ - fn ast_eq(&self, other: &Self) -> bool { - T::ast_eq(self, other) - } -} - -impl AstEq for String { - fn ast_eq(&self, other: &Self) -> bool { - match (Address::from_str(self), Address::from_str(other)) { - (Ok(left), Ok(right)) => left == right, - _ => self == other, - } - } -} - -macro_rules! ast_eq_field { - (#[ast_eq_use($convert_func:ident)] $field:ident) => { - $convert_func($field) - }; - ($field:ident) => { - $field - }; -} - -macro_rules! gen_ast_eq_enum { - ($self:expr, $other:expr, $name:ident { - $($unit_variant:ident),* $(,)? - _ - $($tuple_variant:ident ( $($(#[ast_eq_use($tuple_convert_func:ident)])? $tuple_field:ident),* $(,)? )),* $(,)? - _ - $($struct_variant:ident { $($(#[ast_eq_use($struct_convert_func:ident)])? $struct_field:ident),* $(,)? }),* $(,)? - }) => { - match $self { - $($name::$unit_variant => gen_ast_eq_enum!($other, $name, $unit_variant),)* - $($name::$tuple_variant($($tuple_field),*) => - gen_ast_eq_enum!($other, $name, $tuple_variant ($($(#[ast_eq_use($tuple_convert_func)])? $tuple_field),*)),)* - $($name::$struct_variant { $($struct_field),* } => - gen_ast_eq_enum!($other, $name, $struct_variant {$($(#[ast_eq_use($struct_convert_func)])? $struct_field),*}),)* - } - }; - ($other:expr, $name:ident, $unit_variant:ident) => { - { - matches!($other, $name::$unit_variant) - } - }; - ($other:expr, $name:ident, $tuple_variant:ident ( $($(#[ast_eq_use($tuple_convert_func:ident)])? $tuple_field:ident),* $(,)? ) ) => { - { - let left = ($(ast_eq_field!($(#[ast_eq_use($tuple_convert_func)])? $tuple_field)),*); - if let $name::$tuple_variant($($tuple_field),*) = $other { - let right = ($(ast_eq_field!($(#[ast_eq_use($tuple_convert_func)])? $tuple_field)),*); - left.ast_eq(&right) - } else { - false - } - } - }; - ($other:expr, $name:ident, $struct_variant:ident { $($(#[ast_eq_use($struct_convert_func:ident)])? $struct_field:ident),* $(,)? } ) => { - { - let left = ($(ast_eq_field!($(#[ast_eq_use($struct_convert_func)])? $struct_field)),*); - if let $name::$struct_variant { $($struct_field),* } = $other { - let right = ($(ast_eq_field!($(#[ast_eq_use($struct_convert_func)])? $struct_field)),*); - left.ast_eq(&right) - } else { - false - } - } - }; -} - -macro_rules! wrap_in_box { - ($stmt:expr, $loc:expr) => { - if !matches!(**$stmt, Statement::Block { .. }) { - Box::new(Statement::Block { - loc: $loc, - unchecked: false, - statements: vec![*$stmt.clone()], - }) - } else { - $stmt.clone() - } - }; -} - -impl AstEq for Statement { - fn ast_eq(&self, other: &Self) -> bool { - match self { - Self::If(loc, expr, stmt1, stmt2) => { - #[expect(clippy::borrowed_box)] - let wrap_if = |stmt1: &Box, stmt2: &Option>| { - ( - wrap_in_box!(stmt1, *loc), - stmt2.as_ref().map(|stmt2| { - if matches!(**stmt2, Self::If(..)) { - stmt2.clone() - } else { - wrap_in_box!(stmt2, *loc) - } - }), - ) - }; - let (stmt1, stmt2) = wrap_if(stmt1, stmt2); - let left = (loc, expr, &stmt1, &stmt2); - if let Self::If(loc, expr, stmt1, stmt2) = other { - let (stmt1, stmt2) = wrap_if(stmt1, stmt2); - let right = (loc, expr, &stmt1, &stmt2); - left.ast_eq(&right) - } else { - false - } - } - Self::While(loc, expr, stmt1) => { - let stmt1 = wrap_in_box!(stmt1, *loc); - let left = (loc, expr, &stmt1); - if let Self::While(loc, expr, stmt1) = other { - let stmt1 = wrap_in_box!(stmt1, *loc); - let right = (loc, expr, &stmt1); - left.ast_eq(&right) - } else { - false - } - } - Self::DoWhile(loc, stmt1, expr) => { - let stmt1 = wrap_in_box!(stmt1, *loc); - let left = (loc, &stmt1, expr); - if let Self::DoWhile(loc, stmt1, expr) = other { - let stmt1 = wrap_in_box!(stmt1, *loc); - let right = (loc, &stmt1, expr); - left.ast_eq(&right) - } else { - false - } - } - Self::For(loc, stmt1, expr, stmt2, stmt3) => { - let stmt3 = stmt3.as_ref().map(|stmt3| wrap_in_box!(stmt3, *loc)); - let left = (loc, stmt1, expr, stmt2, &stmt3); - if let Self::For(loc, stmt1, expr, stmt2, stmt3) = other { - let stmt3 = stmt3.as_ref().map(|stmt3| wrap_in_box!(stmt3, *loc)); - let right = (loc, stmt1, expr, stmt2, &stmt3); - left.ast_eq(&right) - } else { - false - } - } - Self::Try(loc, expr, returns, catch) => { - let left_returns = - returns.as_ref().map(|(params, stmt)| (filter_params(params), stmt)); - let left = (loc, expr, left_returns, catch); - if let Self::Try(loc, expr, returns, catch) = other { - let right_returns = - returns.as_ref().map(|(params, stmt)| (filter_params(params), stmt)); - let right = (loc, expr, right_returns, catch); - left.ast_eq(&right) - } else { - false - } - } - _ => gen_ast_eq_enum!(self, other, Statement { - _ - Args(loc, args), - Expression(loc, expr), - VariableDefinition(loc, decl, expr), - Continue(loc, ), - Break(loc, ), - Return(loc, expr), - Revert(loc, expr, expr2), - RevertNamedArgs(loc, expr, args), - Emit(loc, expr), - // provide overridden variants regardless - If(loc, expr, stmt1, stmt2), - While(loc, expr, stmt1), - DoWhile(loc, stmt1, expr), - For(loc, stmt1, expr, stmt2, stmt3), - Try(loc, expr, params, clause), - Error(loc) - _ - Block { - loc, - unchecked, - statements, - }, - Assembly { - loc, - dialect, - block, - flags, - }, - }), - } - } -} - -macro_rules! derive_ast_eq { - ($name:ident) => { - impl AstEq for $name { - fn ast_eq(&self, other: &Self) -> bool { - self == other - } - } - }; - (($($index:tt $gen:tt),*)) => { - impl < $( $gen ),* > AstEq for ($($gen,)*) where $($gen: AstEq),* { - fn ast_eq(&self, other: &Self) -> bool { - $( - if !self.$index.ast_eq(&other.$index) { - return false - } - )* - true - } - } - }; - (struct $name:ident { $($field:ident),* $(,)? }) => { - impl AstEq for $name { - fn ast_eq(&self, other: &Self) -> bool { - let $name { $($field),* } = self; - let left = ($($field),*); - let $name { $($field),* } = other; - let right = ($($field),*); - left.ast_eq(&right) - } - } - }; - (enum $name:ident { - $($unit_variant:ident),* $(,)? - _ - $($tuple_variant:ident ( $($(#[ast_eq_use($tuple_convert_func:ident)])? $tuple_field:ident),* $(,)? )),* $(,)? - _ - $($struct_variant:ident { $($(#[ast_eq_use($struct_convert_func:ident)])? $struct_field:ident),* $(,)? }),* $(,)? - }) => { - impl AstEq for $name { - fn ast_eq(&self, other: &Self) -> bool { - gen_ast_eq_enum!(self, other, $name { - $($unit_variant),* - _ - $($tuple_variant ( $($(#[ast_eq_use($tuple_convert_func)])? $tuple_field),* )),* - _ - $($struct_variant { $($(#[ast_eq_use($struct_convert_func)])? $struct_field),* }),* - }) - } - } - } -} - -derive_ast_eq! { (0 A) } -derive_ast_eq! { (0 A, 1 B) } -derive_ast_eq! { (0 A, 1 B, 2 C) } -derive_ast_eq! { (0 A, 1 B, 2 C, 3 D) } -derive_ast_eq! { (0 A, 1 B, 2 C, 3 D, 4 E) } -derive_ast_eq! { (0 A, 1 B, 2 C, 3 D, 4 E, 5 F) } -derive_ast_eq! { (0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G) } -derive_ast_eq! { bool } -derive_ast_eq! { u8 } -derive_ast_eq! { u16 } -derive_ast_eq! { I256 } -derive_ast_eq! { U256 } -derive_ast_eq! { struct Identifier { loc, name } } -derive_ast_eq! { struct HexLiteral { loc, hex } } -derive_ast_eq! { struct StringLiteral { loc, unicode, string } } -derive_ast_eq! { struct Parameter { loc, annotation, ty, storage, name } } -derive_ast_eq! { struct NamedArgument { loc, name, expr } } -derive_ast_eq! { struct YulBlock { loc, statements } } -derive_ast_eq! { struct YulFunctionCall { loc, id, arguments } } -derive_ast_eq! { struct YulFunctionDefinition { loc, id, params, returns, body } } -derive_ast_eq! { struct YulSwitch { loc, condition, cases, default } } -derive_ast_eq! { struct YulFor { - loc, - init_block, - condition, - post_block, - execution_block, -}} -derive_ast_eq! { struct YulTypedIdentifier { loc, id, ty } } -derive_ast_eq! { struct VariableDeclaration { loc, ty, storage, name } } -derive_ast_eq! { struct Using { loc, list, ty, global } } -derive_ast_eq! { struct UsingFunction { loc, path, oper } } -derive_ast_eq! { struct TypeDefinition { loc, name, ty } } -derive_ast_eq! { struct ContractDefinition { loc, ty, name, base, layout, parts } } -derive_ast_eq! { struct EventParameter { loc, ty, indexed, name } } -derive_ast_eq! { struct ErrorParameter { loc, ty, name } } -derive_ast_eq! { struct EventDefinition { loc, name, fields, anonymous } } -derive_ast_eq! { struct ErrorDefinition { loc, keyword, name, fields } } -derive_ast_eq! { struct StructDefinition { loc, name, fields } } -derive_ast_eq! { struct EnumDefinition { loc, name, values } } -derive_ast_eq! { struct Annotation { loc, id, value } } -derive_ast_eq! { enum PragmaDirective { - _ - Identifier(loc, id1, id2), - StringLiteral(loc, id, lit), - Version(loc, id, version), - _ -}} -derive_ast_eq! { enum UsingList { - Error, - _ - Library(expr), - Functions(exprs), - _ -}} -derive_ast_eq! { enum UserDefinedOperator { - BitwiseAnd, - BitwiseNot, - Negate, - BitwiseOr, - BitwiseXor, - Add, - Divide, - Modulo, - Multiply, - Subtract, - Equal, - More, - MoreEqual, - Less, - LessEqual, - NotEqual, - _ - _ -}} -derive_ast_eq! { enum Visibility { - _ - External(loc), - Public(loc), - Internal(loc), - Private(loc), - _ -}} -derive_ast_eq! { enum Mutability { - _ - Pure(loc), - View(loc), - Constant(loc), - Payable(loc), - _ -}} -derive_ast_eq! { enum FunctionAttribute { - _ - Mutability(muta), - Visibility(visi), - Virtual(loc), - Immutable(loc), - Override(loc, idents), - BaseOrModifier(loc, base), - Error(loc), - _ -}} -derive_ast_eq! { enum StorageLocation { - _ - Memory(loc), - Storage(loc), - Calldata(loc), - Transient(loc), - _ -}} -derive_ast_eq! { enum Type { - Address, - AddressPayable, - Payable, - Bool, - Rational, - DynamicBytes, - String, - _ - Int(int), - Uint(int), - Bytes(int), - _ - Mapping{ loc, key, key_name, value, value_name }, - Function { params, attributes, returns }, -}} -derive_ast_eq! { enum Expression { - _ - PostIncrement(loc, expr1), - PostDecrement(loc, expr1), - New(loc, expr1), - ArraySubscript(loc, expr1, expr2), - ArraySlice( - loc, - expr1, - expr2, - expr3, - ), - MemberAccess(loc, expr1, ident1), - FunctionCall(loc, expr1, exprs1), - FunctionCallBlock(loc, expr1, stmt), - NamedFunctionCall(loc, expr1, args), - Not(loc, expr1), - BitwiseNot(loc, expr1), - Delete(loc, expr1), - PreIncrement(loc, expr1), - PreDecrement(loc, expr1), - UnaryPlus(loc, expr1), - Negate(loc, expr1), - Power(loc, expr1, expr2), - Multiply(loc, expr1, expr2), - Divide(loc, expr1, expr2), - Modulo(loc, expr1, expr2), - Add(loc, expr1, expr2), - Subtract(loc, expr1, expr2), - ShiftLeft(loc, expr1, expr2), - ShiftRight(loc, expr1, expr2), - BitwiseAnd(loc, expr1, expr2), - BitwiseXor(loc, expr1, expr2), - BitwiseOr(loc, expr1, expr2), - Less(loc, expr1, expr2), - More(loc, expr1, expr2), - LessEqual(loc, expr1, expr2), - MoreEqual(loc, expr1, expr2), - Equal(loc, expr1, expr2), - NotEqual(loc, expr1, expr2), - And(loc, expr1, expr2), - Or(loc, expr1, expr2), - ConditionalOperator(loc, expr1, expr2, expr3), - Assign(loc, expr1, expr2), - AssignOr(loc, expr1, expr2), - AssignAnd(loc, expr1, expr2), - AssignXor(loc, expr1, expr2), - AssignShiftLeft(loc, expr1, expr2), - AssignShiftRight(loc, expr1, expr2), - AssignAdd(loc, expr1, expr2), - AssignSubtract(loc, expr1, expr2), - AssignMultiply(loc, expr1, expr2), - AssignDivide(loc, expr1, expr2), - AssignModulo(loc, expr1, expr2), - BoolLiteral(loc, bool1), - NumberLiteral(loc, #[ast_eq_use(to_num)] str1, #[ast_eq_use(to_num)] str2, unit), - RationalNumberLiteral( - loc, - #[ast_eq_use(to_num)] str1, - #[ast_eq_use(to_num_reversed)] str2, - #[ast_eq_use(to_num)] str3, - unit - ), - HexNumberLiteral(loc, str1, unit), - StringLiteral(strs1), - Type(loc, ty1), - HexLiteral(hexs1), - AddressLiteral(loc, str1), - Variable(ident1), - List(loc, params1), - ArrayLiteral(loc, exprs1), - Parenthesis(loc, expr) - _ -}} -derive_ast_eq! { enum CatchClause { - _ - Simple(param, ident, stmt), - Named(loc, ident, param, stmt), - _ -}} -derive_ast_eq! { enum YulStatement { - _ - Assign(loc, exprs, expr), - VariableDeclaration(loc, idents, expr), - If(loc, expr, block), - For(yul_for), - Switch(switch), - Leave(loc), - Break(loc), - Continue(loc), - Block(block), - FunctionDefinition(def), - FunctionCall(func), - Error(loc), - _ -}} -derive_ast_eq! { enum YulExpression { - _ - BoolLiteral(loc, boo, ident), - NumberLiteral(loc, string1, string2, ident), - HexNumberLiteral(loc, string, ident), - HexStringLiteral(hex, ident), - StringLiteral(string, ident), - Variable(ident), - FunctionCall(func), - SuffixAccess(loc, expr, ident), - _ -}} -derive_ast_eq! { enum YulSwitchOptions { - _ - Case(loc, expr, block), - Default(loc, block), - _ -}} -derive_ast_eq! { enum SourceUnitPart { - _ - ContractDefinition(def), - PragmaDirective(pragma), - ImportDirective(import), - EnumDefinition(def), - StructDefinition(def), - EventDefinition(def), - ErrorDefinition(def), - FunctionDefinition(def), - VariableDefinition(def), - TypeDefinition(def), - Using(using), - StraySemicolon(loc), - Annotation(annotation), - _ -}} -derive_ast_eq! { enum ImportPath { - _ - Filename(lit), - Path(path), - _ -}} -derive_ast_eq! { enum Import { - _ - Plain(string, loc), - GlobalSymbol(string, ident, loc), - Rename(string, idents, loc), - _ -}} -derive_ast_eq! { enum FunctionTy { - Constructor, - Function, - Fallback, - Receive, - Modifier, - _ - _ -}} -derive_ast_eq! { enum ContractPart { - _ - StructDefinition(def), - EventDefinition(def), - EnumDefinition(def), - ErrorDefinition(def), - VariableDefinition(def), - FunctionDefinition(def), - TypeDefinition(def), - StraySemicolon(loc), - Using(using), - Annotation(annotation), - _ -}} -derive_ast_eq! { enum ContractTy { - _ - Abstract(loc), - Contract(loc), - Interface(loc), - Library(loc), - _ -}} -derive_ast_eq! { enum VariableAttribute { - _ - Visibility(visi), - Constant(loc), - Immutable(loc), - Override(loc, idents), - StorageType(st), - StorageLocation(st), - _ -}} - -// Who cares -impl AstEq for StorageType { - fn ast_eq(&self, _other: &Self) -> bool { - true - } -} - -impl AstEq for VersionComparator { - fn ast_eq(&self, _other: &Self) -> bool { - true - } -} diff --git a/crates/doc/src/solang_ext/loc.rs b/crates/doc/src/solang_ext/loc.rs deleted file mode 100644 index cec21d7714385..0000000000000 --- a/crates/doc/src/solang_ext/loc.rs +++ /dev/null @@ -1,168 +0,0 @@ -use solang_parser::pt; -use std::{borrow::Cow, rc::Rc, sync::Arc}; - -/// Returns the code location. -/// -/// Patched version of [`pt::CodeLocation`]: includes the block of a [`pt::FunctionDefinition`] in -/// its `loc`. -pub trait CodeLocationExt { - /// Returns the code location of `self`. - fn loc(&self) -> pt::Loc; -} - -impl CodeLocationExt for &T { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -impl CodeLocationExt for &mut T { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -impl CodeLocationExt for Cow<'_, T> { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -impl CodeLocationExt for Box { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -impl CodeLocationExt for Rc { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -impl CodeLocationExt for Arc { - fn loc(&self) -> pt::Loc { - (**self).loc() - } -} - -// FunctionDefinition patch -impl CodeLocationExt for pt::FunctionDefinition { - #[inline] - #[track_caller] - fn loc(&self) -> pt::Loc { - let mut loc = self.loc; - if let Some(ref body) = self.body { - loc.use_end_from(&pt::CodeLocation::loc(body)); - } - loc - } -} - -impl CodeLocationExt for pt::ContractPart { - #[inline] - #[track_caller] - fn loc(&self) -> pt::Loc { - match self { - Self::FunctionDefinition(f) => f.loc(), - _ => pt::CodeLocation::loc(self), - } - } -} - -impl CodeLocationExt for pt::SourceUnitPart { - #[inline] - #[track_caller] - fn loc(&self) -> pt::Loc { - match self { - Self::FunctionDefinition(f) => f.loc(), - _ => pt::CodeLocation::loc(self), - } - } -} - -impl CodeLocationExt for pt::ImportPath { - fn loc(&self) -> pt::Loc { - match self { - Self::Filename(s) => s.loc(), - Self::Path(i) => i.loc(), - } - } -} - -impl CodeLocationExt for pt::VersionComparator { - fn loc(&self) -> pt::Loc { - match self { - Self::Plain { loc, .. } - | Self::Operator { loc, .. } - | Self::Or { loc, .. } - | Self::Range { loc, .. } => *loc, - } - } -} - -macro_rules! impl_delegate { - ($($t:ty),+ $(,)?) => {$( - impl CodeLocationExt for $t { - #[inline] - #[track_caller] - fn loc(&self) -> pt::Loc { - pt::CodeLocation::loc(self) - } - } - )+}; -} - -impl_delegate! { - pt::Annotation, - pt::Base, - pt::ContractDefinition, - pt::EnumDefinition, - pt::ErrorDefinition, - pt::ErrorParameter, - pt::EventDefinition, - pt::EventParameter, - pt::PragmaDirective, - // pt::FunctionDefinition, - pt::HexLiteral, - pt::Identifier, - pt::IdentifierPath, - pt::NamedArgument, - pt::Parameter, - // pt::SourceUnit, - pt::StringLiteral, - pt::StructDefinition, - pt::TypeDefinition, - pt::Using, - pt::UsingFunction, - pt::VariableDeclaration, - pt::VariableDefinition, - pt::YulBlock, - pt::YulFor, - pt::YulFunctionCall, - pt::YulFunctionDefinition, - pt::YulSwitch, - pt::YulTypedIdentifier, - - pt::CatchClause, - pt::Comment, - // pt::ContractPart, - pt::ContractTy, - pt::Expression, - pt::FunctionAttribute, - // pt::FunctionTy, - pt::Import, - pt::Loc, - pt::Mutability, - // pt::SourceUnitPart, - pt::Statement, - pt::StorageLocation, - // pt::Type, - // pt::UserDefinedOperator, - pt::UsingList, - pt::VariableAttribute, - // pt::Visibility, - pt::YulExpression, - pt::YulStatement, - pt::YulSwitchOptions, -} diff --git a/crates/doc/src/solang_ext/mod.rs b/crates/doc/src/solang_ext/mod.rs deleted file mode 100644 index d85dd1a5aace7..0000000000000 --- a/crates/doc/src/solang_ext/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Extension traits and modules to the [`solang_parser`] crate. - -/// Same as [`solang_parser::pt`], but with the patched `CodeLocation`. -pub mod pt { - #[doc(no_inline)] - pub use super::loc::CodeLocationExt as CodeLocation; - - #[doc(no_inline)] - pub use solang_parser::pt::{ - Annotation, Base, CatchClause, Comment, ContractDefinition, ContractPart, ContractTy, - EnumDefinition, ErrorDefinition, ErrorParameter, EventDefinition, EventParameter, - Expression, FunctionAttribute, FunctionDefinition, FunctionTy, HexLiteral, Identifier, - IdentifierPath, Import, ImportPath, Loc, Mutability, NamedArgument, OptionalCodeLocation, - Parameter, ParameterList, PragmaDirective, SourceUnit, SourceUnitPart, Statement, - StorageLocation, StringLiteral, StructDefinition, Type, TypeDefinition, - UserDefinedOperator, Using, UsingFunction, UsingList, VariableAttribute, - VariableDeclaration, VariableDefinition, Visibility, YulBlock, YulExpression, YulFor, - YulFunctionCall, YulFunctionDefinition, YulStatement, YulSwitch, YulSwitchOptions, - YulTypedIdentifier, - }; -} - -mod ast_eq; -mod loc; -mod safe_unwrap; -mod visit; - -pub use ast_eq::AstEq; -pub use loc::CodeLocationExt; -pub use safe_unwrap::SafeUnwrap; -pub use visit::{Visitable, Visitor}; diff --git a/crates/doc/src/solang_ext/safe_unwrap.rs b/crates/doc/src/solang_ext/safe_unwrap.rs deleted file mode 100644 index fe2810ad9705a..0000000000000 --- a/crates/doc/src/solang_ext/safe_unwrap.rs +++ /dev/null @@ -1,52 +0,0 @@ -use solang_parser::pt; - -/// Trait implemented to unwrap optional parse tree items initially introduced in -/// [hyperledger/solang#1068]. -/// -/// Note that the methods of this trait should only be used on parse tree items' fields, like -/// [pt::VariableDefinition] or [pt::EventDefinition], where the `name` field is `None` only when an -/// error occurred during parsing. -/// -/// [hyperledger/solang#1068]: https://github.com/hyperledger/solang/pull/1068 -pub trait SafeUnwrap { - /// See [SafeUnwrap]. - fn safe_unwrap(&self) -> &T; - - /// See [SafeUnwrap]. - fn safe_unwrap_mut(&mut self) -> &mut T; -} - -#[inline(never)] -#[cold] -#[track_caller] -fn invalid() -> ! { - panic!("invalid parse tree") -} - -macro_rules! impl_ { - ($($t:ty),+ $(,)?) => { - $( - impl SafeUnwrap<$t> for Option<$t> { - #[inline] - #[track_caller] - fn safe_unwrap(&self) -> &$t { - match *self { - Some(ref x) => x, - None => invalid(), - } - } - - #[inline] - #[track_caller] - fn safe_unwrap_mut(&mut self) -> &mut $t { - match *self { - Some(ref mut x) => x, - None => invalid(), - } - } - } - )+ - }; -} - -impl_!(pt::Identifier, pt::StringLiteral); diff --git a/crates/doc/src/solang_ext/visit.rs b/crates/doc/src/solang_ext/visit.rs deleted file mode 100644 index 80cb4f3dc1376..0000000000000 --- a/crates/doc/src/solang_ext/visit.rs +++ /dev/null @@ -1,621 +0,0 @@ -//! Visitor helpers to traverse the [solang Solidity Parse Tree](solang_parser::pt). - -use crate::solang_ext::{CodeLocationExt, pt::*}; - -/// A trait that is invoked while traversing the Solidity Parse Tree. -/// Each method of the [Visitor] trait is a hook that can be potentially overridden. -/// -/// Currently the main implementer of this trait is the [`Formatter`](crate::Formatter<'_>) struct. -pub trait Visitor { - type Error: std::error::Error; - - fn visit_source(&mut self, _loc: Loc) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_source_unit(&mut self, _source_unit: &mut SourceUnit) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_contract(&mut self, _contract: &mut ContractDefinition) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_annotation(&mut self, annotation: &mut Annotation) -> Result<(), Self::Error> { - self.visit_source(annotation.loc) - } - - fn visit_pragma(&mut self, pragma: &mut PragmaDirective) -> Result<(), Self::Error> { - self.visit_source(pragma.loc()) - } - - fn visit_import_plain( - &mut self, - _loc: Loc, - _import: &mut ImportPath, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_import_global( - &mut self, - _loc: Loc, - _global: &mut ImportPath, - _alias: &mut Identifier, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_import_renames( - &mut self, - _loc: Loc, - _imports: &mut [(Identifier, Option)], - _from: &mut ImportPath, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_enum(&mut self, _enum: &mut EnumDefinition) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_assembly( - &mut self, - loc: Loc, - _dialect: &mut Option, - _block: &mut YulBlock, - _flags: &mut Option>, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_block( - &mut self, - loc: Loc, - _unchecked: bool, - _statements: &mut Vec, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_args(&mut self, loc: Loc, _args: &mut Vec) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - /// Don't write semicolon at the end because expressions can appear as both - /// part of other node and a statement in the function body - fn visit_expr(&mut self, loc: Loc, _expr: &mut Expression) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_ident(&mut self, loc: Loc, _ident: &mut Identifier) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_ident_path(&mut self, idents: &mut IdentifierPath) -> Result<(), Self::Error> { - self.visit_source(idents.loc) - } - - fn visit_emit(&mut self, loc: Loc, _event: &mut Expression) -> Result<(), Self::Error> { - self.visit_source(loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<(), Self::Error> { - self.visit_source(var.loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_var_definition_stmt( - &mut self, - loc: Loc, - _declaration: &mut VariableDeclaration, - _expr: &mut Option, - ) -> Result<(), Self::Error> { - self.visit_source(loc)?; - self.visit_stray_semicolon() - } - - fn visit_var_declaration(&mut self, var: &mut VariableDeclaration) -> Result<(), Self::Error> { - self.visit_source(var.loc) - } - - fn visit_return( - &mut self, - loc: Loc, - _expr: &mut Option, - ) -> Result<(), Self::Error> { - self.visit_source(loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_revert( - &mut self, - loc: Loc, - _error: &mut Option, - _args: &mut Vec, - ) -> Result<(), Self::Error> { - self.visit_source(loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_revert_named_args( - &mut self, - loc: Loc, - _error: &mut Option, - _args: &mut Vec, - ) -> Result<(), Self::Error> { - self.visit_source(loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_break(&mut self, loc: Loc, _semicolon: bool) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_continue(&mut self, loc: Loc, _semicolon: bool) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - #[expect(clippy::type_complexity)] - fn visit_try( - &mut self, - loc: Loc, - _expr: &mut Expression, - _returns: &mut Option<(Vec<(Loc, Option)>, Box)>, - _clauses: &mut Vec, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_if( - &mut self, - loc: Loc, - _cond: &mut Expression, - _if_branch: &mut Box, - _else_branch: &mut Option>, - _is_first_stmt: bool, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_do_while( - &mut self, - loc: Loc, - _body: &mut Statement, - _cond: &mut Expression, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_while( - &mut self, - loc: Loc, - _cond: &mut Expression, - _body: &mut Statement, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_for( - &mut self, - loc: Loc, - _init: &mut Option>, - _cond: &mut Option>, - _update: &mut Option>, - _body: &mut Option>, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<(), Self::Error> { - self.visit_source(func.loc())?; - if func.body.is_none() { - self.visit_stray_semicolon()?; - } - - Ok(()) - } - - fn visit_function_attribute( - &mut self, - attribute: &mut FunctionAttribute, - ) -> Result<(), Self::Error> { - self.visit_source(attribute.loc())?; - Ok(()) - } - - fn visit_var_attribute( - &mut self, - attribute: &mut VariableAttribute, - ) -> Result<(), Self::Error> { - self.visit_source(attribute.loc())?; - Ok(()) - } - - fn visit_base(&mut self, base: &mut Base) -> Result<(), Self::Error> { - self.visit_source(base.loc) - } - - fn visit_parameter(&mut self, parameter: &mut Parameter) -> Result<(), Self::Error> { - self.visit_source(parameter.loc) - } - - fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<(), Self::Error> { - self.visit_source(structure.loc)?; - - Ok(()) - } - - fn visit_event(&mut self, event: &mut EventDefinition) -> Result<(), Self::Error> { - self.visit_source(event.loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_event_parameter(&mut self, param: &mut EventParameter) -> Result<(), Self::Error> { - self.visit_source(param.loc) - } - - fn visit_error(&mut self, error: &mut ErrorDefinition) -> Result<(), Self::Error> { - self.visit_source(error.loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_error_parameter(&mut self, param: &mut ErrorParameter) -> Result<(), Self::Error> { - self.visit_source(param.loc) - } - - fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> Result<(), Self::Error> { - self.visit_source(def.loc) - } - - fn visit_stray_semicolon(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_using(&mut self, using: &mut Using) -> Result<(), Self::Error> { - self.visit_source(using.loc)?; - self.visit_stray_semicolon()?; - - Ok(()) - } - - fn visit_yul_block( - &mut self, - loc: Loc, - _stmts: &mut Vec, - _attempt_single_line: bool, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_yul_expr(&mut self, expr: &mut YulExpression) -> Result<(), Self::Error> { - self.visit_source(expr.loc()) - } - - fn visit_yul_assignment( - &mut self, - loc: Loc, - _exprs: &mut Vec, - _expr: &mut Option<&mut YulExpression>, - ) -> Result<(), Self::Error> - where - T: Visitable + CodeLocationExt, - { - self.visit_source(loc) - } - - fn visit_yul_for(&mut self, stmt: &mut YulFor) -> Result<(), Self::Error> { - self.visit_source(stmt.loc) - } - - fn visit_yul_function_call(&mut self, stmt: &mut YulFunctionCall) -> Result<(), Self::Error> { - self.visit_source(stmt.loc) - } - - fn visit_yul_fun_def(&mut self, stmt: &mut YulFunctionDefinition) -> Result<(), Self::Error> { - self.visit_source(stmt.loc) - } - - fn visit_yul_if( - &mut self, - loc: Loc, - _expr: &mut YulExpression, - _block: &mut YulBlock, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_yul_leave(&mut self, loc: Loc) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_yul_switch(&mut self, stmt: &mut YulSwitch) -> Result<(), Self::Error> { - self.visit_source(stmt.loc) - } - - fn visit_yul_var_declaration( - &mut self, - loc: Loc, - _idents: &mut Vec, - _expr: &mut Option, - ) -> Result<(), Self::Error> { - self.visit_source(loc) - } - - fn visit_yul_typed_ident(&mut self, ident: &mut YulTypedIdentifier) -> Result<(), Self::Error> { - self.visit_source(ident.loc) - } - - fn visit_parser_error(&mut self, loc: Loc) -> Result<(), Self::Error> { - self.visit_source(loc) - } -} - -/// Visitable trait for [`solang_parser::pt`] types. -/// -/// All [`solang_parser::pt`] types, such as [Statement], should implement the [Visitable] trait -/// that accepts a trait [Visitor] implementation, which has various callback handles for Solidity -/// Parse Tree nodes. -/// -/// We want to take a `&mut self` to be able to implement some advanced features in the future such -/// as modifying the Parse Tree before formatting it. -pub trait Visitable { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor; -} - -impl Visitable for &mut T -where - T: Visitable, -{ - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - T::visit(self, v) - } -} - -impl Visitable for Option -where - T: Visitable, -{ - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - if let Some(inner) = self.as_mut() { inner.visit(v) } else { Ok(()) } - } -} - -impl Visitable for Box -where - T: Visitable, -{ - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - T::visit(self, v) - } -} - -impl Visitable for Vec -where - T: Visitable, -{ - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - for item in self.iter_mut() { - item.visit(v)?; - } - Ok(()) - } -} - -impl Visitable for SourceUnitPart { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - match self { - Self::ContractDefinition(contract) => v.visit_contract(contract), - Self::PragmaDirective(pragma) => v.visit_pragma(pragma), - Self::ImportDirective(import) => import.visit(v), - Self::EnumDefinition(enumeration) => v.visit_enum(enumeration), - Self::StructDefinition(structure) => v.visit_struct(structure), - Self::EventDefinition(event) => v.visit_event(event), - Self::ErrorDefinition(error) => v.visit_error(error), - Self::FunctionDefinition(function) => v.visit_function(function), - Self::VariableDefinition(variable) => v.visit_var_definition(variable), - Self::TypeDefinition(def) => v.visit_type_definition(def), - Self::StraySemicolon(_) => v.visit_stray_semicolon(), - Self::Using(using) => v.visit_using(using), - Self::Annotation(annotation) => v.visit_annotation(annotation), - } - } -} - -impl Visitable for Import { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - match self { - Self::Plain(import, loc) => v.visit_import_plain(*loc, import), - Self::GlobalSymbol(global, import_as, loc) => { - v.visit_import_global(*loc, global, import_as) - } - Self::Rename(from, imports, loc) => v.visit_import_renames(*loc, imports, from), - } - } -} - -impl Visitable for ContractPart { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - match self { - Self::StructDefinition(structure) => v.visit_struct(structure), - Self::EventDefinition(event) => v.visit_event(event), - Self::ErrorDefinition(error) => v.visit_error(error), - Self::EnumDefinition(enumeration) => v.visit_enum(enumeration), - Self::VariableDefinition(variable) => v.visit_var_definition(variable), - Self::FunctionDefinition(function) => v.visit_function(function), - Self::TypeDefinition(def) => v.visit_type_definition(def), - Self::StraySemicolon(_) => v.visit_stray_semicolon(), - Self::Using(using) => v.visit_using(using), - Self::Annotation(annotation) => v.visit_annotation(annotation), - } - } -} - -impl Visitable for Statement { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - match self { - Self::Block { loc, unchecked, statements } => { - v.visit_block(*loc, *unchecked, statements) - } - Self::Assembly { loc, dialect, block, flags } => { - v.visit_assembly(*loc, dialect, block, flags) - } - Self::Args(loc, args) => v.visit_args(*loc, args), - Self::If(loc, cond, if_branch, else_branch) => { - v.visit_if(*loc, cond, if_branch, else_branch, true) - } - Self::While(loc, cond, body) => v.visit_while(*loc, cond, body), - Self::Expression(loc, expr) => { - v.visit_expr(*loc, expr)?; - v.visit_stray_semicolon() - } - Self::VariableDefinition(loc, declaration, expr) => { - v.visit_var_definition_stmt(*loc, declaration, expr) - } - Self::For(loc, init, cond, update, body) => v.visit_for(*loc, init, cond, update, body), - Self::DoWhile(loc, body, cond) => v.visit_do_while(*loc, body, cond), - Self::Continue(loc) => v.visit_continue(*loc, true), - Self::Break(loc) => v.visit_break(*loc, true), - Self::Return(loc, expr) => v.visit_return(*loc, expr), - Self::Revert(loc, error, args) => v.visit_revert(*loc, error, args), - Self::RevertNamedArgs(loc, error, args) => v.visit_revert_named_args(*loc, error, args), - Self::Emit(loc, event) => v.visit_emit(*loc, event), - Self::Try(loc, expr, returns, clauses) => v.visit_try(*loc, expr, returns, clauses), - Self::Error(loc) => v.visit_parser_error(*loc), - } - } -} - -impl Visitable for Loc { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.visit_source(*self) - } -} - -impl Visitable for Expression { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.visit_expr(self.loc(), self) - } -} - -impl Visitable for Identifier { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.visit_ident(self.loc, self) - } -} - -impl Visitable for VariableDeclaration { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.visit_var_declaration(self) - } -} - -impl Visitable for YulBlock { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.visit_yul_block(self.loc, self.statements.as_mut(), false) - } -} - -impl Visitable for YulStatement { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - match self { - Self::Assign(loc, exprs, expr) => v.visit_yul_assignment(*loc, exprs, &mut Some(expr)), - Self::Block(block) => v.visit_yul_block(block.loc, block.statements.as_mut(), false), - Self::Break(loc) => v.visit_break(*loc, false), - Self::Continue(loc) => v.visit_continue(*loc, false), - Self::For(stmt) => v.visit_yul_for(stmt), - Self::FunctionCall(stmt) => v.visit_yul_function_call(stmt), - Self::FunctionDefinition(stmt) => v.visit_yul_fun_def(stmt), - Self::If(loc, expr, block) => v.visit_yul_if(*loc, expr, block), - Self::Leave(loc) => v.visit_yul_leave(*loc), - Self::Switch(stmt) => v.visit_yul_switch(stmt), - Self::VariableDeclaration(loc, idents, expr) => { - v.visit_yul_var_declaration(*loc, idents, expr) - } - Self::Error(loc) => v.visit_parser_error(*loc), - } - } -} - -macro_rules! impl_visitable { - ($type:ty, $func:ident) => { - impl Visitable for $type { - fn visit(&mut self, v: &mut V) -> Result<(), V::Error> - where - V: Visitor, - { - v.$func(self) - } - } - }; -} - -impl_visitable!(SourceUnit, visit_source_unit); -impl_visitable!(FunctionAttribute, visit_function_attribute); -impl_visitable!(VariableAttribute, visit_var_attribute); -impl_visitable!(Parameter, visit_parameter); -impl_visitable!(Base, visit_base); -impl_visitable!(EventParameter, visit_event_parameter); -impl_visitable!(ErrorParameter, visit_error_parameter); -impl_visitable!(IdentifierPath, visit_ident_path); -impl_visitable!(YulExpression, visit_yul_expr); -impl_visitable!(YulTypedIdentifier, visit_yul_typed_ident); diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index 1502322ac87f4..888f6269623a5 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -1,14 +1,11 @@ use crate::{ - CONTRACT_INHERITANCE_ID, CommentTag, Comments, CommentsRef, DEPLOYMENTS_ID, Document, - GIT_SOURCE_ID, INHERITDOC_ID, Markdown, PreprocessorOutput, + BaseInfo, CONTRACT_INHERITANCE_ID, CommentTag, Comments, CommentsRef, DEPLOYMENTS_ID, Document, + FunctionSource, GIT_SOURCE_ID, INHERITDOC_ID, Markdown, PreprocessorOutput, VariableAttr, document::{DocumentContent, read_context}, - helpers::function_signature, parser::ParseSource, - solang_ext::SafeUnwrap, writer::BufWriter, }; use itertools::Itertools; -use solang_parser::pt::{Base, FunctionDefinition, VariableAttribute}; use std::path::Path; /// The result of [`AsDoc::as_doc`]. @@ -75,8 +72,8 @@ impl AsDoc for CommentsRef<'_> { writer.writeln_raw(format!( "{}{}: {}", if customs.len() == 1 { "" } else { "- " }, - &c.tag, - &c.value + c.tag, + c.value ))?; writer.writeln()?; } @@ -86,9 +83,9 @@ impl AsDoc for CommentsRef<'_> { } } -impl AsDoc for Base { +impl AsDoc for BaseInfo { fn as_doc(&self) -> AsDocResult { - Ok(self.name.identifiers.iter().map(|ident| ident.name.clone()).join(".")) + Ok(self.name.clone()) } } @@ -106,8 +103,7 @@ impl AsDoc for Document { } for item in items { - let func = item.as_function().unwrap(); - let heading = function_signature(func).replace(',', ", "); + let heading = item.source.signature().replace(',', ", "); writer.write_heading(&heading)?; writer.write_section(&item.comments, &item.code)?; } @@ -121,7 +117,7 @@ impl AsDoc for Document { for item in items { let var = item.as_variable().unwrap(); - writer.write_heading(&var.name.safe_unwrap().name)?; + writer.write_heading(&var.name)?; writer.write_section(&item.comments, &item.code)?; } } @@ -138,15 +134,15 @@ impl AsDoc for Document { match &item.source { ParseSource::Contract(contract) => { - if !contract.base.is_empty() { + if !contract.bases.is_empty() { writer.write_bold("Inherits:")?; let mut bases = vec![]; let linked = read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); - for base in &contract.base { + for base in &contract.bases { let base_doc = base.as_doc()?; - let base_ident = &base.name.identifiers.last().unwrap().name; + let base_ident = &base.ident; let link = linked .as_ref() @@ -179,8 +175,7 @@ impl AsDoc for Document { item.attrs.iter().any(|attr| { matches!( attr, - VariableAttribute::Constant(_) - | VariableAttribute::Immutable(_) + VariableAttr::Constant | VariableAttr::Immutable ) }) }); @@ -189,11 +184,11 @@ impl AsDoc for Document { writer.write_subtitle("Constants")?; constants.into_iter().try_for_each(|(item, comments, code)| { let comments = comments.merge_inheritdoc( - &item.name.safe_unwrap().name, + &item.name, read_context!(self, INHERITDOC_ID, Inheritdoc), ); - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(&comments, code)?; writer.writeln() })?; @@ -203,11 +198,11 @@ impl AsDoc for Document { writer.write_subtitle("State Variables")?; state_vars.into_iter().try_for_each(|(item, comments, code)| { let comments = comments.merge_inheritdoc( - &item.name.safe_unwrap().name, + &item.name, read_context!(self, INHERITDOC_ID, Inheritdoc), ); - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(&comments, code)?; writer.writeln() })?; @@ -225,7 +220,7 @@ impl AsDoc for Document { if let Some(events) = item.events() { writer.write_subtitle("Events")?; events.into_iter().try_for_each(|(item, comments, code)| { - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(comments, code)?; writer.try_write_events_table(&item.fields, comments) })?; @@ -234,7 +229,7 @@ impl AsDoc for Document { if let Some(errors) = item.errors() { writer.write_subtitle("Errors")?; errors.into_iter().try_for_each(|(item, comments, code)| { - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(comments, code)?; writer.try_write_errors_table(&item.fields, comments) })?; @@ -243,7 +238,7 @@ impl AsDoc for Document { if let Some(structs) = item.structs() { writer.write_subtitle("Structs")?; structs.into_iter().try_for_each(|(item, comments, code)| { - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(comments, code)?; writer.try_write_properties_table(&item.fields, comments) })?; @@ -252,7 +247,7 @@ impl AsDoc for Document { if let Some(enums) = item.enums() { writer.write_subtitle("Enums")?; enums.into_iter().try_for_each(|(item, comments, code)| { - writer.write_heading(&item.name.safe_unwrap().name)?; + writer.write_heading(&item.name)?; writer.write_section(comments, code)?; writer.try_write_variant_table(item, comments) })?; @@ -270,16 +265,16 @@ impl AsDoc for Document { writer.write_code(&item.code)?; // Write function parameter comments in a table - let params = - func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, &item.comments)?; + writer.try_write_param_table( + CommentTag::Param, + &func.params, + &item.comments, + )?; // Write function return parameter comments in a table - let returns = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); writer.try_write_param_table( CommentTag::Return, - &returns, + &func.returns, &item.comments, )?; @@ -315,12 +310,12 @@ impl Document { fn write_function( &self, writer: &mut BufWriter, - func: &FunctionDefinition, + func: &FunctionSource, comments: &Comments, code: &str, ) -> Result<(), std::fmt::Error> { - let func_sign = function_signature(func); - let func_name = func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.clone()); + let func_name = func.name.as_deref().unwrap_or(&func.kind).to_string(); + let func_sign = func.signature(); let comments = comments.merge_inheritdoc(&func_sign, read_context!(self, INHERITDOC_ID, Inheritdoc)); @@ -336,12 +331,10 @@ impl Document { writer.write_code(code)?; // Write function parameter comments in a table - let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, &comments)?; + writer.try_write_param_table(CommentTag::Param, &func.params, &comments)?; // Write function return parameter comments in a table - let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Return, &returns, &comments)?; + writer.try_write_param_table(CommentTag::Return, &func.returns, &comments)?; writer.writeln()?; Ok(()) diff --git a/crates/doc/src/writer/traits.rs b/crates/doc/src/writer/traits.rs index 0b79718d5102f..7b0fbcc813a2b 100644 --- a/crates/doc/src/writer/traits.rs +++ b/crates/doc/src/writer/traits.rs @@ -1,58 +1,23 @@ //! Helper traits for writing documentation. -use solang_parser::pt::Expression; +use crate::ParamInfo; -/// Helper trait to abstract over a solang type that can be documented as parameter +/// Helper trait to abstract over a type that can be documented as a parameter. pub(crate) trait ParamLike { - /// Returns the type of the parameter. - fn ty(&self) -> &Expression; - /// Returns the type as a string. - fn type_name(&self) -> String { - self.ty().to_string() - } + fn type_name(&self) -> &str; /// Returns the identifier of the parameter. fn name(&self) -> Option<&str>; } -impl ParamLike for solang_parser::pt::Parameter { - fn ty(&self) -> &Expression { - &self.ty - } - - fn name(&self) -> Option<&str> { - self.name.as_ref().map(|id| id.name.as_str()) - } -} - -impl ParamLike for solang_parser::pt::VariableDeclaration { - fn ty(&self) -> &Expression { - &self.ty - } - - fn name(&self) -> Option<&str> { - self.name.as_ref().map(|id| id.name.as_str()) - } -} - -impl ParamLike for solang_parser::pt::EventParameter { - fn ty(&self) -> &Expression { - &self.ty - } - - fn name(&self) -> Option<&str> { - self.name.as_ref().map(|id| id.name.as_str()) - } -} - -impl ParamLike for solang_parser::pt::ErrorParameter { - fn ty(&self) -> &Expression { +impl ParamLike for ParamInfo { + fn type_name(&self) -> &str { &self.ty } fn name(&self) -> Option<&str> { - self.name.as_ref().map(|id| id.name.as_str()) + self.name.as_deref() } } @@ -60,8 +25,8 @@ impl ParamLike for &T where T: ParamLike, { - fn ty(&self) -> &Expression { - T::ty(*self) + fn type_name(&self) -> &str { + T::type_name(*self) } fn name(&self) -> Option<&str> { diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 03d569c17f500..801e813026a39 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -36,7 +36,7 @@ alloy-primitives = { workspace = true, features = [ alloy-provider.workspace = true alloy-network.workspace = true alloy-consensus.workspace = true -alloy-op-evm.workspace = true +alloy-op-evm = { workspace = true, optional = true } alloy-rpc-types = { workspace = true, features = ["anvil"] } alloy-sol-types.workspace = true alloy-rlp.workspace = true @@ -54,9 +54,10 @@ revm = { workspace = true, features = [ "blst", ] } revm-inspectors.workspace = true -op-alloy-consensus = { workspace = true, features = ["k256"] } -op-alloy-network.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } +op-alloy-network = { workspace = true, optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } tempo-revm.workspace = true tempo-alloy.workspace = true tempo-contracts.workspace = true @@ -77,7 +78,18 @@ url.workspace = true [dev-dependencies] alloy-serde.workspace = true -op-alloy-consensus.workspace = true -op-alloy-rpc-types.workspace = true anvil.workspace = true foundry-test-utils.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-network", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", + "foundry-common/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", +] diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 0cfd56a44219c..b836023a968b7 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -223,8 +223,8 @@ fn trimmed_hex(s: &[u8]) -> String { } else { format!( "{}…{} ({} bytes)", - &hex::encode(&s[..n / 2]), - &hex::encode(&s[s.len() - n / 2..]), + hex::encode(&s[..n / 2]), + hex::encode(&s[s.len() - n / 2..]), s.len(), ) } diff --git a/crates/evm/core/src/env.rs b/crates/evm/core/src/env.rs index 132b986f55e7f..76429e4bc78e3 100644 --- a/crates/evm/core/src/env.rs +++ b/crates/evm/core/src/env.rs @@ -4,13 +4,9 @@ use alloy_consensus::Typed2718; pub use alloy_evm::EvmEnv; use alloy_evm::FromRecoveredTx; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, U256}; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; -use op_revm::{ - OpTransaction, - transaction::{OpTxTr, deposit::DEPOSIT_TRANSACTION_TYPE}, -}; +#[cfg(feature = "optimism")] +use op_revm::transaction::deposit::DEPOSIT_TRANSACTION_TYPE; use revm::{ Context, Database, Journal, context::{Block, BlockEnv, Cfg, CfgEnv, Transaction, TxEnv}, @@ -236,9 +232,16 @@ pub trait FoundryTransaction: Transaction { /// Sets whether the transaction is a system transaction fn set_system_transaction(&mut self, _is_system_transaction: bool) {} - /// Returns `true` if transaction is of type [`DEPOSIT_TRANSACTION_TYPE`]. + /// Returns `true` if transaction is an Optimism deposit transaction. fn is_deposit(&self) -> bool { - self.tx_type() == DEPOSIT_TRANSACTION_TYPE + #[cfg(feature = "optimism")] + { + self.tx_type() == DEPOSIT_TRANSACTION_TYPE + } + #[cfg(not(feature = "optimism"))] + { + false + } } // Tempo methods @@ -320,188 +323,6 @@ impl FoundryTransaction for TxEnv { } } -impl FoundryTransaction for OpTransaction { - fn set_tx_type(&mut self, tx_type: u8) { - self.base.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.base.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.base.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.base.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.base.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.base.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.base.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.base.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.base.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.base.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.base.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.base.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - OpTxTr::enveloped_tx(self) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.enveloped_tx = Some(bytes); - } - - fn source_hash(&self) -> Option { - OpTxTr::source_hash(self) - } - - fn set_source_hash(&mut self, source_hash: B256) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.source_hash = source_hash; - } - } - - fn mint(&self) -> Option { - OpTxTr::mint(self) - } - - fn set_mint(&mut self, mint: u128) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.mint = Some(mint); - } - } - - fn is_system_transaction(&self) -> bool { - OpTxTr::is_system_transaction(self) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { - self.deposit.is_system_transaction = is_system_transaction; - } - } -} - -impl FoundryTransaction for OpTx { - fn set_tx_type(&mut self, tx_type: u8) { - self.0.set_tx_type(tx_type); - } - - fn set_caller(&mut self, caller: Address) { - self.0.set_caller(caller); - } - - fn set_gas_limit(&mut self, gas_limit: u64) { - self.0.set_gas_limit(gas_limit); - } - - fn set_gas_price(&mut self, gas_price: u128) { - self.0.set_gas_price(gas_price); - } - - fn set_kind(&mut self, kind: TxKind) { - self.0.set_kind(kind); - } - - fn set_value(&mut self, value: U256) { - self.0.set_value(value); - } - - fn set_data(&mut self, data: Bytes) { - self.0.set_data(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.0.set_nonce(nonce); - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.0.set_chain_id(chain_id); - } - - fn set_access_list(&mut self, access_list: AccessList) { - self.0.set_access_list(access_list); - } - - fn authorization_list_mut( - &mut self, - ) -> &mut Vec> { - self.0.authorization_list_mut() - } - - fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { - self.0.set_gas_priority_fee(gas_priority_fee); - } - - fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} - - fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} - - fn enveloped_tx(&self) -> Option<&Bytes> { - FoundryTransaction::enveloped_tx(&self.0) - } - - fn set_enveloped_tx(&mut self, bytes: Bytes) { - self.0.set_enveloped_tx(bytes); - } - - fn source_hash(&self) -> Option { - FoundryTransaction::source_hash(&self.0) - } - - fn set_source_hash(&mut self, source_hash: B256) { - self.0.set_source_hash(source_hash); - } - - fn mint(&self) -> Option { - FoundryTransaction::mint(&self.0) - } - - fn set_mint(&mut self, mint: u128) { - self.0.set_mint(mint); - } - - fn is_system_transaction(&self) -> bool { - FoundryTransaction::is_system_transaction(&self.0) - } - - fn set_system_transaction(&mut self, is_system_transaction: bool) { - self.0.set_system_transaction(is_system_transaction); - } -} - impl FoundryTransaction for TempoTxEnv { fn set_tx_type(&mut self, tx_type: u8) { self.inner.set_tx_type(tx_type); @@ -687,32 +508,6 @@ impl FromAnyRpcTransaction for TxEnv { } } -impl FromAnyRpcTransaction for OpTx { - fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { - if let Some(envelope) = tx.as_envelope() { - return Ok(Self(OpTransaction:: { - base: TxEnv::from_recovered_tx(envelope, tx.from()), - enveloped_tx: None, - deposit: Default::default(), - })); - } - - // Handle OP deposit transactions from `Unknown` envelope variant. - if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner - && unknown.ty() == DEPOSIT_TX_TYPE_ID - { - let mut fields = unknown.inner.fields.clone(); - fields.insert("from".to_string(), serde_json::to_value(tx.from())?); - let deposit_tx: TxDeposit = fields - .deserialize_into() - .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; - return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); - } - - eyre::bail!("cannot convert unknown transaction type to OpTransaction") - } -} - impl FromAnyRpcTransaction for TempoTxEnv { fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { use alloy_consensus::Transaction as _; @@ -747,22 +542,234 @@ impl FromAnyRpcTransaction for TempoTxEnv { } } +#[cfg(feature = "optimism")] +mod optimism { + use super::*; + use alloy_op_evm::OpTx; + use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; + use op_revm::{OpTransaction, transaction::OpTxTr}; + + impl FoundryTransaction for OpTransaction { + fn set_tx_type(&mut self, tx_type: u8) { + self.base.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.base.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.base.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.base.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.base.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.base.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.base.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.base.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.base.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.base.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.base.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.base.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + OpTxTr::enveloped_tx(self) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.enveloped_tx = Some(bytes); + } + + fn source_hash(&self) -> Option { + OpTxTr::source_hash(self) + } + + fn set_source_hash(&mut self, source_hash: B256) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.source_hash = source_hash; + } + } + + fn mint(&self) -> Option { + OpTxTr::mint(self) + } + + fn set_mint(&mut self, mint: u128) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.mint = Some(mint); + } + } + + fn is_system_transaction(&self) -> bool { + OpTxTr::is_system_transaction(self) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + self.deposit.is_system_transaction = is_system_transaction; + } + } + } + + impl FoundryTransaction for OpTx { + fn set_tx_type(&mut self, tx_type: u8) { + self.0.set_tx_type(tx_type); + } + + fn set_caller(&mut self, caller: Address) { + self.0.set_caller(caller); + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.0.set_gas_limit(gas_limit); + } + + fn set_gas_price(&mut self, gas_price: u128) { + self.0.set_gas_price(gas_price); + } + + fn set_kind(&mut self, kind: TxKind) { + self.0.set_kind(kind); + } + + fn set_value(&mut self, value: U256) { + self.0.set_value(value); + } + + fn set_data(&mut self, data: Bytes) { + self.0.set_data(data); + } + + fn set_nonce(&mut self, nonce: u64) { + self.0.set_nonce(nonce); + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.0.set_chain_id(chain_id); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.0.set_access_list(access_list); + } + + fn authorization_list_mut( + &mut self, + ) -> &mut Vec> { + self.0.authorization_list_mut() + } + + fn set_gas_priority_fee(&mut self, gas_priority_fee: Option) { + self.0.set_gas_priority_fee(gas_priority_fee); + } + + fn set_blob_hashes(&mut self, _blob_hashes: Vec) {} + + fn set_max_fee_per_blob_gas(&mut self, _max_fee_per_blob_gas: u128) {} + + fn enveloped_tx(&self) -> Option<&Bytes> { + FoundryTransaction::enveloped_tx(&self.0) + } + + fn set_enveloped_tx(&mut self, bytes: Bytes) { + self.0.set_enveloped_tx(bytes); + } + + fn source_hash(&self) -> Option { + FoundryTransaction::source_hash(&self.0) + } + + fn set_source_hash(&mut self, source_hash: B256) { + self.0.set_source_hash(source_hash); + } + + fn mint(&self) -> Option { + FoundryTransaction::mint(&self.0) + } + + fn set_mint(&mut self, mint: u128) { + self.0.set_mint(mint); + } + + fn is_system_transaction(&self) -> bool { + FoundryTransaction::is_system_transaction(&self.0) + } + + fn set_system_transaction(&mut self, is_system_transaction: bool) { + self.0.set_system_transaction(is_system_transaction); + } + } + + impl FromAnyRpcTransaction for OpTx { + fn from_any_rpc_transaction(tx: &AnyRpcTransaction) -> eyre::Result { + if let Some(envelope) = tx.as_envelope() { + return Ok(Self(OpTransaction:: { + base: TxEnv::from_recovered_tx(envelope, tx.from()), + enveloped_tx: None, + deposit: Default::default(), + })); + } + + // Handle OP deposit transactions from `Unknown` envelope variant. + if let AnyTxEnvelope::Unknown(unknown) = &*tx.inner.inner + && unknown.ty() == DEPOSIT_TX_TYPE_ID + { + let mut fields = unknown.inner.fields.clone(); + fields.insert("from".to_string(), serde_json::to_value(tx.from())?); + let deposit_tx: TxDeposit = fields + .deserialize_into() + .map_err(|e| eyre::eyre!("failed to deserialize deposit tx: {e}"))?; + return Ok(Self::from_recovered_tx(&deposit_tx, deposit_tx.from)); + } + + eyre::bail!("cannot convert unknown transaction type to OpTransaction") + } + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU64; use super::*; - use alloy_consensus::{Sealed, Signed, TxEip1559, transaction::Recovered}; + use alloy_consensus::{Signed, TxEip1559, transaction::Recovered}; use alloy_evm::{EthEvmFactory, EvmFactory}; use alloy_network::{AnyTxType, UnknownTxEnvelope, UnknownTypedTransaction}; - use alloy_op_evm::OpEvmFactory; use alloy_primitives::Signature; use alloy_rpc_types::{Transaction as RpcTransaction, TransactionInfo}; use alloy_serde::WithOtherFields; use foundry_evm_hardforks::TempoHardfork; - use op_alloy_consensus::{OpTxEnvelope, transaction::OpTransactionInfo}; - use op_alloy_rpc_types::Transaction as OpRpcTransaction; - use op_revm::OpSpecId; use revm::database::EmptyDB; use tempo_alloy::primitives::{ AASigned, TempoSignature, TempoTransaction, TempoTxEnvelope, @@ -793,30 +800,6 @@ mod tests { evm.ctx_mut().set_evm(evm_env); } - #[test] - fn op_evm_foundry_context_ext_implementation() { - let mut evm = - OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); - - // Test EVM Context Block mutation - evm.ctx_mut().block_mut().set_number(U256::from(123)); - assert_eq!(evm.ctx().block().number(), U256::from(123)); - - // Test EVM Context Tx mutation - evm.ctx_mut().tx_mut().set_nonce(99); - assert_eq!(evm.ctx().tx().nonce(), 99); - - // Test EVM Context Cfg mutation - evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; - assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); - - // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env - let tx_env = evm.ctx().tx_clone(); - evm.ctx_mut().set_tx(tx_env); - let evm_env = evm.ctx().evm_clone(); - evm.ctx_mut().set_evm(evm_env); - } - #[test] fn tempo_evm_foundry_context_ext_implementation() { let mut evm = TempoEvmFactory::default().create_evm(EmptyDB::default(), EvmEnv::default()); @@ -874,23 +857,6 @@ mod tests { assert_eq!(tx_env.kind, TxKind::Call(Address::with_last_byte(0xBB))); } - #[test] - fn from_any_rpc_transaction_for_op() { - let from = Address::random(); - let signed_tx = make_signed_eip1559(); - - // Build the eth TxEnv to compare against op base - let rpc_tx = RpcTransaction::from_transaction( - Recovered::new_unchecked(signed_tx.into(), from), - TransactionInfo::default(), - ); - let any_tx = >::from(rpc_tx); - let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base, expected_base); - } - #[test] fn from_any_rpc_transaction_unknown_envelope_errors() { let unknown = AnyTxEnvelope::Unknown(UnknownTxEnvelope { @@ -915,39 +881,6 @@ mod tests { assert!(result.to_string().contains("unknown transaction type")); } - #[test] - fn from_any_rpc_transaction_for_op_deposit() { - let from = Address::random(); - let source_hash = B256::random(); - let deposit = TxDeposit { - source_hash, - from, - to: TxKind::Call(Address::with_last_byte(0xCC)), - mint: 1111, - value: U256::from(200), - gas_limit: 21000, - is_system_transaction: true, - input: Default::default(), - }; - - // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as AnyRpcTransaction. - let op_rpc_tx = OpRpcTransaction::from_transaction( - Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), - OpTransactionInfo::default(), - ); - let json = serde_json::to_value(&op_rpc_tx).unwrap(); - let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); - - let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); - assert_eq!(op_tx_env.base.caller, from); - assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); - assert_eq!(op_tx_env.base.value, U256::from(200)); - assert_eq!(op_tx_env.base.gas_limit, 21000); - assert_eq!(op_tx_env.deposit.source_hash, source_hash); - assert_eq!(op_tx_env.deposit.mint, Some(1111)); - assert!(op_tx_env.deposit.is_system_transaction); - } - #[test] fn from_any_rpc_transaction_for_tempo_eth_envelope() { let from = Address::random(); @@ -1004,4 +937,89 @@ mod tests { assert_eq!(tx_env.inner.chain_id, Some(42431)); assert_eq!(tx_env.fee_token, fee_token); } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + use alloy_consensus::Sealed; + use alloy_op_evm::{OpEvmFactory, OpTx}; + use op_alloy_consensus::{OpTxEnvelope, TxDeposit, transaction::OpTransactionInfo}; + use op_alloy_rpc_types::Transaction as OpRpcTransaction; + use op_revm::OpSpecId; + + #[test] + fn op_evm_foundry_context_ext_implementation() { + let mut evm = + OpEvmFactory::::default().create_evm(EmptyDB::default(), EvmEnv::default()); + + // Test EVM Context Block mutation + evm.ctx_mut().block_mut().set_number(U256::from(123)); + assert_eq!(evm.ctx().block().number(), U256::from(123)); + + // Test EVM Context Tx mutation + evm.ctx_mut().tx_mut().set_nonce(99); + assert_eq!(evm.ctx().tx().nonce(), 99); + + // Test EVM Context Cfg mutation + evm.ctx_mut().cfg_mut().spec = OpSpecId::JOVIAN; + assert_eq!(evm.ctx().cfg().spec, OpSpecId::JOVIAN); + + // Round-trip test to ensure no issues with cloning and setting tx_env and evm_env + let tx_env = evm.ctx().tx_clone(); + evm.ctx_mut().set_tx(tx_env); + let evm_env = evm.ctx().evm_clone(); + evm.ctx_mut().set_evm(evm_env); + } + + #[test] + fn from_any_rpc_transaction_for_op() { + let from = Address::random(); + let signed_tx = make_signed_eip1559(); + + // Build the eth TxEnv to compare against op base + let rpc_tx = RpcTransaction::from_transaction( + Recovered::new_unchecked(signed_tx.into(), from), + TransactionInfo::default(), + ); + let any_tx = >::from(rpc_tx); + let expected_base = TxEnv::from_any_rpc_transaction(&any_tx).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base, expected_base); + } + + #[test] + fn from_any_rpc_transaction_for_op_deposit() { + let from = Address::random(); + let source_hash = B256::random(); + let deposit = TxDeposit { + source_hash, + from, + to: TxKind::Call(Address::with_last_byte(0xCC)), + mint: 1111, + value: U256::from(200), + gas_limit: 21000, + is_system_transaction: true, + input: Default::default(), + }; + + // Build a concrete OpRpcTransaction, serialize to JSON, deserialize as + // AnyRpcTransaction. + let op_rpc_tx = OpRpcTransaction::from_transaction( + Recovered::new_unchecked(OpTxEnvelope::Deposit(Sealed::new(deposit)), from), + OpTransactionInfo::default(), + ); + let json = serde_json::to_value(&op_rpc_tx).unwrap(); + let any_tx: AnyRpcTransaction = serde_json::from_value(json).unwrap(); + + let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); + assert_eq!(op_tx_env.base.caller, from); + assert_eq!(op_tx_env.base.kind, TxKind::Call(Address::with_last_byte(0xCC))); + assert_eq!(op_tx_env.base.value, U256::from(200)); + assert_eq!(op_tx_env.base.gas_limit, 21000); + assert_eq!(op_tx_env.deposit.source_hash, source_hash); + assert_eq!(op_tx_env.deposit.mint, Some(1111)); + assert!(op_tx_env.deposit.is_system_transaction); + } + } } diff --git a/crates/evm/core/src/evm/mod.rs b/crates/evm/core/src/evm/mod.rs index 708226be003a2..fc9e9e7d2810f 100644 --- a/crates/evm/core/src/evm/mod.rs +++ b/crates/evm/core/src/evm/mod.rs @@ -10,14 +10,11 @@ use alloy_evm::{ EthEvmFactory, Evm, EvmEnv, EvmFactory, FromRecoveredTx, precompiles::PrecompilesMap, }; use alloy_network::{Ethereum, Network}; -use alloy_op_evm::OpEvmFactory; use alloy_primitives::{Address, Signature, U256}; use alloy_rlp::Decodable; use foundry_common::{FoundryReceiptResponse, FoundryTransactionBuilder, fmt::UIfmt}; use foundry_config::FromEvmVersion; use foundry_fork_db::{DatabaseError, ForkBlockEnv}; -use op_alloy_network::Optimism; -use op_revm::OpHaltReason; use revm::{ Database, context::{ @@ -36,10 +33,12 @@ use tempo_evm::evm::TempoEvmFactory; use tempo_revm::TempoHaltReason; pub mod eth; +#[cfg(feature = "optimism")] pub mod op; pub mod tempo; pub use eth::*; +#[cfg(feature = "optimism")] pub use op::*; pub use tempo::*; @@ -75,13 +74,6 @@ impl FoundryEvmNetwork for TempoEvmNetwork { type EvmFactory = TempoEvmFactory; } -#[derive(Clone, Copy, Debug, Default)] -pub struct OpEvmNetwork; -impl FoundryEvmNetwork for OpEvmNetwork { - type Network = Optimism; - type EvmFactory = OpEvmFactory; -} - /// Convenience type aliases for accessing associated types through [`FoundryEvmNetwork`]. pub type EvmFactoryFor = ::EvmFactory; pub type FoundryContextFor<'db, FEN> = @@ -249,15 +241,6 @@ impl IntoInstructionResult for HaltReason { } } -impl IntoInstructionResult for OpHaltReason { - fn into_instruction_result(self) -> InstructionResult { - match self { - Self::Base(eth) => eth.into(), - Self::FailedDeposit => InstructionResult::Stop, - } - } -} - impl IntoInstructionResult for TempoHaltReason { fn into_instruction_result(self) -> InstructionResult { match self { diff --git a/crates/evm/core/src/evm/op.rs b/crates/evm/core/src/evm/op.rs index cb8bf272d9a05..efb74ad3abf50 100644 --- a/crates/evm/core/src/evm/op.rs +++ b/crates/evm/core/src/evm/op.rs @@ -1,6 +1,7 @@ use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; use alloy_op_evm::{OpEvm, OpEvmContext, OpEvmFactory, OpTx}; use foundry_fork_db::DatabaseError; +use op_alloy_network::Optimism; use op_revm::{OpEvm as RevmEvm, OpHaltReason, OpSpecId, OpTransactionError, handler::OpHandler}; use revm::{ context::{ @@ -10,16 +11,33 @@ use revm::{ handler::{EthFrame, EvmTr, FrameResult, Handler, instructions::EthInstructions}, inspector::InspectorHandler, interpreter::{ - FrameInput, SharedMemory, interpreter::EthInterpreter, interpreter_action::FrameInit, + FrameInput, InstructionResult, SharedMemory, interpreter::EthInterpreter, + interpreter_action::FrameInit, }, }; use crate::{ FoundryContextExt, FoundryInspectorExt, backend::{DatabaseExt, JournaledState}, - evm::{FoundryEvmFactory, NestedEvm}, + evm::{FoundryEvmFactory, FoundryEvmNetwork, IntoInstructionResult, NestedEvm}, }; +#[derive(Clone, Copy, Debug, Default)] +pub struct OpEvmNetwork; +impl FoundryEvmNetwork for OpEvmNetwork { + type Network = Optimism; + type EvmFactory = OpEvmFactory; +} + +impl IntoInstructionResult for OpHaltReason { + fn into_instruction_result(self) -> InstructionResult { + match self { + Self::Base(eth) => eth.into(), + Self::FailedDeposit => InstructionResult::Stop, + } + } +} + type OpEvmHandler<'db, I> = OpHandler, EVMError, EthFrame>; diff --git a/crates/evm/core/src/fork/database.rs b/crates/evm/core/src/fork/database.rs index aefa0e2ee9741..2284823047ca6 100644 --- a/crates/evm/core/src/fork/database.rs +++ b/crates/evm/core/src/fork/database.rs @@ -212,13 +212,18 @@ pub struct ForkDbStateSnapshot { } impl ForkDbStateSnapshot { - fn get_storage(&self, address: Address, index: U256) -> Option { - self.local - .cache - .accounts - .get(&address) - .and_then(|account| account.storage.get(&index)) - .copied() + /// Lookup storage in `state_snapshot`, then fall back to the backend (remote RPC). + fn storage_from_snapshot_or_backend( + &self, + address: Address, + index: U256, + ) -> Result { + // Check state_snapshot.storage first (data fetched by SharedBackend / disk cache). + if let Some(val) = self.state_snapshot.storage.get(&address).and_then(|s| s.get(&index)) { + return Ok(*val); + } + // Fall back to the underlying backend (SharedBackend → remote RPC). + DatabaseRef::storage_ref(&self.local, address, index) } } @@ -250,15 +255,9 @@ impl DatabaseRef for ForkDbStateSnapshot { match self.local.cache.accounts.get(&address) { Some(account) => match account.storage.get(&index) { Some(entry) => Ok(*entry), - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), - }, - }, - None => match self.get_storage(address, index) { - None => DatabaseRef::storage_ref(&self.local, address, index), - Some(storage) => Ok(storage), + None => self.storage_from_snapshot_or_backend(address, index), }, + None => self.storage_from_snapshot_or_backend(address, index), } } @@ -303,4 +302,28 @@ mod tests { assert!(loaded.is_some()); assert_eq!(loaded.unwrap(), info); } + + /// Verifies that `ForkDbStateSnapshot::storage_ref` reads from `state_snapshot.storage` + /// when the slot is missing from `local.cache.accounts`. Without this lookup the call + /// would fall through to the backend and return the unrelated remote value. + #[tokio::test(flavor = "multi_thread")] + async fn fork_db_state_snapshot_reads_storage_from_snapshot() { + let rpc = foundry_test_utils::rpc::next_http_rpc_endpoint(); + let provider = get_http_provider(rpc.clone()); + let meta = BlockchainDbMeta::new(BlockEnv::default(), rpc); + let db = BlockchainDb::new(meta, None); + let backend = SharedBackend::spawn_backend(Arc::new(provider), db, None).await; + + let address = Address::random(); + let slot = U256::from(42u64); + let expected = U256::from(0xdeadbeefu64); + + let mut state_snapshot = StateSnapshot::default(); + state_snapshot.storage.entry(address).or_default().insert(slot, expected); + + let snapshot = ForkDbStateSnapshot { local: CacheDB::new(backend), state_snapshot }; + + let got = DatabaseRef::storage_ref(&snapshot, address, slot).unwrap(); + assert_eq!(got, expected); + } } diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index 1b2201a9b8b84..c2edbb9dfd33b 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -5,6 +5,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "optimism")] +use op_alloy_rpc_types as _; + use crate::constants::DEFAULT_CREATE2_DEPLOYER; use alloy_primitives::{Address, map::HashMap}; use auto_impl::auto_impl; diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 82efd4a1aaaaa..ab68eb08821e8 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -137,8 +137,12 @@ impl EvmOpts { /// [`NetworkConfigs::with_chain_id`] to auto-enable the correct network /// (e.g. Tempo, OP Stack) based on the chain ID. pub async fn infer_network_from_fork(&mut self) { + #[cfg(feature = "optimism")] + let already_op = self.networks.is_optimism(); + #[cfg(not(feature = "optimism"))] + let already_op = false; if !self.networks.is_tempo() - && !self.networks.is_optimism() + && !already_op && let Some(ref fork_url) = self.fork_url && let Ok(provider) = self.fork_provider_with_url::(fork_url) && let Ok(chain_id) = provider.get_chain_id().await @@ -474,6 +478,7 @@ mod tests { // Plain anvil (chain id 31337) without tempo flag -> Ethereum (no network flags set). assert!(!evm_opts.networks.is_tempo()); + #[cfg(feature = "optimism")] assert!(!evm_opts.networks.is_optimism()); assert!(!evm_opts.networks.is_celo()); assert_eq!(evm_opts.networks, NetworkConfigs::default()); diff --git a/crates/evm/coverage/Cargo.toml b/crates/evm/coverage/Cargo.toml index d2d7b077ee9f0..758604564726e 100644 --- a/crates/evm/coverage/Cargo.toml +++ b/crates/evm/coverage/Cargo.toml @@ -25,3 +25,7 @@ semver.workspace = true tracing.workspace = true rayon.workspace = true solar.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 6bf7163d6e023..dd5138f532074 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -62,3 +62,16 @@ serde.workspace = true uuid.workspace = true rayon.workspace = true tokio.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm-core/optimism", + "foundry-evm-hardforks/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-fuzz/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 33152b73dda3c..1932a834ab397 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -17,7 +17,7 @@ use foundry_evm_core::{ use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::{ BaseCounterExample, BasicTxDetails, CallDetails, CounterExample, FuzzCase, FuzzError, - FuzzFixtures, FuzzTestResult, + FuzzFixtures, FuzzRunMetadata, FuzzTestResult, strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state}, }; use foundry_evm_traces::SparsedTraceArena; @@ -71,6 +71,8 @@ struct WorkerState { runs: u32, /// Failure reason if this worker failed failure: Option, + /// Fuzz run metadata that produced the failure. + failure_run: Option, /// Last run timestamp in milliseconds /// /// Used to identify which worker ran last and collect its traces and call breakpoints @@ -93,6 +95,7 @@ impl WorkerState { deprecated_cheatcodes: HashMap::default(), runs: 0, failure: None, + failure_run: None, last_run_timestamp: 0, failed_corpus_replays: 0, } @@ -196,8 +199,14 @@ impl FuzzedExecutor { config: FuzzConfig, persisted_failure: Option, ) -> Self { - let max_workers = - if config.runs == 0 { 0 } else { Ord::max(1, config.runs / MIN_RUNS_PER_WORKER) }; + let run_limit = if config.run.is_some() { 1 } else { config.runs }; + let max_workers = if run_limit == 0 { + 0 + } else if config.run.is_some() { + 1 + } else { + Ord::max(1, run_limit / MIN_RUNS_PER_WORKER) + }; let num_workers = Ord::min(rayon::current_num_threads(), max_workers as usize); Self { executor_f: executor, runner, sender, config, persisted_failure, num_workers } } @@ -221,8 +230,9 @@ impl FuzzedExecutor { ) -> Result { let shared_state = SharedFuzzState::new(state, self.config.timeout, early_exit.clone()); - debug!(n = self.num_workers, "spawning workers"); - let workers = (0..self.num_workers) + let worker_ids = self.worker_ids(); + debug!(n = worker_ids.len(), "spawning workers"); + let workers = worker_ids .into_par_iter() .map(|worker_id| { let _guard = tokio_handle.enter(); @@ -364,8 +374,14 @@ impl FuzzedExecutor { } else { vec![] }; + let fuzz = failed_worker.failure_run.unwrap_or_default(); result.counterexample = Some(CounterExample::Single( - BaseCounterExample::from_fuzz_call(calldata, args, call.traces), + BaseCounterExample::from_fuzz_call(calldata, args, call.traces) + .with_fuzz_metadata(FuzzRunMetadata::new( + fuzz.seed.or(self.config.seed), + fuzz.run, + fuzz.worker, + )), )); } Some(TestCaseError::Reject(reason)) => { @@ -453,16 +469,7 @@ impl FuzzedExecutor { runner_config.cases = worker_runs; let mut runner = if let Some(seed) = self.config.seed { - // For deterministic parallel fuzzing, derive a unique seed for each worker - let worker_seed = if worker_id == 0 { - // Master worker uses the provided seed as is. - seed - } else { - // Derive a worker-specific seed using keccak256(seed || worker_id) - let seed_data = - [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); - U256::from_be_bytes(keccak256(seed_data).0) - }; + let worker_seed = Self::fuzz_worker_seed(seed, worker_id); trace!(target: "forge::test", ?worker_seed, "deterministic seed for worker {worker_id}"); let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &worker_seed.to_be_bytes::<32>()); TestRunner::new_with_rng(runner_config, rng) @@ -470,11 +477,25 @@ impl FuzzedExecutor { TestRunner::new(runner_config) }; - let mut persisted_failure = self.persisted_failure.as_ref().filter(|_| worker_id == 0); + if let Some(target_run) = self.config.run { + for _ in 1..target_run { + if let Err(err) = corpus.new_input(&mut runner, &shared_state.state, func) { + worker.failure = Some(TestCaseError::fail(format!( + "failed to generate fuzzed input in worker {}: {err}", + worker.id + ))); + shared_state.try_claim_failure(worker_id); + return Ok(worker); + } + } + } + + let mut persisted_failure = + self.persisted_failure.as_ref().filter(|_| worker_id == 0 && self.config.run.is_none()); // Offset to stagger corpus syncs across workers; so that workers don't sync at the same // time. - let sync_offset = worker_id as u32 * 100; + let sync_offset = (worker_id as u32).saturating_mul(100); let sync_threshold = SYNC_INTERVAL + sync_offset; let mut runs_since_sync = sync_threshold; // Always sync at the start. let mut last_metrics_report = Instant::now(); @@ -483,11 +504,27 @@ impl FuzzedExecutor { // 2. Worker hasn't reached its specific run limit 'stop: while shared_state.should_continue() && worker.runs < worker_runs { // If counterexample recorded, replay it first, without incrementing runs. - let input = if worker_id == 0 + let (input, fuzz_run) = if worker_id == 0 && let Some(failure) = persisted_failure.take() && failure.calldata.get(..4).is_some_and(|selector| func.selector() == selector) { - failure.calldata.clone() + let seed = failure.fuzz.seed.or(self.config.seed); + if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() + && let Some(seed) = seed + { + let run = failure.fuzz.run.unwrap_or(1); + let worker = failure.fuzz.worker.unwrap_or(worker_id as u32) as usize; + cheats.set_seed(Self::fuzz_run_seed(seed, worker, run)); + } + + ( + failure.calldata.clone(), + Some(FuzzRunMetadata::new( + seed, + failure.fuzz.run, + Some(failure.fuzz.worker.unwrap_or(worker_id as u32)), + )), + ) } else { runs_since_sync += 1; if runs_since_sync >= sync_threshold { @@ -503,13 +540,14 @@ impl FuzzedExecutor { runs_since_sync = 0; } + let fuzz_run = self.config.run.unwrap_or(worker.runs + 1); if let Some(cheats) = executor.inspector_mut().cheatcodes.as_mut() && let Some(seed) = self.config.seed { - cheats.set_seed(seed.wrapping_add(U256::from(worker.runs))); + cheats.set_seed(Self::fuzz_run_seed(seed, worker_id, fuzz_run)); } - match corpus.new_input(&mut runner, &shared_state.state, func) { + let input = match corpus.new_input(&mut runner, &shared_state.state, func) { Ok(input) => input, Err(err) => { worker.failure = Some(TestCaseError::fail(format!( @@ -519,13 +557,24 @@ impl FuzzedExecutor { shared_state.try_claim_failure(worker_id); break 'stop; } - } + }; + + ( + input, + Some(FuzzRunMetadata::new( + self.config.seed, + Some(fuzz_run), + Some(worker_id as u32), + )), + ) }; let mut inc_runs = || { let total_runs = shared_state.increment_runs(); debug_assert!( - shared_state.timer.is_enabled() || total_runs <= self.config.runs, + shared_state.timer.is_enabled() + || total_runs + <= if self.config.run.is_some() { 1 } else { self.config.runs }, "worker runs were not distributed correctly" ); worker.runs += 1; @@ -595,6 +644,7 @@ impl FuzzedExecutor { .. }) => { inc_runs(); + worker.failure_run = fuzz_run; // Only classify magic skip payloads when the revert originates from the // cheatcode address. @@ -656,7 +706,7 @@ impl FuzzedExecutor { /// Determines the number of runs per worker. const fn runs_per_worker(&self, worker_id: usize) -> u32 { let worker_id = worker_id as u32; - let total_runs = self.config.runs; + let total_runs = if self.config.run.is_some() { 1 } else { self.config.runs }; let n = self.num_workers as u32; let runs = total_runs / n; let remainder = total_runs % n; @@ -664,4 +714,29 @@ impl FuzzedExecutor { // assuming `worker_id` is in `0..n`. if worker_id < remainder { runs + 1 } else { runs } } + + /// Returns the worker IDs to execute. + fn worker_ids(&self) -> Vec { + if self.config.run.is_some() { + vec![self.config.worker.unwrap_or(0) as usize] + } else { + (0..self.num_workers).collect() + } + } + + /// Derives the deterministic RNG seed for a fuzz worker. + fn fuzz_worker_seed(seed: U256, worker_id: usize) -> U256 { + if worker_id == 0 { + seed + } else { + let worker_id = worker_id as u32; + let seed_data = [&seed.to_be_bytes::<32>()[..], &worker_id.to_be_bytes()[..]].concat(); + U256::from_be_bytes(keccak256(seed_data).0) + } + } + + /// Derives the deterministic RNG seed for cheatcode randomness in a worker-local run. + fn fuzz_run_seed(seed: U256, worker_id: usize, run: u32) -> U256 { + Self::fuzz_worker_seed(seed, worker_id).wrapping_add(U256::from(run.saturating_sub(1))) + } } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 27d8e6a0ed588..e02cdbc393ee6 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -736,7 +736,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { if !msg.is_empty() { msg.push_str(", "); } - msg.push_str(&format!("{}", &corpus_manager.metrics)); + msg.push_str(&format!("{}", corpus_manager.metrics)); } progress.set_message(msg); } diff --git a/crates/evm/fuzz/Cargo.toml b/crates/evm/fuzz/Cargo.toml index 62e4e80a73674..5629b17d936da 100644 --- a/crates/evm/fuzz/Cargo.toml +++ b/crates/evm/fuzz/Cargo.toml @@ -50,3 +50,12 @@ rand.workspace = true serde.workspace = true thiserror.workspace = true tracing.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm-core/optimism", + "foundry-evm-coverage/optimism", + "foundry-evm-traces/optimism", +] diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 44d71fb6deee3..9c3e7d179c7f6 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -33,6 +33,27 @@ pub use strategies::LiteralMaps; mod inspector; pub use inspector::Fuzzer; +/// Metadata needed to reproduce a fuzz run. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct FuzzRunMetadata { + /// Seed used for the worker's input stream. + #[serde(default, rename = "fuzz_seed", skip_serializing_if = "Option::is_none")] + pub seed: Option, + /// 1-based run inside the worker's input stream. + #[serde(default, rename = "fuzz_run", skip_serializing_if = "Option::is_none")] + pub run: Option, + /// Worker that generated the input stream. + #[serde(default, rename = "fuzz_worker", skip_serializing_if = "Option::is_none")] + pub worker: Option, +} + +impl FuzzRunMetadata { + /// Creates metadata for reproducing a fuzz run. + pub const fn new(seed: Option, run: Option, worker: Option) -> Self { + Self { seed, run, worker } + } +} + /// Details of a transaction generated by fuzz strategy for fuzzing a target. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BasicTxDetails { @@ -102,6 +123,9 @@ pub struct BaseCounterExample { /// Whether to display sequence as solidity. #[serde(skip)] pub show_solidity: bool, + /// Fuzz metadata needed to reproduce this counterexample. + #[serde(flatten)] + pub fuzz: FuzzRunMetadata, } impl BaseCounterExample { @@ -137,6 +161,7 @@ impl BaseCounterExample { ), traces, show_solidity, + fuzz: FuzzRunMetadata::default(), }; } } @@ -154,6 +179,7 @@ impl BaseCounterExample { raw_args: None, traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } @@ -176,8 +202,15 @@ impl BaseCounterExample { raw_args: Some(foundry_common::fmt::format_tokens_raw(&args).format(", ").to_string()), traces, show_solidity: false, + fuzz: FuzzRunMetadata::default(), } } + + /// Sets fuzz metadata for reproducing this counterexample. + pub const fn with_fuzz_metadata(mut self, fuzz: FuzzRunMetadata) -> Self { + self.fuzz = fuzz; + self + } } impl fmt::Display for BaseCounterExample { @@ -229,7 +262,7 @@ impl fmt::Display for BaseCounterExample { if let Some(sig) = &self.signature { write!(f, "calldata={sig}")? } else { - write!(f, "calldata={}", &self.calldata)? + write!(f, "calldata={}", self.calldata)? } if let Some(args) = &self.args { diff --git a/crates/evm/hardforks/Cargo.toml b/crates/evm/hardforks/Cargo.toml index 9bf318028487a..68f6fb23bab07 100644 --- a/crates/evm/hardforks/Cargo.toml +++ b/crates/evm/hardforks/Cargo.toml @@ -16,10 +16,14 @@ workspace = true [dependencies] alloy-chains.workspace = true alloy-hardforks = { workspace = true, features = ["serde"] } -alloy-op-hardforks = { workspace = true, features = ["serde"] } +alloy-op-hardforks = { workspace = true, features = ["serde"], optional = true } alloy-rpc-types.workspace = true -op-revm.workspace = true +op-revm = { workspace = true, optional = true } revm.workspace = true serde = { workspace = true, features = ["derive"] } tempo-chainspec.workspace = true foundry-compilers.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "dep:op-revm"] diff --git a/crates/evm/hardforks/src/lib.rs b/crates/evm/hardforks/src/lib.rs index 8a29ebb7af4ec..a8e0d51738263 100644 --- a/crates/evm/hardforks/src/lib.rs +++ b/crates/evm/hardforks/src/lib.rs @@ -8,11 +8,13 @@ use std::str::FromStr; use alloy_chains::Chain; use alloy_rpc_types::BlockNumberOrTag; use foundry_compilers::artifacts::EvmVersion; +#[cfg(feature = "optimism")] use op_revm::OpSpecId; use revm::primitives::hardfork::SpecId; use serde::{Deserialize, Serialize}; pub use alloy_hardforks::EthereumHardfork; +#[cfg(feature = "optimism")] pub use alloy_op_hardforks::OpHardfork; pub use tempo_chainspec::hardfork::TempoHardfork; @@ -20,6 +22,7 @@ pub use tempo_chainspec::hardfork::TempoHardfork; #[serde(into = "String")] pub enum FoundryHardfork { Ethereum(EthereumHardfork), + #[cfg(feature = "optimism")] Optimism(OpHardfork), Tempo(TempoHardfork), } @@ -28,6 +31,7 @@ impl From for String { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(h) => format!("optimism:{h}"), FoundryHardfork::Tempo(h) => format!("tempo:{h}"), } @@ -64,6 +68,7 @@ impl FromStr for FoundryHardfork { .map(Self::Ethereum) .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")), + #[cfg(feature = "optimism")] "op" | "optimism" => OpHardfork::from_str(&fork) .map(Self::Optimism) .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")), @@ -83,6 +88,7 @@ impl FoundryHardfork { Self::Ethereum(h) } + #[cfg(feature = "optimism")] pub const fn optimism(h: OpHardfork) -> Self { Self::Optimism(h) } @@ -95,6 +101,7 @@ impl FoundryHardfork { pub fn name(&self) -> String { match self { Self::Ethereum(h) => format!("{h}"), + #[cfg(feature = "optimism")] Self::Optimism(h) => format!("{h}"), Self::Tempo(h) => format!("{h}"), } @@ -106,6 +113,7 @@ impl FoundryHardfork { pub const fn namespace(&self) -> Option<&'static str> { match self { Self::Ethereum(_) => None, + #[cfg(feature = "optimism")] Self::Optimism(_) => Some("optimism"), Self::Tempo(_) => Some("tempo"), } @@ -119,6 +127,7 @@ impl FoundryHardfork { if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Ethereum(fork)); } + #[cfg(feature = "optimism")] if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) { return Some(Self::Optimism(fork)); } @@ -143,12 +152,14 @@ impl From for EthereumHardfork { } } +#[cfg(feature = "optimism")] impl From for FoundryHardfork { fn from(value: OpHardfork) -> Self { Self::Optimism(value) } } +#[cfg(feature = "optimism")] impl From for OpHardfork { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -177,12 +188,14 @@ impl From for SpecId { fn from(fork: FoundryHardfork) -> Self { match fork { FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork).into(), FoundryHardfork::Tempo(hardfork) => hardfork.into(), } } } +#[cfg(feature = "optimism")] impl From for OpSpecId { fn from(fork: FoundryHardfork) -> Self { match fork { @@ -223,6 +236,7 @@ pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId { } /// Map an `OptimismHardfork` enum into its corresponding `OpSpecId`. +#[cfg(feature = "optimism")] pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId { match hardfork { OpHardfork::Bedrock => OpSpecId::BEDROCK, @@ -265,6 +279,7 @@ impl FromEvmVersion for SpecId { } } +#[cfg(feature = "optimism")] impl FromEvmVersion for OpSpecId { fn from_evm_version(version: EvmVersion) -> Self { match version { @@ -324,16 +339,6 @@ mod tests { assert_eq!(spec_id_from_ethereum_hardfork(EthereumHardfork::Osaka), SpecId::OSAKA); } - #[test] - fn test_optimism_spec_id_mapping() { - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); - - // Test latest hardforks - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); - assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); - } - #[test] fn test_tempo_spec_id_mapping() { assert_eq!(SpecId::from(TempoHardfork::Genesis), SpecId::OSAKA); @@ -371,25 +376,40 @@ mod tests { } #[test] - fn test_from_chain_and_timestamp_op_mainnet() { - let op_chain_id = 10; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); + fn test_from_chain_and_timestamp_unknown_chain() { + assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); } - #[test] - fn test_from_chain_and_timestamp_base() { - let base_chain_id = 8453; - assert!(matches!( - FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), - Some(FoundryHardfork::Optimism(_)) - )); - } + #[cfg(feature = "optimism")] + mod optimism { + use super::*; - #[test] - fn test_from_chain_and_timestamp_unknown_chain() { - assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None); + #[test] + fn test_optimism_spec_id_mapping() { + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH); + + // Test latest hardforks + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE); + assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Interop), OpSpecId::INTEROP); + } + + #[test] + fn test_from_chain_and_timestamp_op_mainnet() { + let op_chain_id = 10; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } + + #[test] + fn test_from_chain_and_timestamp_base() { + let base_chain_id = 8453; + assert!(matches!( + FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX), + Some(FoundryHardfork::Optimism(_)) + )); + } } } diff --git a/crates/evm/networks/Cargo.toml b/crates/evm/networks/Cargo.toml index a63ed34ba61cf..00c9abf0f90f7 100644 --- a/crates/evm/networks/Cargo.toml +++ b/crates/evm/networks/Cargo.toml @@ -19,7 +19,7 @@ foundry-evm-hardforks.workspace = true alloy-chains.workspace = true alloy-eips.workspace = true alloy-evm.workspace = true -alloy-op-hardforks.workspace = true +alloy-op-hardforks = { workspace = true, optional = true } alloy-primitives = { workspace = true, features = [ "serde", "getrandom", @@ -43,4 +43,8 @@ clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } serde.workspace = true [dev-dependencies] -serde_json.workspace = true \ No newline at end of file +serde_json.workspace = true + +[features] +default = ["optimism"] +optimism = ["dep:alloy-op-hardforks", "foundry-evm-hardforks/optimism"] diff --git a/crates/evm/networks/src/lib.rs b/crates/evm/networks/src/lib.rs index 4672deff353d0..384cee5a7bed3 100644 --- a/crates/evm/networks/src/lib.rs +++ b/crates/evm/networks/src/lib.rs @@ -11,7 +11,6 @@ use alloy_chains::{ }; use alloy_eips::eip1559::BaseFeeParams; use alloy_evm::precompiles::PrecompilesMap; -use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; use alloy_primitives::{Address, ChainId, map::AddressHashMap}; use clap::Parser; use foundry_evm_hardforks::FoundryHardfork; @@ -20,20 +19,52 @@ use std::collections::BTreeMap; pub mod celo; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] +#[cfg(feature = "optimism")] +mod optimism; + +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + clap::ValueEnum, +)] #[serde(rename_all = "lowercase")] #[clap(rename_all = "lowercase")] pub enum NetworkVariant { #[default] Ethereum, + #[cfg(feature = "optimism")] Optimism, Tempo, } +impl std::str::FromStr for NetworkVariant { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "ethereum" => Ok(Self::Ethereum), + #[cfg(feature = "optimism")] + "optimism" => Ok(Self::Optimism), + "tempo" => Ok(Self::Tempo), + _ => Err(format!("unknown network variant: {s}")), + } + } +} + impl NetworkVariant { pub const fn name(&self) -> &'static str { match self { Self::Ethereum => "ethereum", + #[cfg(feature = "optimism")] Self::Optimism => "optimism", Self::Tempo => "tempo", } @@ -50,31 +81,39 @@ impl From for NetworkVariant { fn from(chain_id: ChainId) -> Self { let chain = Chain::from_id(chain_id); if chain.is_tempo() { - Self::Tempo - } else if chain.is_optimism() { - Self::Optimism - } else { - Self::Ethereum + return Self::Tempo; + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::Optimism; } + Self::Ethereum } } -#[derive(Clone, Debug, Default, Parser, Serialize, Deserialize, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)] pub struct NetworkConfigs { /// Enable a specific network family. - #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "optimism", "tempo"])] - #[serde(skip_serializing_if = "Option::is_none")] - network: Option, + #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] + #[serde(default)] + pub(crate) network: Option, /// Enable Celo network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "optimism", "tempo"])] + #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] celo: bool, /// Enable Optimism network features (deprecated: use --network optimism). + #[cfg(feature = "optimism")] #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])] - // Skipped from configs (forge) as there is no feature to be added yet. - #[serde(skip)] - optimism: bool, + // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the + // canonical form is `network = "optimism"`. + #[serde(default)] + pub(crate) optimism: bool, /// Enable Tempo network features (deprecated: use --network tempo). - #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "optimism"])] + #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])] + #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))] + // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the + // canonical form is `network = "tempo"`. #[serde(default)] tempo: bool, /// Whether to bypass prevrandao. @@ -83,11 +122,22 @@ pub struct NetworkConfigs { bypass_prevrandao: bool, } -impl NetworkConfigs { - pub fn with_optimism() -> Self { - Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } +// Custom `Serialize` impl: always emits the *resolved* network as the canonical +// `network = "..."` field, and never emits the legacy `tempo` / `optimism` aliases. This avoids +// confusing output like `network = "tempo"` next to `tempo = false`, and ensures `tempo = true` +// in foundry.toml round-trips as `network = "tempo"`. +impl Serialize for NetworkConfigs { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + let mut s = serializer.serialize_struct("NetworkConfigs", 3)?; + s.serialize_field("network", &self.resolved_network())?; + s.serialize_field("celo", &self.celo)?; + s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?; + s.end() } +} +impl NetworkConfigs { pub fn with_celo() -> Self { Self { celo: true, ..Default::default() } } @@ -96,11 +146,7 @@ impl NetworkConfigs { Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() } } - pub fn is_optimism(&self) -> bool { - matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) - } - - pub fn is_tempo(&self) -> bool { + pub const fn is_tempo(&self) -> bool { matches!(self.resolved_network(), Some(NetworkVariant::Tempo)) } @@ -109,14 +155,18 @@ impl NetworkConfigs { } /// Returns the resolved network variant, folding legacy flags. - fn resolved_network(&self) -> Option { - self.network.or(if self.optimism { - Some(NetworkVariant::Optimism) - } else if self.tempo { - Some(NetworkVariant::Tempo) - } else { - None - }) + const fn resolved_network(&self) -> Option { + if let Some(n) = self.network { + return Some(n); + } + #[cfg(feature = "optimism")] + if self.optimism { + return Some(NetworkVariant::Optimism); + } + if self.tempo { + return Some(NetworkVariant::Tempo); + } + None } /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum. @@ -132,16 +182,12 @@ impl NetworkConfigs { /// For Optimism networks, returns Canyon parameters if the Canyon hardfork is active /// at the given timestamp, otherwise returns pre-Canyon parameters. pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + #[cfg(feature = "optimism")] if self.is_optimism() { - let op_hardforks = OpChainHardforks::op_mainnet(); - if op_hardforks.is_canyon_active_at_timestamp(timestamp) { - BaseFeeParams::optimism_canyon() - } else { - BaseFeeParams::optimism() - } - } else { - BaseFeeParams::ethereum() + return self.op_base_fee_params(timestamp); } + let _ = timestamp; + BaseFeeParams::ethereum() } pub fn bypass_prevrandao(&self, chain_id: u64) -> bool { @@ -156,21 +202,23 @@ impl NetworkConfigs { pub fn with_chain_id(self, chain_id: u64) -> Self { let chain = Chain::from_id(chain_id); - if self.resolved_network().is_none() { - if chain.is_tempo() { - Self::with_tempo() - } else if chain.is_optimism() { - Self::with_optimism() + if self.resolved_network().is_some() { + return if !self.celo + && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) + { + Self::with_celo() } else { self - } - } else if !self.celo - && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) - { - Self::with_celo() - } else { - self + }; + } + if chain.is_tempo() { + return Self::with_tempo(); + } + #[cfg(feature = "optimism")] + if chain.is_optimism() { + return Self::with_optimism(); } + self } /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an @@ -190,6 +238,7 @@ impl NetworkConfigs { let network = match hardfork { FoundryHardfork::Ethereum(_) => self, FoundryHardfork::Tempo(_) => Self::with_tempo(), + #[cfg(feature = "optimism")] FoundryHardfork::Optimism(_) => Self::with_optimism(), }; @@ -225,6 +274,21 @@ impl NetworkConfigs { } } +impl From for NetworkConfigs { + fn from(network: NetworkVariant) -> Self { + match network { + NetworkVariant::Ethereum => Self::default(), + NetworkVariant::Tempo => { + Self { network: Some(network), tempo: true, ..Default::default() } + } + #[cfg(feature = "optimism")] + NetworkVariant::Optimism => { + Self { network: Some(network), optimism: true, ..Default::default() } + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -236,17 +300,6 @@ mod tests { let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() }; let via_old = NetworkConfigs { tempo: true, ..Default::default() }; assert_eq!(via_new.is_tempo(), via_old.is_tempo()); - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.active_network_name(), via_old.active_network_name()); - } - - #[test] - fn new_optimism_flag_equivalent_to_legacy() { - let via_new = - NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; - let via_old = NetworkConfigs { optimism: true, ..Default::default() }; - assert_eq!(via_new.is_optimism(), via_old.is_optimism()); - assert_eq!(via_new.is_tempo(), via_old.is_tempo()); assert_eq!(via_new.active_network_name(), via_old.active_network_name()); } @@ -258,31 +311,11 @@ mod tests { assert_eq!(cfg.active_network_name(), Some("tempo")); } - #[test] - fn active_network_name_optimism() { - let cfg = NetworkConfigs::with_optimism(); - assert_eq!(cfg.active_network_name(), Some("optimism")); - } - #[test] fn active_network_name_default_is_none() { assert_eq!(NetworkConfigs::default().active_network_name(), None); } - // --- new flag takes precedence over legacy flag --- - - #[test] - fn new_flag_wins_over_legacy_when_both_set() { - // --network optimism --tempo: network field wins - let cfg = NetworkConfigs { - network: Some(NetworkVariant::Optimism), - tempo: true, - ..Default::default() - }; - assert!(cfg.is_optimism()); - assert!(!cfg.is_tempo()); - } - // --- Serde round-trip --- #[test] @@ -291,16 +324,6 @@ mod tests { let json = serde_json::to_string(&original).unwrap(); let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); assert!(restored.is_tempo()); - assert!(!restored.is_optimism()); - } - - #[test] - fn serde_roundtrip_optimism() { - let original = NetworkConfigs::with_optimism(); - let json = serde_json::to_string(&original).unwrap(); - let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); - assert!(restored.is_optimism()); - assert!(!restored.is_tempo()); } #[test] @@ -311,13 +334,71 @@ mod tests { assert!(cfg.is_tempo()); } + #[test] + fn serde_serializes_legacy_alias_as_canonical_network() { + // Legacy `tempo = true` should serialize as the canonical `network = "tempo"`, + // and the legacy `tempo` / `optimism` keys must not appear in the output. + let cfg = NetworkConfigs { tempo: true, ..Default::default() }; + let json = serde_json::to_value(cfg).unwrap(); + assert_eq!(json["network"], serde_json::json!("tempo")); + assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized"); + assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized"); + } + #[test] fn serde_new_network_field_deserialized() { let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#; let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap(); assert!(cfg_tempo.is_tempo()); - let json_optimism = r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; - let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); - assert!(cfg_optimism.is_optimism()); + } + + #[cfg(feature = "optimism")] + mod optimism { + use super::*; + + #[test] + fn new_optimism_flag_equivalent_to_legacy() { + let via_new = + NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; + let via_old = NetworkConfigs { optimism: true, ..Default::default() }; + assert_eq!(via_new.is_optimism(), via_old.is_optimism()); + assert_eq!(via_new.is_tempo(), via_old.is_tempo()); + assert_eq!(via_new.active_network_name(), via_old.active_network_name()); + } + + #[test] + fn active_network_name_optimism() { + let cfg = NetworkConfigs::with_optimism(); + assert_eq!(cfg.active_network_name(), Some("optimism")); + } + + #[test] + fn new_flag_wins_over_legacy_when_both_set() { + // --network optimism --tempo: network field wins + let cfg = NetworkConfigs { + network: Some(NetworkVariant::Optimism), + tempo: true, + ..Default::default() + }; + assert!(cfg.is_optimism()); + assert!(!cfg.is_tempo()); + } + + #[test] + fn serde_roundtrip_optimism() { + let original = NetworkConfigs::with_optimism(); + let json = serde_json::to_string(&original).unwrap(); + let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); + assert!(restored.is_optimism()); + assert!(!restored.is_tempo()); + } + + #[test] + fn serde_optimism_field_deserialized() { + let json_optimism = + r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; + let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); + assert!(cfg_optimism.is_optimism()); + } } } diff --git a/crates/evm/networks/src/optimism.rs b/crates/evm/networks/src/optimism.rs new file mode 100644 index 0000000000000..5fffa38a333c7 --- /dev/null +++ b/crates/evm/networks/src/optimism.rs @@ -0,0 +1,25 @@ +//! Optimism-specific extensions for [`NetworkConfigs`] and related helpers. + +use crate::{NetworkConfigs, NetworkVariant}; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; + +impl NetworkConfigs { + pub fn with_optimism() -> Self { + Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } + } + + pub const fn is_optimism(&self) -> bool { + matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) + } + + /// Optimism-specific base fee parameters, picking Canyon vs pre-Canyon based on `timestamp`. + pub(crate) fn op_base_fee_params(&self, timestamp: u64) -> BaseFeeParams { + let op_hardforks = OpChainHardforks::op_mainnet(); + if op_hardforks.is_canyon_active_at_timestamp(timestamp) { + BaseFeeParams::optimism_canyon() + } else { + BaseFeeParams::optimism() + } + } +} diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index 90d2db724cebc..73d64d3ab5d07 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -50,3 +50,7 @@ tempfile.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true yansi.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism", "foundry-evm-core/optimism"] diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml index bad5c577bc69e..b6f11772620f9 100644 --- a/crates/fmt/Cargo.toml +++ b/crates/fmt/Cargo.toml @@ -26,3 +26,7 @@ foundry-test-utils.workspace = true toml.workspace = true snapbox.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index 89a9bf152c8c2..4b986017b71dd 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -711,7 +711,7 @@ impl<'sess> State<'sess, '_> { // Merge the lines and let the wrapper handle breaking if needed let merged_line = format!( "{current_line} {next_content}", - next_content = &next_line[prefix.len()..].trim_start() + next_content = next_line[prefix.len()..].trim_start() ); result.push(merged_line); diff --git a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol index 25074229db558..26809a9418cf0 100644 --- a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol @@ -51,3 +51,9 @@ contract AnotherContract is { } contract WithLayoutAndBase layout at 69 is Base { } + +contract ERC7201Short layout at erc7201("s") is Base { } + +contract ERC7201Mid layout at erc7201("openzeppelin.med") is Base { } + +contract ERC7201OverMax layout at erc7201("openzeppelin.storage.exceeds.max") is Base { } diff --git a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol index 990845d3d1349..9dca04b11e25c 100644 --- a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol @@ -62,3 +62,12 @@ contract AnotherContract is {} contract WithLayoutAndBase layout at 69 is Base {} + +contract ERC7201Short layout at erc7201("s") is Base {} + +contract ERC7201Mid layout at erc7201("openzeppelin.med") is Base {} + +contract ERC7201OverMax layout at erc7201("openzeppelin.storage.exceeds.max") + is + Base +{} diff --git a/crates/fmt/testdata/ContractDefinition/fmt.sol b/crates/fmt/testdata/ContractDefinition/fmt.sol index 93cddcd2c6a20..afe196dbf3da9 100644 --- a/crates/fmt/testdata/ContractDefinition/fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/fmt.sol @@ -57,3 +57,12 @@ contract AnotherContract is {} contract WithLayoutAndBase layout at 69 is Base {} + +contract ERC7201Short layout at erc7201("s") is Base {} + +contract ERC7201Mid layout at erc7201("openzeppelin.med") is Base {} + +contract ERC7201OverMax layout at erc7201("openzeppelin.storage.exceeds.max") + is + Base +{} diff --git a/crates/fmt/testdata/ContractDefinition/original.sol b/crates/fmt/testdata/ContractDefinition/original.sol index c0aa88a7d6d17..ed66f2c5728d2 100644 --- a/crates/fmt/testdata/ContractDefinition/original.sol +++ b/crates/fmt/testdata/ContractDefinition/original.sol @@ -50,3 +50,6 @@ contract AnotherContract is { } contract WithLayoutAndBase layout at 69 is Base {} +contract ERC7201Short layout at erc7201("s") is Base {} +contract ERC7201Mid layout at erc7201("openzeppelin.med") is Base {} +contract ERC7201OverMax layout at erc7201("openzeppelin.storage.exceeds.max") is Base {} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 064834d248d5f..4e83eb168abca 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -62,6 +62,7 @@ alloy-primitives = { workspace = true, features = ["serde"] } alloy-provider = { workspace = true, features = ["reqwest", "ws", "ipc"] } alloy-signer.workspace = true alloy-transport.workspace = true +alloy-hardforks.workspace = true tempo-alloy.workspace = true @@ -117,7 +118,7 @@ tempfile.workspace = true alloy-signer-local.workspace = true [features] -default = ["jemalloc", "asm-keccak"] +default = ["jemalloc", "asm-keccak", "optimism"] asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] @@ -126,3 +127,15 @@ aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cli/optimism", + "forge-script/optimism", + "forge-verify/optimism", + "forge-doc/optimism", + "forge-fmt/optimism", + "forge-lint/optimism", + "forge-sol-macro-gen/optimism", +] diff --git a/crates/forge/assets/tempo/MailTemplate.s.sol b/crates/forge/assets/tempo/MailTemplate.s.sol index 27512efe4d5ec..45006f7cd0e06 100644 --- a/crates/forge/assets/tempo/MailTemplate.s.sol +++ b/crates/forge/assets/tempo/MailTemplate.s.sol @@ -14,7 +14,7 @@ contract MailScript is Script { function run(string memory salt) public { vm.startBroadcast(); - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); ITIP20 token = ITIP20( diff --git a/crates/forge/assets/tempo/MailTemplate.t.sol b/crates/forge/assets/tempo/MailTemplate.t.sol index b1749db5df0bf..19760303860a1 100644 --- a/crates/forge/assets/tempo/MailTemplate.t.sol +++ b/crates/forge/assets/tempo/MailTemplate.t.sol @@ -17,7 +17,7 @@ contract MailTest is Test { address public constant BOB = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); function setUp() public virtual { - address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.ALPHA_USD_ADDRESS); + address feeToken = vm.envOr("TEMPO_FEE_TOKEN", StdTokens.PATH_USD_ADDRESS); StdPrecompiles.TIP_FEE_MANAGER.setUserToken(feeToken); token = ITIP20( diff --git a/crates/forge/src/cmd/coverage.rs b/crates/forge/src/cmd/coverage.rs index ea034bce87185..b8ce2a9b945b1 100644 --- a/crates/forge/src/cmd/coverage.rs +++ b/crates/forge/src/cmd/coverage.rs @@ -87,8 +87,11 @@ impl CoverageArgs { config = self.load_config()?; } - // Set fuzz seed so coverage reports are deterministic - config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so coverage reports are deterministic, + // but allow the user to override it via `--fuzz-seed` or `[fuzz] seed` in config. + if config.fuzz.seed.is_none() { + config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let (paths, mut output) = { let (project, output) = self.build(&config)?; diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 765bb64f95fdd..623723317cdcb 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -13,7 +13,10 @@ use eyre::{Context, ContextCompat, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{LoadConfig, find_contract_artifacts, read_constructor_args_file}, + utils::{ + LoadConfig, ResolvedLane, find_contract_artifacts, maybe_print_resolved_lane, + read_constructor_args_file, resolve_lane, + }, }; use foundry_common::{ FoundryTransactionBuilder, @@ -193,6 +196,12 @@ impl CreateArgs { constructor_args.as_deref().unwrap_or(&self.constructor_args), )? } else { + if !self.constructor_args.is_empty() || self.constructor_args_path.is_some() { + sh_warn!( + "`{}` has no constructor; ignoring provided constructor arguments", + self.contract.name + )?; + } vec![] }; @@ -203,6 +212,11 @@ impl CreateArgs { self.tx.tempo.key_id = Some(ak.key_address); } + // Resolve `--tempo.lane ` against the lanes file (default + // `/tempo.lanes.toml`) and populate `self.tx.tempo.nonce_key` from the lane. + // Must happen before `self.deploy(...)` so `TempoOpts::apply` picks up the nonce_key. + let resolved_lane = resolve_lane(&mut self.tx.tempo, &config.root)?; + // Whether to broadcast the transaction or not let dry_run = !self.broadcast; @@ -223,6 +237,7 @@ impl CreateArgs { dry_run, None, Some(browser), + resolved_lane, ) .await } else if self.unlocked { @@ -239,6 +254,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } else if let Some(ak) = access_key { @@ -259,6 +275,7 @@ impl CreateArgs { dry_run, Some((signer, ak)), None, + resolved_lane, ) .await } else { @@ -282,6 +299,7 @@ impl CreateArgs { dry_run, None, None, + resolved_lane, ) .await } @@ -362,6 +380,7 @@ impl CreateArgs { dry_run: bool, tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>, browser_signer: Option>, + resolved_lane: Option, ) -> Result<()> where N::TransactionRequest: FoundryTransactionBuilder + serde::Serialize, @@ -398,7 +417,7 @@ impl CreateArgs { // If Tempo chain fee token must be set if chain.is_tempo() { - if let Some(fee_token) = self.tx.tempo.fee_token { + if let Some(fee_token) = self.tx.tempo.common.fee_token { deployer.tx.set_fee_token(fee_token); } else { deployer.tx.set_fee_token(DEFAULT_FEE_TOKEN); @@ -408,15 +427,18 @@ impl CreateArgs { // Apply user-provided gas, fee, nonce, and Tempo options. self.tx.apply::(&mut deployer.tx, is_legacy); - // For keychain mode, set key_id and nonce_key before gas estimation. // Convert the CREATE into an AA-compatible call entry since Tempo AA // transactions use a `calls` list instead of `to`+`input`. + if chain.is_tempo() { + deployer.tx.convert_create_to_call(); + } + + // For keychain mode, set key_id and nonce_key before gas estimation. if let Some((_, ref ak)) = tempo_keychain { deployer.tx.set_key_id(ak.key_address); if deployer.tx.nonce_key().is_none() { deployer.tx.set_nonce_key(U256::ZERO); } - deployer.tx.convert_create_to_call(); } // Fetch defaults from provider for values not specified by user. @@ -424,6 +446,20 @@ impl CreateArgs { deployer.tx.set_nonce(provider.get_transaction_count(deployer_address).await?); } + maybe_print_resolved_lane(resolved_lane.as_ref(), deployer.tx.nonce().unwrap_or_default())?; + + if let Some((_, ref ak)) = tempo_keychain { + deployer + .tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } + // set access list if specified if let Some(access_list) = match self.tx.access_list { None => None, @@ -500,6 +536,11 @@ impl CreateArgs { return Ok(()); } + let tempo_sponsor = self.tx.tempo.sponsor_config().await?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut deployer.tx, deployer_address).await?; + } + // Deploy the actual contract let (deployed_contract, receipt) = if let Some(browser) = browser_signer { // Browser wallet signs and sends the transaction diff --git a/crates/forge/src/cmd/snapshot.rs b/crates/forge/src/cmd/snapshot.rs index c8dc2ba72aae1..7c6fb51ce3266 100644 --- a/crates/forge/src/cmd/snapshot.rs +++ b/crates/forge/src/cmd/snapshot.rs @@ -99,8 +99,11 @@ impl GasSnapshotArgs { } pub async fn run(mut self) -> Result<()> { - // Set fuzz seed so gas snapshots are deterministic - self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + // Default to a static fuzz seed so gas snapshots are deterministic, + // but allow the user to override it via `--fuzz-seed`. + if self.test.fuzz_seed.is_none() { + self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED)); + } let outcome = self.test.compile_and_run().await?; outcome.ensure_ok(false)?; diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index da300c429e37e..dd8f3afd56197 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -3,7 +3,7 @@ use crate::{ MultiContractRunner, MultiContractRunnerBuilder, decode::decode_console_logs, gas_report::GasReport, - multi_runner::matches_artifact, + multi_runner::{MultiNetworkConfig, matches_artifact}, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ CallTraceDecoderBuilder, InternalTraceMode, TraceKind, @@ -31,7 +31,7 @@ use foundry_compilers::{ utils::source_files_iter, }; use foundry_config::{ - Config, figment, + Config, InlineConfig, figment, figment::{ Metadata, Profile, Provider, value::{Dict, Map}, @@ -39,10 +39,11 @@ use foundry_config::{ filter::GlobMatcher, }; use foundry_debugger::Debugger; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::evm::{ - BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, SpecFor, TempoEvmNetwork, - TxEnvFor, + BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor, }, opts::EvmOpts, traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth}, @@ -169,6 +170,14 @@ pub struct TestArgs { #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")] pub fuzz_runs: Option, + /// Run only the fuzz case at the given 1-based run index. + #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")] + pub fuzz_run: Option, + + /// Run the fuzz case from the given worker. Requires `--fuzz-run`. + #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")] + pub fuzz_worker: Option, + /// Timeout for each fuzz run in seconds. #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")] pub fuzz_timeout: Option, @@ -301,6 +310,10 @@ impl TestArgs { filter: &ProjectPathsAwareFilter, coverage: bool, ) -> Result { + if config.fuzz.run == Some(0) { + bail!("`fuzz.run` must be greater than 0"); + } + // Explicitly enable isolation for gas reports for more correct gas accounting. if self.gas_report { evm_opts.isolate = true; @@ -342,40 +355,80 @@ impl TestArgs { // Auto-detect network from fork chain ID when not explicitly configured. evm_opts.infer_network_from_fork().await; - // Dispatch based on network type. - let (libraries, mut outcome) = if evm_opts.networks.is_tempo() { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? - } else if evm_opts.networks.is_optimism() { - self.build_and_run_tests::( + // Parse inline config early to detect per-test network annotations. + let inline_config = InlineConfig::new_parsed(output, &config)?; + let override_networks = inline_config.referenced_override_networks(&config.profile); + + let (libraries, mut outcome) = if override_networks.is_empty() { + // Single-pass: no per-test network overrides, use global network setting. + self.dispatch_network( + &evm_opts, config, - evm_opts, + evm_opts.clone(), output, filter, coverage, should_debug, decode_internal, + MultiNetworkConfig::default(), ) .await? } else { - self.build_and_run_tests::( - config, - evm_opts, - output, - filter, - coverage, - should_debug, - decode_internal, - ) - .await? + // Multi-pass: run each distinct network separately and merge results. + let all_override_networks = override_networks.clone(); + let multi_pass_timer = Instant::now(); + + // Default pass: global network, runs tests without an explicit network annotation. + let (libraries, mut outcome) = self + .dispatch_network( + &evm_opts, + config.clone(), + evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: None, + }, + ) + .await?; + + // Override passes: one per annotated network. + for &network in &override_networks { + let mut pass_evm_opts = evm_opts.clone(); + pass_evm_opts.networks = network.into(); + let (_, pass_outcome) = self + .dispatch_network( + &pass_evm_opts, + config.clone(), + pass_evm_opts.clone(), + output, + filter, + coverage, + should_debug, + decode_internal, + MultiNetworkConfig { + all_override_networks: all_override_networks.clone(), + pass_network: Some(network), + }, + ) + .await?; + merge_outcomes(&mut outcome, pass_outcome); + } + + // Print the merged summary (per-pass summaries are suppressed in `run_tests_inner`). + if !self.summary && !shell::is_json() { + sh_println!("{}", outcome.summary(multi_pass_timer.elapsed()))?; + } + if self.summary && !outcome.results.is_empty() { + let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); + sh_println!("{}", &summary_report)?; + } + + (libraries, outcome) }; if should_draw { @@ -461,6 +514,7 @@ impl TestArgs { coverage: bool, should_debug: bool, decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, ) -> eyre::Result<(Libraries, TestOutcome)> { let verbosity = evm_opts.verbosity; let (evm_env, tx_env, fork_block) = @@ -476,6 +530,7 @@ impl TestArgs { .enable_isolation(evm_opts.isolate) .fail_fast(self.fail_fast) .set_coverage(coverage) + .with_multi_network(multi_network) .build::(output, evm_env, tx_env, evm_opts)?; let libraries = runner.libraries.clone(); @@ -483,6 +538,62 @@ impl TestArgs { Ok((libraries, outcome)) } + /// Dispatches `build_and_run_tests` to the correct network type based on `evm_opts.networks`. + #[allow(clippy::too_many_arguments)] + async fn dispatch_network( + &self, + dispatch_opts: &EvmOpts, + config: Config, + evm_opts: EvmOpts, + output: &ProjectCompileOutput, + filter: &ProjectPathsAwareFilter, + coverage: bool, + should_debug: bool, + decode_internal: InternalTraceMode, + multi_network: MultiNetworkConfig, + ) -> eyre::Result<(Libraries, TestOutcome)> { + if dispatch_opts.networks.is_tempo() { + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } else { + #[cfg(feature = "optimism")] + if dispatch_opts.networks.is_optimism() { + return self + .build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await; + } + self.build_and_run_tests::( + config, + evm_opts, + output, + filter, + coverage, + should_debug, + decode_internal, + multi_network, + ) + .await + } + } + /// Run all tests that matches the filter predicate from a test runner async fn run_tests_inner( &self, @@ -586,6 +697,11 @@ impl TestArgs { let libraries = runner.libraries.clone(); + // Capture multi-pass state before moving `runner` into the spawn task. + // In multi-pass mode the per-pass summary is suppressed; the merged summary is + // printed once by the caller after all passes complete. + let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty(); + // Run tests in a streaming fashion. let (tx, rx) = channel::<(String, SuiteResult)>(); let timer = Instant::now(); @@ -643,6 +759,13 @@ impl TestArgs { let tests = &mut suite_result.test_results; let has_tests = !tests.is_empty(); + // In multi-pass (per-test network override) mode, skip suites that contributed no + // tests to this pass so we don't emit a stray blank line in the suite header or + // pollute the outcome with empty entries. + if is_multi_pass && !has_tests && suite_result.warnings.is_empty() { + continue; + } + // Clear the addresses and labels from previous test. decoder.clear_addresses(); @@ -812,9 +935,8 @@ impl TestArgs { // // Exiting early with code 1 if differences are found. if self.gas_snapshot_check.unwrap_or(config.gas_snapshot_check) { - let differences_found = gas_snapshots.clone().into_iter().fold( - false, - |mut found, (group, snapshots)| { + let differences_found = + gas_snapshots.iter().fold(false, |mut found, (group, snapshots)| { // If the snapshot file doesn't exist, we can't compare so we skip. if !&config.snapshots.join(format!("{group}.json")).exists() { return found; @@ -852,8 +974,7 @@ impl TestArgs { } found - }, - ); + }); if differences_found { sh_eprintln!()?; @@ -875,13 +996,13 @@ impl TestArgs { fs::create_dir_all(&config.snapshots)?; // Write gas snapshots to disk per group. - gas_snapshots.clone().into_iter().for_each(|(group, snapshots)| { + for (group, snapshots) in &gas_snapshots { fs::write_pretty_json_file( &config.snapshots.join(format!("{group}.json")), &snapshots, ) .expect("Failed to write gas snapshots to disk"); - }); + } } } @@ -905,17 +1026,17 @@ impl TestArgs { if let Some(gas_report) = gas_report { let finalized = gas_report.finalize(); - sh_println!("{}", &finalized)?; + sh_println!("{finalized}")?; outcome.gas_report = Some(finalized); } - if !self.summary && !shell::is_json() { + if !is_multi_pass && !self.summary && !shell::is_json() { sh_println!("{}", outcome.summary(duration))?; } - if self.summary && !outcome.results.is_empty() { + if !is_multi_pass && self.summary && !outcome.results.is_empty() { let summary_report = TestSummaryReport::new(self.detailed, outcome.clone()); - sh_println!("{}", &summary_report)?; + sh_println!("{summary_report}")?; } // Reattach the task. @@ -982,6 +1103,12 @@ impl Provider for TestArgs { if let Some(fuzz_runs) = self.fuzz_runs { fuzz_dict.insert("runs".to_string(), fuzz_runs.into()); } + if let Some(fuzz_run) = self.fuzz_run { + fuzz_dict.insert("run".to_string(), fuzz_run.into()); + } + if let Some(fuzz_worker) = self.fuzz_worker { + fuzz_dict.insert("worker".to_string(), fuzz_worker.into()); + } if let Some(fuzz_timeout) = self.fuzz_timeout { fuzz_dict.insert("timeout".to_string(), fuzz_timeout.into()); } @@ -1025,6 +1152,29 @@ fn list( Ok(TestOutcome::empty(Some(runner.known_contracts), false)) } +/// Merges `other` into `base` by extending suite results. +/// +/// For suites that appear in both, test results are combined (function-level pass routing ensures +/// each function appears in exactly one pass, so there are no key conflicts in practice). +fn merge_outcomes(base: &mut TestOutcome, other: TestOutcome) { + for (suite_id, other_suite) in other.results { + match base.results.entry(suite_id) { + std::collections::btree_map::Entry::Vacant(e) => { + e.insert(other_suite); + } + std::collections::btree_map::Entry::Occupied(mut e) => { + let base_suite = e.get_mut(); + base_suite.test_results.extend(other_suite.test_results); + base_suite.warnings.extend(other_suite.warnings); + base_suite.duration = base_suite.duration.max(other_suite.duration); + } + } + } + if let Some(decoder) = other.last_run_decoder { + base.last_run_decoder = Some(decoder); + } +} + /// Load persisted filter (with last test run failures) from file. fn last_run_failures(config: &Config) -> Option { match fs::read_to_string(&config.test_failures_file) { @@ -1133,6 +1283,14 @@ mod tests { assert!(args.fuzz_seed.is_some()); } + #[test] + fn fuzz_run() { + let args: TestArgs = + TestArgs::parse_from(["foundry-cli", "--fuzz-run", "10", "--fuzz-worker", "2"]); + assert_eq!(args.fuzz_run, Some(10)); + assert_eq!(args.fuzz_worker, Some(2)); + } + #[test] fn extract_chain() { let test = |arg: &str, expected: Chain| { diff --git a/crates/forge/src/cmd/test/summary.rs b/crates/forge/src/cmd/test/summary.rs index f8a72272af53c..a0123e896d0bf 100644 --- a/crates/forge/src/cmd/test/summary.rs +++ b/crates/forge/src/cmd/test/summary.rs @@ -25,9 +25,9 @@ impl TestSummaryReport { impl Display for TestSummaryReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "{}", self.format_json_output(&self.is_detailed, &self.outcome))?; } else { - writeln!(f, "\n{}", &self.format_table_output(&self.is_detailed, &self.outcome))?; + writeln!(f, "\n{}", self.format_table_output(&self.is_detailed, &self.outcome))?; } Ok(()) } diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index 6c93dc03b28b5..58b11d98874ed 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -146,7 +146,7 @@ impl GasReport { impl Display for GasReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if shell::is_json() { - writeln!(f, "{}", &self.format_json_output())?; + writeln!(f, "{}", self.format_json_output())?; } else { for (name, contract) in &self.contracts { if contract.functions.is_empty() { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 88bbc6156c812..675f0c3e6c99c 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -27,6 +27,7 @@ use foundry_evm::{ opts::EvmOpts, traces::{InternalTraceMode, TraceMode}, }; +use foundry_evm_networks::NetworkVariant; use foundry_linking::{LinkOutput, Linker}; use rayon::prelude::*; @@ -280,6 +281,25 @@ impl MultiContractRunner { } } +/// Tracks network assignment across a multi-network test run. +/// +/// When inline config specifies different networks for different tests, the runner performs one +/// pass per distinct network. This struct encodes which pass we're in so each `ContractRunner` +/// can skip tests that belong to a different pass. +/// +/// Default (empty `all_override_networks`, `None` pass) = single-pass mode, every test runs. +#[derive(Clone, Debug, Default)] +pub struct MultiNetworkConfig { + /// All networks explicitly referenced in inline config annotations across the whole suite. + /// Empty means single-pass mode (no per-test network overrides present). + pub all_override_networks: Vec, + /// The network this pass is responsible for. + /// `None` = default pass: runs tests *without* an explicit network annotation (or annotated + /// with a network not in `all_override_networks`). + /// `Some(v)` = override pass: runs only tests annotated with exactly `v`. + pub pass_network: Option, +} + /// Configuration for the test runner. /// /// This is modified after instantiation through inline config. @@ -311,6 +331,9 @@ pub struct TestRunnerConfig { pub isolation: bool, /// Whether to exit early on test failure or if test run interrupted. pub early_exit: EarlyExit, + + /// Multi-network pass configuration. Default = single-pass mode. + pub multi_network: MultiNetworkConfig, } impl TestRunnerConfig { @@ -423,6 +446,8 @@ pub struct MultiContractRunnerBuilder { pub isolation: bool, /// Whether to exit early on test failure. pub fail_fast: bool, + /// Multi-network pass configuration. + pub multi_network: MultiNetworkConfig, } impl MultiContractRunnerBuilder { @@ -437,6 +462,7 @@ impl MultiContractRunnerBuilder { isolation: Default::default(), decode_internal: Default::default(), fail_fast: false, + multi_network: Default::default(), } } @@ -470,6 +496,11 @@ impl MultiContractRunnerBuilder { self } + pub fn with_multi_network(mut self, multi_network: MultiNetworkConfig) -> Self { + self.multi_network = multi_network; + self + } + pub const fn fail_fast(mut self, fail_fast: bool) -> Self { self.fail_fast = fail_fast; self @@ -594,6 +625,7 @@ impl MultiContractRunnerBuilder { inline_config: Arc::new(InlineConfig::new_parsed(output, &self.config)?), isolation: self.isolation, early_exit: EarlyExit::new(self.fail_fast), + multi_network: self.multi_network, config: self.config, }, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index d924c416759a2..7feaf35254636 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -109,6 +109,25 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { } } + /// Returns `true` if `func` should run in the current multi-network pass. + /// + /// In single-pass mode (no inline network overrides) every function passes. + /// In multi-pass mode: + /// - Default pass (`pass_network = None`): includes functions *without* an override annotation. + /// - Override pass (`pass_network = Some(v)`): includes only functions annotated with `v`. + fn function_matches_network_pass(&self, func: &Function) -> bool { + let multi = &self.mcr.tcfg.multi_network; + if multi.all_override_networks.is_empty() { + return true; + } + let profile = &self.tcfg.config.profile; + let func_network = self.mcr.inline_config.network_for(profile, self.name, &func.name); + match &multi.pass_network { + None => func_network.is_none_or(|n| !multi.all_override_networks.contains(&n)), + Some(target) => func_network.as_ref() == Some(target), + } + } + /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -380,6 +399,7 @@ impl<'a, FEN: FoundryEvmNetwork> ContractRunner<'a, FEN> { .abi .functions() .filter(|func| filter.matches_test_function(func)) + .filter(|func| self.function_matches_network_pass(func)) .collect::>(); debug!( "Found {} test functions out of {} in {:?}", @@ -826,7 +846,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { ); if let Some(ref progress) = progress { - progress.set_prefix(format!("{}\n{warn}\n", &func.name)); + progress.set_prefix(format!("{}\n{warn}\n", func.name)); } else { let _ = sh_warn!("{warn}"); } @@ -1052,7 +1072,7 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { self.cr.name, &func.name, fuzz_config.timeout, - fuzz_config.runs, + if fuzz_config.run.is_some() { 1 } else { fuzz_config.runs }, ); let state = self.build_fuzz_state(false); diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index d3f1503faf569..5d8378c50c9ac 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -916,11 +916,18 @@ Installing tempo-std in [..] (url: https://github.com/tempoxyz/tempo-std, tag: N assert!(prj.root().join("foundry.toml").exists()); - // Verify foundry.toml contains `tempo = true` so subsequent commands auto-detect the network. + // Verify foundry.toml contains `network = "tempo"` so subsequent commands auto-detect the + // network. let foundry_toml = std::fs::read_to_string(prj.root().join("foundry.toml")).unwrap(); assert!( - foundry_toml.contains("tempo = true"), - "foundry.toml should contain `tempo = true`, got:\n{foundry_toml}" + foundry_toml.contains("network = \"tempo\""), + "foundry.toml should contain `network = \"tempo\"`, got:\n{foundry_toml}" + ); + assert!( + foundry_toml.contains("[rpc_endpoints]") + && foundry_toml.contains("tempo = \"https://rpc.tempo.xyz/\"") + && foundry_toml.contains("moderato = \"https://rpc.moderato.tempo.xyz/\""), + "foundry.toml should contain tempo rpc_endpoints, got:\n{foundry_toml}" ); assert!(prj.root().join("lib/forge-std").exists()); @@ -1816,7 +1823,7 @@ forgetest!(gas_report_all_contracts, |prj, cmd| { +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -1830,7 +1837,7 @@ forgetest!(gas_report_all_contracts, |prj, cmd| { +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -1844,7 +1851,7 @@ forgetest!(gas_report_all_contracts, |prj, cmd| { +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -1863,7 +1870,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -1879,7 +1886,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -1895,7 +1902,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -1921,7 +1928,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -1935,7 +1942,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -1949,7 +1956,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -1968,7 +1975,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -1984,7 +1991,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2000,7 +2007,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2026,7 +2033,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2040,7 +2047,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2054,7 +2061,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2073,7 +2080,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2089,7 +2096,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2105,7 +2112,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2134,7 +2141,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2148,7 +2155,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2162,7 +2169,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2181,7 +2188,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2197,7 +2204,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2213,7 +2220,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2246,7 +2253,7 @@ forgetest!(gas_report_some_contracts, |prj, cmd| { +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2265,7 +2272,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2293,7 +2300,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2312,7 +2319,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2340,7 +2347,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2359,7 +2366,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2395,7 +2402,7 @@ forgetest!(gas_report_ignore_some_contracts, |prj, cmd| { +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2409,7 +2416,7 @@ forgetest!(gas_report_ignore_some_contracts, |prj, cmd| { +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2428,7 +2435,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2444,7 +2451,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2476,7 +2483,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2490,7 +2497,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2509,7 +2516,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2525,7 +2532,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2565,7 +2572,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2579,7 +2586,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| -| 133243 | 395 | | | | | +| 133219 | 395 | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------+-----------------+--------+--------+--------+---------| @@ -2593,7 +2600,7 @@ Ran 3 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) +=============================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| -| 133027 | 394 | | | | | +| 133015 | 394 | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------+-----------------+-------+--------+-------+---------| @@ -2622,7 +2629,7 @@ Warning: ContractThree is listed in both 'gas_reports' and 'gas_reports_ignore'. { "contract": "src/Contracts.sol:ContractOne", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2638,7 +2645,7 @@ Warning: ContractThree is listed in both 'gas_reports' and 'gas_reports_ignore'. { "contract": "src/Contracts.sol:ContractThree", "deployment": { - "gas": 133243, + "gas": 133219, "size": 395 }, "functions": { @@ -2654,7 +2661,7 @@ Warning: ContractThree is listed in both 'gas_reports' and 'gas_reports_ignore'. { "contract": "src/Contracts.sol:ContractTwo", "deployment": { - "gas": 133027, + "gas": 133015, "size": 394 }, "functions": { @@ -2993,7 +3000,7 @@ Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] +=====================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 132459 | 396 | | | | | +| 132471 | 396 | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -3016,7 +3023,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/FallbackWithCalldataTest.sol:CounterWithFallback", "deployment": { - "gas": 132459, + "gas": 132471, "size": 396 }, "functions": { @@ -3106,7 +3113,7 @@ contract NestedDeploy is Test { +============================================================================================+ | Deployment Cost | Deployment Size | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| -| 328961 | 1163 | | | | | +| 328949 | 1163 | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| | | | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| @@ -3161,7 +3168,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/NestedDeployTest.sol:Parent", "deployment": { - "gas": 328961, + "gas": 328949, "size": 1163 }, "functions": { @@ -3918,7 +3925,7 @@ forgetest_init!(gas_report_include_tests, |prj, cmd| { +=======================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------+-----------------+-------+--------+-------+---------| -| 156813 | 509 | | | | | +| 156801 | 509 | | | | | |----------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------+-----------------+-------+--------+-------+---------| @@ -3942,7 +3949,7 @@ forgetest_init!(gas_report_include_tests, |prj, cmd| { |-----------------------------------------+-----------------+--------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-----------------------------------------+-----------------+--------+--------+--------+---------| -| setUp | 218902 | 218902 | 218902 | 218902 | 1 | +| setUp | 218890 | 218890 | 218890 | 218890 | 1 | |-----------------------------------------+-----------------+--------+--------+--------+---------| | test_Increment | 51847 | 51847 | 51847 | 51847 | 1 | ╰-----------------------------------------+-----------------+--------+--------+--------+---------╯ @@ -3960,7 +3967,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) | src/Counter.sol:Counter Contract | | | | | | |----------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 156813 | 509 | | | | | +| 156801 | 509 | | | | | | | | | | | | | Function Name | Min | Avg | Median | Max | # Calls | | increment | 43482 | 43482 | 43482 | 43482 | 1 | @@ -3973,7 +3980,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) | 1544498 | 7573 | | | | | | | | | | | | | Function Name | Min | Avg | Median | Max | # Calls | -| setUp | 218902 | 218902 | 218902 | 218902 | 1 | +| setUp | 218890 | 218890 | 218890 | 218890 | 1 | | test_Increment | 51847 | 51847 | 51847 | 51847 | 1 | @@ -3990,7 +3997,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "src/Counter.sol:Counter", "deployment": { - "gas": 156813, + "gas": 156801, "size": 509 }, "functions": { @@ -4026,10 +4033,10 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "functions": { "setUp()": { "calls": 1, - "min": 218902, - "mean": 218902, - "median": 218902, - "max": 218902 + "min": 218890, + "mean": 218890, + "median": 218890, + "max": 218890 }, "test_Increment()": { "calls": 1, diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index dca88ad1c2f63..3eebca475a781 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -112,7 +112,6 @@ create2_deployer = "0x4e59b44847b379578588920ca78fbf26c0b4956c" assertions_revert = true legacy_assertions = false celo = false -tempo = false bypass_prevrandao = false transaction_timeout = 120 additional_compiler_profiles = [] @@ -578,6 +577,32 @@ forgetest_init!(can_get_evm_opts, |prj, _cmd| { } }); +// Regression test for : +// the bare `ETH_RPC_URL` env var must NOT cause `forge` commands to set +// `eth_rpc_url` (which would silently fork all `forge test` runs). +// Only `--rpc-url`, `foundry.toml`, the `FOUNDRY_ETH_RPC_URL` env var, or +// cheatcodes should configure forking. +forgetest_init!(eth_rpc_url_env_does_not_set_fork_url, |prj, _cmd| { + prj.initialize_default_contracts(); + let url = "http://127.0.0.1:8545"; + + let mut cmd = prj.forge_bin(); + cmd.arg("config") + .arg("--root") + .arg(prj.root()) + .arg("--json") + .env("ETH_RPC_URL", url) + // Make sure the figment-style env var is not set in the test environment. + .env_remove("FOUNDRY_ETH_RPC_URL"); + let output = cmd.output().unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let config: Config = serde_json::from_str(stdout.as_ref()).unwrap(); + assert_eq!( + config.eth_rpc_url, None, + "bare ETH_RPC_URL must not propagate to forge config (regression #14538)" + ); +}); + // checks that we can set various config values forgetest_init!(can_set_config_values, |prj, _cmd| { prj.initialize_default_contracts(); @@ -1270,6 +1295,8 @@ forgetest_init!(test_default_config, |prj, cmd| { "show_progress": false, "fuzz": { "runs": 256, + "run": null, + "worker": null, "fail_on_revert": true, "max_test_rejects": 65536, "seed": null, @@ -1437,8 +1464,8 @@ forgetest_init!(test_default_config, |prj, cmd| { "soldeer": null, "assertions_revert": true, "legacy_assertions": false, + "network": null, "celo": false, - "tempo": false, "bypass_prevrandao": false, "transaction_timeout": 120, "additional_compiler_profiles": [], diff --git a/crates/forge/tests/cli/ext_integration.rs b/crates/forge/tests/cli/ext_integration.rs index fbd84739635c5..f792f5b4a7887 100644 --- a/crates/forge/tests/cli/ext_integration.rs +++ b/crates/forge/tests/cli/ext_integration.rs @@ -1,12 +1,12 @@ use foundry_test_utils::util::ExtTester; // Actively maintained tests -// Last updated: June 19th 2025 +// Last updated: April 29th 2026 // #[test] fn forge_std() { - ExtTester::new("foundry-rs", "forge-std", "b69e66b0ff79924d487d49bf7fb47c9ec326acba") + ExtTester::new("foundry-rs", "forge-std", "8987040ede9553cea20c95ad40d0455930f9c8e0") // Skip fork tests. .args(["--nmc", "Fork"]) .verbosity(2) @@ -17,7 +17,7 @@ fn forge_std() { #[test] #[cfg_attr(windows, ignore = "Windows cannot find installed programs")] fn prb_math() { - ExtTester::new("PaulRBerg", "prb-math", "aad73cfc6cdc2c9b660199b5b1e9db391ea48640") + ExtTester::new("PaulRBerg", "prb-math", "82e5ed5561d0a1c43a3a59edbf4291c8de26479e") .install_command(&["bun", "install", "--prefer-offline"]) // Try npm if bun fails / is not installed. .install_command(&["npm", "install", "--prefer-offline"]) @@ -40,7 +40,7 @@ fn prb_proxy() { #[cfg_attr(windows, ignore = "Windows cannot find installed programs")] fn sablier_v2_core() { let mut tester = - ExtTester::new("sablier-labs", "v2-core", "d85521f5615f6c19612ff250ee89c57b9afa6aa2") + ExtTester::new("sablier-labs", "v2-core", "8b6823c019ff7556ac9ad24cbb5ac62821854d2f") // Skip fork tests. .args(["--nmc", "Fork"]) // Increase the gas limit: https://github.com/sablier-labs/v2-core/issues/956 @@ -65,7 +65,7 @@ fn sablier_v2_core() { #[test] fn solady() { let mut tester = - ExtTester::new("Vectorized", "solady", "cbcfe0009477aa329574f17e8db0a05703bb8bdd"); + ExtTester::new("Vectorized", "solady", "90db92ce173856605d24a554969f2c67cadbc7e9"); // This test expects the mover contract created via CREATE2 to be selfdestructed within the // same transaction. In isolation mode, each top-level call runs as a separate transaction @@ -82,7 +82,7 @@ fn solady() { #[cfg_attr(windows, ignore = "Windows cannot find installed programs")] #[cfg(not(feature = "isolate-by-default"))] fn snekmate() { - ExtTester::new("pcaversaccio", "snekmate", "601031d244475b160a00f73053532528bf665cc3") + ExtTester::new("pcaversaccio", "snekmate", "1a54931129f2814cbbd7ddbafb4005707f8a5bf8") .install_command(&["pnpm", "install", "--prefer-offline"]) // Try npm if pnpm fails / is not installed. .install_command(&["npm", "install", "--prefer-offline"]) @@ -92,7 +92,7 @@ fn snekmate() { // #[test] fn mds1_multicall3() { - ExtTester::new("mds1", "multicall", "5f90062160aedb7c807fadca469ac783a0557b57").run(); + ExtTester::new("mds1", "multicall", "b667d67ecfa5361a81e8f110234ce242613b0012").run(); } // Legacy tests diff --git a/crates/forge/tests/cli/failure_assertions.rs b/crates/forge/tests/cli/failure_assertions.rs index 48a17c723b261..77d5a5e84cfbb 100644 --- a/crates/forge/tests/cli/failure_assertions.rs +++ b/crates/forge/tests/cli/failure_assertions.rs @@ -70,8 +70,13 @@ Suite result: FAILED. 0 passed; 7 failed; 0 skipped; [ELAPSED] .stdout_eq( r#"No files changed, compilation skipped ... +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != [..]] testShouldFailExpectRevertNestedCreateInnerAddress() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterNestedCreate() ([GAS]) +[FAIL: Reverter != expected reverter: [..] != 0x000000000000000000000000000000000000dEaD] testShouldFailExpectRevertWrongReverterTopLevelCreate() ([GAS]) [FAIL: next call did not revert as expected] testShouldFailExpectRevertsNotOnImmediateNextCall() ([GAS]) -Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +Suite result: FAILED. 0 passed; 6 failed; 0 skipped; [ELAPSED] ... "#, ); diff --git a/crates/forge/tests/cli/inline_config.rs b/crates/forge/tests/cli/inline_config.rs index 04fb2369d83b0..ba01767d58b26 100644 --- a/crates/forge/tests/cli/inline_config.rs +++ b/crates/forge/tests/cli/inline_config.rs @@ -425,3 +425,107 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); + +// Checks that tests annotated with `forge-config: default.networks.network` run on the correct +// EVM network, and that unannotated tests run on the globally configured network. +// +// Each test makes a real call to the Tempo `TipFeeManager` precompile at +// `0xfeec000000000000000000000000000000000000` (a Tempo-only contract that exists on the +// Moderato testnet and is auto-injected by the in-memory Tempo EVM): +// +// * The default-network test asserts the precompile has no code (it does not exist on Ethereum). +// * The Tempo-network test asserts the precompile has code and `userTokens(address)` returns the +// unset zero-address sentinel, proving the Tempo network was actually selected for that test and +// the Tempo genesis state was loaded. +forgetest!(per_test_network_routing, |prj, cmd| { + prj.add_test( + "inline.sol", + r#" + address constant TIP_FEE_MANAGER = 0xfeEC000000000000000000000000000000000000; + + contract DefaultNetwork { + // No annotation -> runs on the globally selected network (Ethereum by default). + // The Tempo FeeManager precompile must NOT exist here. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + } + + contract TempoNetwork { + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + // Sentinel bytecode (0xef) is injected at every Tempo precompile address. + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + // Call a Tempo-only method: `userTokens(address)` returns the user's preferred + // fee token, or the zero address when none is set. + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + + // Mixed contract: one function annotated with Tempo, one unannotated (runs on Ethereum). + contract MixedNetwork { + // No annotation -> runs on Ethereum; precompile must be absent. + function test_fee_manager_absent_on_ethereum() public view { + require( + TIP_FEE_MANAGER.code.length == 0, + "TipFeeManager should not exist on Ethereum" + ); + } + + /// forge-config: default.networks.network = "tempo" + function test_fee_manager_callable_on_tempo() public view { + require( + TIP_FEE_MANAGER.code.length > 0, + "TipFeeManager must be deployed on Tempo" + ); + + (bool ok, bytes memory ret) = TIP_FEE_MANAGER.staticcall( + abi.encodeWithSignature("userTokens(address)", address(0)) + ); + require(ok, "userTokens call to TipFeeManager failed"); + require(ret.length == 32, "unexpected return data length"); + address token = abi.decode(ret, (address)); + require(token == address(0), "expected unset user fee token"); + } + } + "#, + ); + + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_absent_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test for test/inline.sol:[..]Network +[PASS] test_fee_manager_callable_on_[..]() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 3 test suites [ELAPSED]: 4 tests passed, 0 failed, 0 skipped (4 total tests) + +"#]]); +}); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index fd69907be2f09..8420e24eb3df7 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -1,4 +1,7 @@ -use forge_lint::{linter::Lint, sol::med::REGISTERED_LINTS}; +use forge_lint::{ + linter::Lint, + sol::{self, SolLint}, +}; use foundry_config::{ DenyLevel, LintSeverity, LinterConfig, SolidityErrorCode, lint::LintSpecificConfig, }; @@ -203,7 +206,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -230,7 +233,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -610,7 +613,7 @@ note[mixed-case-function]: function names should use mixedCase 9 │ function functionMIXEDCaseInfo() public {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCaseInfo` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function "#]]); @@ -637,7 +640,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]); @@ -665,7 +668,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect 13 │ uint256 result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift "# @@ -694,7 +697,7 @@ warning[divide-before-multiply]: multiplication should occur before division to 16 │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply "#]]).stdout_eq(str![[r#" @@ -855,7 +858,7 @@ note[unused-import]: unused imports should be removed 8 │ import { _PascalCaseInfo } from "./ContractWithLints.sol"; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import "#]]); @@ -887,7 +890,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase 6 │ uint256 public CounterB_Fail_Lint; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `counterBFailLint` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable "#]]); @@ -992,7 +995,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { ], "children": [ { - "message": "https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic", + "message": "https://getfoundry.sh/forge/linting/unwrapped-modifier-logic", "code": null, "level": "help", "spans": [], @@ -1048,7 +1051,7 @@ forgetest!(lint_json_output_no_ansi_escape_codes, |prj, cmd| { "rendered": null } ], - "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" + "rendered": "note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size\n\nhelp: wrap modifier logic to reduce code size\n 9 + _onlyOwner();\n10 + _;\n11 + }\n12 + \n13 + function _onlyOwner() internal {\n14 + require(isOwner[msg.sender], \"Not owner\");\n15 + require(msg.sender != address(0), \"Zero address\");\n16 + }\n ╭▸ src/UnwrappedModifierTest.sol:8:13\n │\n 8 │ ┏ modifier onlyOwner() {\n 9 │ ┃ require(isOwner[msg.sender], \"Not owner\");\n10 │ ┃ require(msg.sender != address(0), \"Zero address\");\n11 │ ┃ _;\n12 │ ┃ }\n │ ┗━━━━━━━━━━━━━┛\n │\n ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic\n ╭╴\n 8 ± modifier onlyOwner() {\n ╰╴\n" } "#]], ); @@ -1129,46 +1132,46 @@ Warning: Key `deny_warnings` is being deprecated in favor of `deny = warnings`. #[tokio::test] async fn ensure_lint_rule_docs() { - const FOUNDRY_BOOK_LINT_PAGE_URL: &str = "https://book.getfoundry.sh/forge/linting"; - - // Fetch the content of the lint reference - let content = match reqwest::get(FOUNDRY_BOOK_LINT_PAGE_URL).await { - Ok(resp) => { - assert!( - resp.status().is_success(), - "Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}). Status: {status}", - status = resp.status() - ); - match resp.text().await { - Ok(text) => text, - Err(e) => { - panic!("Failed to read response text: {e}"); - } + let client = reqwest::Client::new(); + let mut failures = Vec::new(); + + for lint in registered_lints() { + let url = lint.help(); + let response = match client.get(url).send().await { + Ok(response) => response, + Err(err) => { + failures.push(format!("{} ({url}) could not be fetched: {err}", lint.id())); + continue; } + }; + + if !response.status().is_success() { + failures.push(format!("{} ({url}) returned HTTP {}", lint.id(), response.status())); + continue; } - Err(e) => { - panic!("Failed to fetch Foundry Book lint page ({FOUNDRY_BOOK_LINT_PAGE_URL}): {e}",); - } - }; - // Ensure no missing lints - let mut missing_lints = Vec::new(); - for lint in REGISTERED_LINTS { + let content = match response.text().await { + Ok(content) => content.to_lowercase(), + Err(err) => { + failures + .push(format!("{} ({url}) response body could not be read: {err}", lint.id())); + continue; + } + }; + let selector = lint.id().to_lowercase(); let selector_with_space = selector.replace('-', " "); - if !content.to_lowercase().contains(&selector) - && !content.to_lowercase().contains(&selector_with_space) - { - missing_lints.push(lint.id()); + if !content.contains(&selector) && !content.contains(&selector_with_space) { + failures.push(format!("{} ({url}) did not mention the lint id", lint.id())); } } - if !missing_lints.is_empty() { + if !failures.is_empty() { let mut msg = String::from( - "Foundry Book lint validation failed. The following lints must be added to the docs:\n", + "Foundry Book lint validation failed. The following lint pages are missing or invalid:\n", ); - for lint in missing_lints { - msg.push_str(&format!(" - {lint}\n")); + for failure in failures { + msg.push_str(&format!(" - {failure}\n")); } msg.push_str("Please open a PR: https://github.com/foundry-rs/book"); panic!("{msg}"); @@ -1177,11 +1180,21 @@ async fn ensure_lint_rule_docs() { #[test] fn ensure_no_privileged_lint_id() { - for lint in REGISTERED_LINTS { + for lint in registered_lints() { assert_ne!(lint.id(), "all", "lint-id 'all' is reserved. Please use a different id"); } } +fn registered_lints() -> impl Iterator { + sol::high::REGISTERED_LINTS + .iter() + .chain(sol::med::REGISTERED_LINTS) + .chain(sol::low::REGISTERED_LINTS) + .chain(sol::info::REGISTERED_LINTS) + .chain(sol::gas::REGISTERED_LINTS) + .chain(sol::codesize::REGISTERED_LINTS) +} + // forgetest!(dependency_warnings_do_not_affect_lint_exit_code, |prj, cmd| { // Library with code that triggers a solc warning (unused local variable) @@ -1265,3 +1278,195 @@ contract OldContract { "# ]]); }); + +const PRAGMA_INCONSISTENT_ALPHA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Alpha {} +"#; + +const PRAGMA_INCONSISTENT_BETA: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract Beta {} +"#; + +forgetest!(pragma_inconsistent_cross_file, |prj, cmd| { + prj.add_source("Alpha", PRAGMA_INCONSISTENT_ALPHA); + prj.add_source("Beta", PRAGMA_INCONSISTENT_BETA); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +const PRAGMA_EXACT_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract A {} +"#; + +const PRAGMA_EXACT_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract B {} +"#; + +const PRAGMA_EXACT_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +contract C {} +"#; + +const PRAGMA_CARET_A: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract A {} +"#; + +const PRAGMA_CARET_B: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract B {} +"#; + +const PRAGMA_CARET_C: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract C {} +"#; + +const NO_PRAGMA_C: &str = r#" +// SPDX-License-Identifier: MIT + +contract C {} +"#; + +// Multiple files all using the exact same pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_exact_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_EXACT_C); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Multiple files all using the exact same caret pragma must NOT warn. +forgetest!(pragma_inconsistent_consistent_caret_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + prj.add_source("B", PRAGMA_CARET_B); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// A single file in the project cannot conflict with itself. +forgetest!(pragma_inconsistent_single_file_no_warning, |prj, cmd| { + prj.add_source("A", PRAGMA_CARET_A); + + cmd.arg("lint") + .args(["--only-lint", "pragma-inconsistent"]) + .assert_success() + .stderr_eq(str![[r#""#]]); +}); + +// Even files that share a requirement still emit when ANY other variant exists. +// Two files with `0.8.20` plus one file with `^0.8.20` => 3 emits total. +forgetest!(pragma_inconsistent_duplicates_among_conflict, |prj, cmd| { + prj.add_source("A", PRAGMA_EXACT_A); + prj.add_source("B", PRAGMA_EXACT_B); + prj.add_source("C", PRAGMA_CARET_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); + +// Files without a `pragma solidity` directive must not affect the conflict computation. +// Note: `add_raw_source` is used here to bypass the helper that would otherwise inject a default +// `pragma solidity =;` for files that omit one. +forgetest!(pragma_inconsistent_files_without_pragma, |prj, cmd| { + prj.add_raw_source("A", PRAGMA_EXACT_A); + prj.add_raw_source("B", PRAGMA_CARET_B); + // C has no pragma at all; should be ignored by the cross-file check. + prj.add_raw_source("C", NO_PRAGMA_C); + + cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![ + [r#" +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + [FILE]:3:1 + │ +3 │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + + +"#] + ]); +}); diff --git a/crates/forge/tests/cli/lint/geiger.rs b/crates/forge/tests/cli/lint/geiger.rs index faecfb212fb90..202866e83e35f 100644 --- a/crates/forge/tests/cli/lint/geiger.rs +++ b/crates/forge/tests/cli/lint/geiger.rs @@ -21,7 +21,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ bytes memory stuff = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 1 linter note(s) ... @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 9 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:10:20 @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 10 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations [FILE]:11:20 @@ -100,7 +100,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op 11 │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode Error: aborting due to 3 linter note(s) ... diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 242a0ebb4267f..031d80f0cf071 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3241,7 +3241,7 @@ contract CounterScript is Script { error: the following required arguments were not provided: --broadcast -Usage: [..] script --broadcast --verify --rpc-url [ARGS]... +Usage: [..] script --broadcast --verify --rpc-url [ARGS]... For more information, try '--help'. diff --git a/crates/forge/tests/cli/test_cmd/fuzz.rs b/crates/forge/tests/cli/test_cmd/fuzz.rs index fefefb30d9b15..454b014a6e1bc 100644 --- a/crates/forge/tests/cli/test_cmd/fuzz.rs +++ b/crates/forge/tests/cli/test_cmd/fuzz.rs @@ -1,4 +1,5 @@ use alloy_primitives::U256; +use foundry_evm::fuzz::BaseCounterExample; use foundry_test_utils::{TestCommand, forgetest_init, str}; use regex::Regex; @@ -845,6 +846,8 @@ forgetest_init!(test_fuzz_random_uint_varies_across_runs, |prj, cmd| { prj.add_test( "RandomFuzzTest.t.sol", r#" +pragma solidity >=0.8.0; + import {Test} from "forge-std/Test.sol"; contract RandomFuzzTest is Test { @@ -868,3 +871,145 @@ Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) ... "#]]); }); + +forgetest_init!(test_fuzz_run_replays_random_uint_failure, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + function testFuzz_randomUint_shouldFail(uint256) public { + uint256 rand = vm.randomUint(0, 4); + assertTrue(rand != 0, "hit value 0"); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: hit value 0; counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +"#]]; + + cmd.args(["test", "--fuzz-seed", "1", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + assert_eq!(persisted_failure.fuzz.seed, Some(U256::from(1))); + assert_eq!(persisted_failure.fuzz.worker, Some(0)); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + cmd.forge_fuse() + .args([ + "test", + "--fuzz-seed", + "1", + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + + cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure().stdout_eq(expected_output); +}); + +forgetest_init!(test_fuzz_rerun_replays_random_uint_failure_without_seed, |prj, cmd| { + prj.add_test( + "RandomFuzzTest.t.sol", + r#" +pragma solidity >=0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract RandomFuzzTest is Test { + error Random(uint256 value); + + function testFuzz_randomUint_shouldFail(uint256) public { + revert Random(vm.randomUint()); + } +} + "#, + ); + + let expected_output = str![[r#" +... +Ran 1 test for test/RandomFuzzTest.t.sol:RandomFuzzTest +[FAIL: Random([..]); counterexample: [..]] testFuzz_randomUint_shouldFail(uint256) (runs: [..], [AVG_GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... +Tip: Run `forge test --rerun` to retry only the 1 failed test + +[SEED] (use `--fuzz-seed` to reproduce) + +"#]]; + + let assert = cmd + .args(["test", "--mt", "testFuzz_randomUint_shouldFail", "-j1"]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + let reason = random_failure_reason(&stdout); + + let failure_file = + prj.root().join("cache/fuzz/failures/RandomFuzzTest/testFuzz_randomUint_shouldFail"); + let persisted_failure: BaseCounterExample = + serde_json::from_slice(&std::fs::read(&failure_file).unwrap()).unwrap(); + let fuzz_seed = format!("{:#x}", persisted_failure.fuzz.seed.unwrap()); + let fuzz_run = persisted_failure.fuzz.run.unwrap().to_string(); + let fuzz_worker = persisted_failure.fuzz.worker.unwrap().to_string(); + + let assert = cmd + .forge_fuse() + .args([ + "test", + "--fuzz-seed", + &fuzz_seed, + "--fuzz-run", + &fuzz_run, + "--fuzz-worker", + &fuzz_worker, + "--mt", + "testFuzz_randomUint_shouldFail", + "-j1", + ]) + .assert_failure() + .stdout_eq(expected_output.clone()); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd + .forge_fuse() + .args(["test", "--rerun", "-j1"]) + .assert_failure() + .stdout_eq(expected_output); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); + + let assert = cmd.forge_fuse().args(["test", "--rerun", "-j1"]).assert_failure(); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(random_failure_reason(&stdout), reason, "{stdout}"); +}); + +fn random_failure_reason(stdout: &str) -> String { + Regex::new(r"\[FAIL: (Random\([^)]+\))") + .unwrap() + .captures(stdout) + .unwrap_or_else(|| panic!("{stdout}"))[1] + .to_string() +} diff --git a/crates/forge/tests/cli/test_cmd/invariant/common.rs b/crates/forge/tests/cli/test_cmd/invariant/common.rs index fefd95def0412..107b387bb0ec5 100644 --- a/crates/forge/tests/cli/test_cmd/invariant/common.rs +++ b/crates/forge/tests/cli/test_cmd/invariant/common.rs @@ -2670,7 +2670,7 @@ contract InvariantWarp is Test { [FAIL: max time] [Sequence] (original: 3, shrunk: 1) vm.warp(block.timestamp + 656868); - vm.prank(0x00000000000000000000000000000000000012d2); + vm.prank(0x00000000000000000000000000000000000012d1); Warp(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).increment(); invariant_warp() (runs: 0, calls: 0, reverts: 2) ... diff --git a/crates/forge/tests/cli/test_cmd/invariant/mod.rs b/crates/forge/tests/cli/test_cmd/invariant/mod.rs index 723b5bd789c8b..bbe65f2f2f2fa 100644 --- a/crates/forge/tests/cli/test_cmd/invariant/mod.rs +++ b/crates/forge/tests/cli/test_cmd/invariant/mod.rs @@ -422,9 +422,9 @@ Failing tests: Encountered 1 failing test in test/InvariantSequenceLenTest.t.sol:InvariantSequenceLenTest [FAIL: invariant increment failure] [Sequence] (original: 3, shrunk: 3) - sender=0x00000000000000000000000000000000000014aD addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] + sender=0x0000000000000000000000000000000000001490 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] sender=0x8ef7F804bAd9183981A366EA618d9D47D3124649 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] - sender=0x00000000000000000000000000000000000016Ac addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=setNumber(uint256) args=[284406551521730736391345481857560031052359183671404042152984097777 [2.844e65]] + sender=0x00000000000000000000000000000000000016C5 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=setNumber(uint256) args=[284406551521730736391345481857560031052359183671404042152984097777 [2.844e65]] invariant_increment() (runs: 0, calls: 0, reverts: 0) Encountered a total of 1 failing tests, 0 tests succeeded @@ -448,11 +448,11 @@ Failing tests: Encountered 1 failing test in test/InvariantSequenceLenTest.t.sol:InvariantSequenceLenTest [FAIL: invariant increment failure] [Sequence] (original: 3, shrunk: 3) - vm.prank(0x00000000000000000000000000000000000014aD); + vm.prank(0x0000000000000000000000000000000000001490); Counter(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).increment(); vm.prank(0x8ef7F804bAd9183981A366EA618d9D47D3124649); Counter(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).increment(); - vm.prank(0x00000000000000000000000000000000000016Ac); + vm.prank(0x00000000000000000000000000000000000016C5); Counter(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f).setNumber(284406551521730736391345481857560031052359183671404042152984097777); invariant_increment() (runs: 0, calls: 0, reverts: 0) @@ -476,9 +476,9 @@ Failing tests: Encountered 1 failing test in test/InvariantSequenceLenTest.t.sol:InvariantSequenceLenTest [FAIL: invariant increment failure] [Sequence] (original: 3, shrunk: 3) - sender=0x00000000000000000000000000000000000014aD addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] + sender=0x0000000000000000000000000000000000001490 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] sender=0x8ef7F804bAd9183981A366EA618d9D47D3124649 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=increment() args=[] - sender=0x00000000000000000000000000000000000016Ac addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=setNumber(uint256) args=[284406551521730736391345481857560031052359183671404042152984097777 [2.844e65]] + sender=0x00000000000000000000000000000000000016C5 addr=[src/Counter.sol:Counter]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=setNumber(uint256) args=[284406551521730736391345481857560031052359183671404042152984097777 [2.844e65]] invariant_increment() (runs: 1, calls: 1, reverts: 1) Encountered a total of 1 failing tests, 0 tests succeeded diff --git a/crates/forge/tests/cli/test_cmd/repros.rs b/crates/forge/tests/cli/test_cmd/repros.rs index 32bfe6a98a9fd..3803385b496ab 100644 --- a/crates/forge/tests/cli/test_cmd/repros.rs +++ b/crates/forge/tests/cli/test_cmd/repros.rs @@ -783,6 +783,66 @@ ParserError: Source "Missing.sol" not found: File not found. Searched the follow "#]]); }); +// https://github.com/foundry-rs/foundry/issues/10463 +forgetest_init!(issue_10463, |prj, cmd| { + prj.add_test( + "Issue10463.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract Issue10463Test is Test { + event Foo(); + + error CustomError(uint256 code); + + function revertingBefore(bool shouldRevert) external { + if (shouldRevert) revert(); + emit Foo(); + } + + function revertingWithReason() external pure { + revert("revert reason"); + } + + function revertingWithCustomError() external pure { + revert CustomError(42); + } + + function testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() public { + vm.expectEmit(); + emit Foo(); + + this.revertingBefore(true); + } + + function testExpectEmitPreservesRevertReason() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithReason(); + } + + function testExpectEmitPreservesCustomError() public { + vm.expectEmit(); + emit Foo(); + + this.revertingWithCustomError(); + } +} +"#, + ); + + cmd.arg("test").assert_failure().stdout_eq(str![[r#" +... +Ran 3 tests for test/Issue10463.t.sol:Issue10463Test +[FAIL: CustomError(42)] testExpectEmitPreservesCustomError() ([GAS]) +[FAIL: revert reason] testExpectEmitPreservesRevertReason() ([GAS]) +[FAIL: EvmError: Revert] testExpectEmitPreservesRevertWhenCallRevertsBeforeLog() ([GAS]) +Suite result: FAILED. 0 passed; 3 failed; 0 skipped; [ELAPSED] +... +"#]]); +}); + // https://github.com/foundry-rs/foundry/issues/12803 // Test gas underflow prevention on Cancun (no EIP-7702 gas floor) forgetest_init!(issue_12803_cancun, |prj, cmd| { diff --git a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol index 7e482c6673155..838183a1b0b5e 100644 --- a/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol +++ b/crates/forge/tests/fixtures/ExpectRevertFailures.t.sol @@ -233,6 +233,63 @@ contract ExpectRevertWithReverterFailureTest is DSTest { aContract.doNotRevert(); aContract.callAndRevert(); } + + // + // Regression: must fail because 0xdead is not the actual reverter when a + // top-level CREATE constructor reverts directly. + function testShouldFailExpectRevertWrongReverterTopLevelCreate() public { + vm.expectRevert(address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // even when an exact-bytes pattern is also supplied for a top-level CREATE. + function testShouldFailExpectRevertWithBytesWrongReverterTopLevelCreate() public { + vm.expectRevert(abi.encodePacked("Reverted by DContract"), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail because the reverter address argument is enforced + // for `expectPartialRevert(bytes4, address)` against a top-level CREATE. + function testShouldFailExpectPartialRevertWrongReverterTopLevelCreate() public { + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), address(0xdead)); + new DContract(); + } + + // + // Regression: must fail when the innermost reverting frame is a nested + // CREATE and the reverter address argument does not match the would-be + // deployed address of the failed deployment. + function testShouldFailExpectRevertWrongReverterNestedCreate() public { + vm.expectRevert(address(0xdead)); + new NestedDContractCreator(); + } + + // + // Regression: documents the intended semantics for nested CREATEs — the + // matched reverter is the *outer* would-be-deployed address (the contract + // whose deployment failed), NOT the innermost reverting CREATE's address. + // Supplying the inner address must fail. + function testShouldFailExpectRevertNestedCreateInnerAddress() public { + // Outer = NestedDContractCreator at this contract's next nonce. + // Inner = DContract created from inside the outer constructor (deployer + // is the outer, nonce 1). + address outer = + vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + address inner = vm.computeCreateAddress(outer, 1); + vm.expectRevert(inner); + new NestedDContractCreator(); + } +} + +// Used by `testShouldFailExpectRevertWrongReverterNestedCreate`: a contract whose +// constructor directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } } contract ExpectRevertCountFailureTest is DSTest { diff --git a/crates/lint/Cargo.toml b/crates/lint/Cargo.toml index 87864721432d9..589a0a5069e37 100644 --- a/crates/lint/Cargo.toml +++ b/crates/lint/Cargo.toml @@ -24,3 +24,7 @@ eyre.workspace = true heck.workspace = true rayon.workspace = true thiserror.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/lint/README.md b/crates/lint/README.md index 9e3f394eba832..9d5dad0c0272e 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -11,14 +11,20 @@ It helps enforce best practices and improve code quality within Foundry projects - `incorrect-shift`: Warns against shift operations where operands might be in the wrong order. - `unchecked-call`: Low-level calls should check the success return value. - `erc20-unchecked-transfer`: ERC20 `transfer` and `transferFrom` calls should check the return value. + - `rtlo`: Flags Unicode bidirectional override characters ("Trojan Source", CVE-2021-42574) that can hide malicious code. - **Medium Severity:** + - `boolean-cst`: Flags misuse of boolean constants. - `divide-before-multiply`: Warns against performing division before multiplication in the same expression, which can cause precision loss. - `incorrect-erc20-interface`: Flags ERC20 interfaces and implementations with non-compliant function signatures. - `incorrect-erc721-interface`: Flags ERC721 interfaces and implementations with non-compliant function signatures. + - `tx-origin`: Flags use of `tx.origin` in authorization-like predicates. - `unsafe-typecast`: Typecasts that can truncate values should be checked. - **Low Severity:** - `block-timestamp`: Warns when `block.timestamp` is used in a comparison, as it may be manipulated by validators. + - `missing-zero-check`: Address parameter is used in a state write or value transfer without a zero-address check. - **Informational / Style Guide:** + - `boolean-equal`: Boolean comparisons to constants should be simplified. + - `too-many-digits`: Numeric literals with 5+ consecutive zeros are error-prone. - `pascal-case-struct`: Flags for struct names not adhering to `PascalCase`. - `mixed-case-function`: Flags for function names not adhering to `mixedCase`. - `mixed-case-variable`: Flags for mutable variable names not adhering to `mixedCase`. @@ -28,9 +34,15 @@ It helps enforce best practices and improve code quality within Foundry projects - `unaliased-plain-import`: Use named imports `{A, B}` or alias `import ".." as X`. - `named-struct-fields`: Prefer initializing structs with named fields. - `unsafe-cheatcode`: Usage of unsafe cheatcodes that can perform dangerous operations. + - `multi-contract-file`: Prefer having only one contract, interface, or library per file. + - `interface-file-naming`: Interface file names should be prefixed with `I`. + - `interface-naming`: Interface names should be prefixed with `I`. + - `pragma-inconsistent`: Flags projects whose source files declare different Solidity pragma version requirements. - **Gas Optimizations:** - `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings. + - `could-be-immutable`: Recommends declaring constructor-only state variables as `immutable`. - `custom-errors`: Recommends using custom errors instead of strings and plain reverts for potential gas savings. + - `unused-state-variables`: State variables that are never used should be removed. - **Code Size:** - `unwrapped-modifier-logic`: Recommends wrapping modifier logic to reduce contract code size. diff --git a/crates/lint/docs/README.md b/crates/lint/docs/README.md new file mode 100644 index 0000000000000..5eb4110a92e57 --- /dev/null +++ b/crates/lint/docs/README.md @@ -0,0 +1,52 @@ +# Forge lint documentation + +This directory contains one markdown file per registered `forge-lint` rule. Each file is referenced +by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the +[Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. + +## Adding a new lint + +When you add a new lint with `declare_forge_lint!`, you **must** also add a documentation file at +`crates/lint/docs/.md`. The presence of the file is enforced by the +`registered_lints_have_docs` unit test in [`crates/lint/src/sol/mod.rs`](../src/sol/mod.rs). + +Use [`_template.md`](./_template.md) as a starting point. + +## File structure + +Each lint doc file should follow this structure: + +```markdown +# + +**Severity**: `` +**ID**: `` + +A one-paragraph description of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` + +## Configuration + +Document any inline-config or `foundry.toml` options that affect this lint, if any. +``` diff --git a/crates/lint/docs/_template.md b/crates/lint/docs/_template.md new file mode 100644 index 0000000000000..41c735a0ba579 --- /dev/null +++ b/crates/lint/docs/_template.md @@ -0,0 +1,28 @@ +# + +**Severity**: `` +**ID**: `` + +One-paragraph summary of what this lint detects and why it matters. + +## What it does + +Explain precisely what the lint flags. + +## Why is this bad? + +Explain the impact (security, correctness, gas, readability). + +## Example + +### Bad + +```solidity +// triggering example +``` + +### Good + +```solidity +// non-triggering, recommended example +``` diff --git a/crates/lint/docs/asm-keccak256.md b/crates/lint/docs/asm-keccak256.md new file mode 100644 index 0000000000000..4678cfe9f8d12 --- /dev/null +++ b/crates/lint/docs/asm-keccak256.md @@ -0,0 +1,42 @@ +# Inefficient keccak256 call + +**Severity**: `Gas` +**ID**: `asm-keccak256` + +Flags calls to the high-level `keccak256(...)` builtin that can be cheaply rewritten with inline +assembly. + +## What it does + +Reports `keccak256(arg)` calls and (when possible) emits a fix suggestion that uses inline +assembly to compute the hash directly, avoiding the overhead of the high-level call. + +## Why is this bad? + +The high-level `keccak256` call performs additional memory management and ABI encoding compared +to a direct `keccak256(ptr, len)` opcode invocation. In hot paths the difference is visible in +gas reports. + +## Example + +### Bad + +```solidity +bytes32 h = keccak256(abi.encodePacked(a, b)); +``` + +### Good + +```solidity +bytes32 h; +assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, a) + mstore(add(m, 0x20), b) + h := keccak256(m, 0x40) +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/block-timestamp.md b/crates/lint/docs/block-timestamp.md new file mode 100644 index 0000000000000..a51b55ff5d8cc --- /dev/null +++ b/crates/lint/docs/block-timestamp.md @@ -0,0 +1,44 @@ +# Use of block.timestamp in comparisons + +**Severity**: `Low` +**ID**: `block-timestamp` + +Flags use of `block.timestamp` as an operand of a comparison, where its value can be slightly +manipulated by the block proposer. + +## What it does + +Reports any comparison expression (`<`, `<=`, `>`, `>=`, `==`, `!=`) that directly or +transitively reads `block.timestamp`. + +## Why is this bad? + +Block proposers can adjust `block.timestamp` within a small window (a few seconds). This is +usually harmless, but for short-window logic — auctions ending, randomness, time-locked +withdrawals — a few seconds of manipulation can be enough for an attacker to capture value. + +Using `block.timestamp` for general scheduling (hours/days) is fine; what's risky is fine-grained +timing and treating timestamps as a source of randomness. + +## Example + +### Bad + +```solidity +function settle() external { + require(block.timestamp >= auctionEnd, "auction ongoing"); + // ... +} +``` + +### Good + +```solidity +// Prefer block numbers for tight windows, or accept a clearly large grace period. +require(block.number >= endBlock, "auction ongoing"); +``` + +## Notes + +This lint is intentionally conservative: not every flagged comparison is exploitable. Review +each occurrence in context. diff --git a/crates/lint/docs/boolean-cst.md b/crates/lint/docs/boolean-cst.md new file mode 100644 index 0000000000000..f5c65dfec2789 --- /dev/null +++ b/crates/lint/docs/boolean-cst.md @@ -0,0 +1,37 @@ +# Misuse of a boolean constant + +**Severity**: `Med` +**ID**: `boolean-cst` + +Flags expressions where a boolean constant (`true`/`false`) is used as a control-flow condition +or operand of a boolean operator, which usually indicates dead code or a leftover debug toggle. + +## What it does + +Reports `if (true)`, `if (false)`, `while (true)` outside of intentional infinite loops, and +boolean operators (`&&`, `||`) where one side is a literal `true`/`false`. + +## Why is this bad? + +A literal boolean as a condition makes the surrounding branch dead, hides logic errors, or +preserves a forgotten debug shortcut that bypasses real checks. + +## Example + +### Bad + +```solidity +if (true) { // always taken + doSomething(); +} +require(condition && true, "unreachable"); // 'true' is redundant +``` + +### Good + +```solidity +if (condition) { + doSomething(); +} +require(condition, "..."); +``` diff --git a/crates/lint/docs/boolean-equal.md b/crates/lint/docs/boolean-equal.md new file mode 100644 index 0000000000000..9397003b039b4 --- /dev/null +++ b/crates/lint/docs/boolean-equal.md @@ -0,0 +1,34 @@ +# Boolean comparison to a constant + +**Severity**: `Info` +**ID**: `boolean-equal` + +Flags expressions of the form `x == true`, `x == false`, `x != true`, `x != false`, which can be +simplified. + +## What it does + +Reports any equality comparison between a boolean expression and a literal `true` or `false`. + +## Why is this bad? + +Comparing a boolean to a boolean literal is redundant and harms readability. Use the boolean +expression directly (or its negation). + +## Example + +### Bad + +```solidity +if (paused == true) revert(); +if (paused == false) doSomething(); +require(ok != false, "fail"); +``` + +### Good + +```solidity +if (paused) revert(); +if (!paused) doSomething(); +require(ok, "fail"); +``` diff --git a/crates/lint/docs/could-be-immutable.md b/crates/lint/docs/could-be-immutable.md new file mode 100644 index 0000000000000..bda1de6379955 --- /dev/null +++ b/crates/lint/docs/could-be-immutable.md @@ -0,0 +1,42 @@ +# State variable could be immutable + +**Severity**: `Gas` +**ID**: `could-be-immutable` + +Flags state variables that are assigned only in the constructor and never written to afterward — +making them eligible to be declared `immutable`. + +## What it does + +Reports each non-`constant`, non-`immutable` state variable whose only writes occur in the +constructor (or in initialization at declaration time). + +## Why is this bad? + +`immutable` state variables are stored in the deployed bytecode rather than in storage, eliminating +an `SLOAD` per access and saving substantial gas across the contract's lifetime. Declaring such +variables `immutable` also expresses intent and prevents future writes. + +## Example + +### Bad + +```solidity +contract C { + address owner; + constructor() { owner = msg.sender; } +} +``` + +### Good + +```solidity +contract C { + address immutable OWNER; + constructor() { OWNER = msg.sender; } +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/custom-errors.md b/crates/lint/docs/custom-errors.md new file mode 100644 index 0000000000000..9e01e01d593e9 --- /dev/null +++ b/crates/lint/docs/custom-errors.md @@ -0,0 +1,45 @@ +# Prefer custom errors over revert strings + +**Severity**: `Gas` +**ID**: `custom-errors` + +Flags `require(cond, "message")`, `revert("message")`, and `revert()` calls; suggests replacing +them with a `revert CustomError(...)`. + +## What it does + +Reports `require` calls whose second argument is a string literal, and `revert(...)` calls that +are either bare or have a string-literal argument. + +## Why is this bad? + +Custom errors: +- cost less gas than encoding/decoding a string, +- can carry typed parameters for richer diagnostics, +- shrink contract bytecode (string constants live in code). + +Solidity 0.8.4+ supports custom errors natively. + +## Example + +### Bad + +```solidity +require(amount > 0, "amount must be > 0"); +revert("not authorized"); +revert(); +``` + +### Good + +```solidity +error AmountZero(); +error NotAuthorized(); + +if (amount == 0) revert AmountZero(); +if (!authorized) revert NotAuthorized(); +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/divide-before-multiply.md b/crates/lint/docs/divide-before-multiply.md new file mode 100644 index 0000000000000..f082bef19a1bd --- /dev/null +++ b/crates/lint/docs/divide-before-multiply.md @@ -0,0 +1,32 @@ +# Divide before multiply + +**Severity**: `Med` +**ID**: `divide-before-multiply` + +Flags arithmetic expressions where division is performed before multiplication, which can cause +unintended precision loss in integer arithmetic. + +## What it does + +Warns on expressions of the form `(a / b) * c` (or equivalent shapes), where the integer division +truncates before the result is multiplied. + +## Why is this bad? + +Solidity's integer division truncates toward zero. Performing `(a / b) * c` discards the remainder +of `a / b` before scaling, while `(a * c) / b` preserves precision. This pattern frequently +manifests as fee/share/yield miscalculations. + +## Example + +### Bad + +```solidity +uint256 share = (amount / total) * weight; // truncates first, then scales +``` + +### Good + +```solidity +uint256 share = (amount * weight) / total; // preserves precision +``` diff --git a/crates/lint/docs/erc20-unchecked-transfer.md b/crates/lint/docs/erc20-unchecked-transfer.md new file mode 100644 index 0000000000000..d7d053e020cca --- /dev/null +++ b/crates/lint/docs/erc20-unchecked-transfer.md @@ -0,0 +1,43 @@ +# Unchecked ERC20 transfer return value + +**Severity**: `High` +**ID**: `erc20-unchecked-transfer` + +Flags calls to ERC20 `transfer` and `transferFrom` where the boolean return value is ignored. + +## What it does + +Warns when a function with the same signature as +`transfer(address,uint256)` or `transferFrom(address,address,uint256)` and a `bool` return type is +invoked but the result is not checked. + +## Why is this bad? + +The ERC20 spec allows tokens to signal failure by returning `false` instead of reverting. Ignoring +the return value lets a "failed" transfer go unnoticed, allowing accounting to drift and creating +common DeFi exploits. Use a wrapper such as OpenZeppelin's `SafeERC20` or check the boolean +explicitly. + +## Example + +### Bad + +```solidity +token.transfer(to, amount); +token.transferFrom(from, to, amount); +``` + +### Good + +```solidity +require(token.transfer(to, amount), "transfer failed"); +require(token.transferFrom(from, to, amount), "transferFrom failed"); + +// or use SafeERC20 +SafeERC20.safeTransfer(token, to, amount); +``` + +## Notes + +This lint can produce false positives when the callee does not strictly conform to the ERC20 +interface (e.g. tokens that revert on failure rather than returning `false`). diff --git a/crates/lint/docs/incorrect-erc20-interface.md b/crates/lint/docs/incorrect-erc20-interface.md new file mode 100644 index 0000000000000..65fb8313c205f --- /dev/null +++ b/crates/lint/docs/incorrect-erc20-interface.md @@ -0,0 +1,42 @@ +# Incorrect ERC20 interface + +**Severity**: `Med` +**ID**: `incorrect-erc20-interface` + +Flags interfaces or contracts whose function signatures match an ERC20 method by name and +parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC20 method +(`totalSupply`, `balanceOf`, `transfer`, `transferFrom`, `approve`, `allowance`), the lint checks +that the return type matches the spec. A mismatch is reported. + +## Why is this bad? + +Tokens that diverge from the ERC20 spec break composability with the wider ecosystem (DEXes, +lending protocols, multisigs) and are a common source of integration bugs and exploits. + +## Example + +### Bad + +```solidity +interface IBadERC20 { + function balanceOf(address) external view returns (bool); // should be uint256 + function transfer(address, uint256) external; // should return bool +} +``` + +### Good + +```solidity +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-erc721-interface.md b/crates/lint/docs/incorrect-erc721-interface.md new file mode 100644 index 0000000000000..4803afdde7cc1 --- /dev/null +++ b/crates/lint/docs/incorrect-erc721-interface.md @@ -0,0 +1,48 @@ +# Incorrect ERC721 interface + +**Severity**: `Med` +**ID**: `incorrect-erc721-interface` + +Flags interfaces or contracts whose function signatures match an ERC721 (or ERC165) method by +name and parameters but use the wrong return type. + +## What it does + +For each function whose name and parameter types match a canonical ERC721/ERC165 method +(`balanceOf`, `ownerOf`, `safeTransferFrom`, `transferFrom`, `approve`, `setApprovalForAll`, +`getApproved`, `isApprovedForAll`, `supportsInterface`), the lint checks that the return type +matches the spec. A mismatch is reported. + +## Why is this bad? + +Non-conforming NFT contracts break marketplaces, indexers, and any protocol that relies on the +ERC721 spec. A wrong return type often compiles and deploys silently but causes integration +failures at runtime. + +## Example + +### Bad + +```solidity +interface IBadERC721 { + function balanceOf(address) external view returns (bool); // should be uint256 + function ownerOf(uint256) external view returns (bool); // should be address + function supportsInterface(bytes4) external view returns (uint256); // should be bool +} +``` + +### Good + +```solidity +interface IERC721 { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` diff --git a/crates/lint/docs/incorrect-shift.md b/crates/lint/docs/incorrect-shift.md new file mode 100644 index 0000000000000..a9a70c7f93128 --- /dev/null +++ b/crates/lint/docs/incorrect-shift.md @@ -0,0 +1,37 @@ +# Incorrect shift order + +**Severity**: `High` +**ID**: `incorrect-shift` + +Flags shift operations where a literal appears on the left and a non-literal on the right, which +is almost always the wrong operand order. + +## What it does + +Warns when the left-hand operand of `<<` or `>>` is a numeric literal and the right-hand operand +is a non-literal expression (e.g. a variable, function call, or composite expression). + +## Why is this bad? + +Shift expressions like `2 << x` are usually a typo for `x << 2`. In the former, the *value being +shifted* is a tiny constant and the *shift amount* is dynamic — almost never the intended +behavior, and a known source of bugs in production contracts. + +## Example + +### Bad + +```solidity +result = 2 << stateValue; // shift amount comes from state +result = 8 >> localValue; // shift amount comes from a local +result = 16 << (stateValue + 1); // shift amount is a dynamic expression +``` + +### Good + +```solidity +result = stateValue << 2; +result = localValue >> 3; +result = stateValue << localShiftAmount; +result = 1 << 8; // both literals — fine +``` diff --git a/crates/lint/docs/inline-assembly.md b/crates/lint/docs/inline-assembly.md new file mode 100644 index 0000000000000..bba61148b84c5 --- /dev/null +++ b/crates/lint/docs/inline-assembly.md @@ -0,0 +1,69 @@ +# Inline assembly + +**Severity**: `Info` +**ID**: `inline-assembly` + +Flags every `assembly { ... }` block. Inline assembly bypasses many of Solidity's safety +features (type checks, overflow checks, memory layout invariants) and is a common source of +high-impact bugs, so each occurrence should be reviewed deliberately. + +## What it does + +Reports every inline assembly statement, including blocks declared with the `"evmasm"` dialect +and/or the `("memory-safe")` flag. Blocks declared as memory-safe — either via the modern +`("memory-safe")` flag or the legacy `/// @solidity memory-safe-assembly` NatSpec marker — are +still reported, but with a softer message acknowledging the developer attestation: review +focuses on business logic and side effects rather than memory layout. + +## Why is this bad? + +Assembly skips Solidity's compile-time checks and many of its runtime guarantees. Mistakes +inside an `assembly` block can corrupt memory, break the free memory pointer, leak storage, +escalate privileges via `delegatecall`, or destroy the contract via `selfdestruct`. Even when +required for gas or features unavailable in high-level Solidity, assembly should be small, +documented, and reviewed. + +## When inline assembly is reasonable + +Some idioms are widely used and generally safe: + +- Reading transaction/chain context: `chainid()`, `gas()`, `returndatasize()`. +- Probing code: `codesize()`, `extcodesize(addr)`, `extcodehash(addr)`. +- Reading the free memory pointer: `mload(0x40)`. +- Cheap hashing of a known memory layout, when paired with `("memory-safe")`. + +If you must use assembly: + +1. Keep the block minimal and well-commented. +2. Add the `("memory-safe")` flag when the block does not violate Solidity's memory model, so + the optimizer (and reviewers) can rely on it. The legacy + `/// @solidity memory-safe-assembly` NatSpec marker on the line directly above the block is + also recognized for compatibility with older codebases. +3. Suppress the lint locally to mark the block as audited: + ```solidity + // forge-lint: disable-next-line(inline-assembly) + assembly ("memory-safe") { /* reviewed: ... */ } + ``` + +## Example + +### Bad + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory) { + assembly { + let ok := call(gas(), target, 0, add(data.offset, 0), data.length, 0, 0) + // ... + } +} +``` + +### Good + +```solidity +function rawCall(address target, bytes calldata data) external returns (bytes memory result) { + bool ok; + (ok, result) = target.call(data); + require(ok, "call failed"); +} +``` diff --git a/crates/lint/docs/interface-file-naming.md b/crates/lint/docs/interface-file-naming.md new file mode 100644 index 0000000000000..ff72a0c175e8e --- /dev/null +++ b/crates/lint/docs/interface-file-naming.md @@ -0,0 +1,31 @@ +# Interface file naming + +**Severity**: `Info` +**ID**: `interface-file-naming` + +Flags Solidity files whose only top-level declaration is an interface but whose filename is not +prefixed with `I`. + +## What it does + +Reports interface-only files whose path basename does not start with `I` (e.g. `IERC20.sol`). + +## Why is this bad? + +Prefixing interface filenames with `I` is the prevailing convention in the Solidity ecosystem. +Following it makes import paths predictable and lets reviewers tell at a glance whether they are +looking at an interface or an implementation. + +## Example + +### Bad + +```text +contracts/Token.sol // file contains only `interface Token { ... }` +``` + +### Good + +```text +contracts/IToken.sol // file contains only `interface IToken { ... }` +``` diff --git a/crates/lint/docs/interface-naming.md b/crates/lint/docs/interface-naming.md new file mode 100644 index 0000000000000..5c6b12b946091 --- /dev/null +++ b/crates/lint/docs/interface-naming.md @@ -0,0 +1,31 @@ +# Interface name should be prefixed with 'I' + +**Severity**: `Info` +**ID**: `interface-naming` + +Flags `interface` declarations whose names are not prefixed with `I`. + +## What it does + +Reports `interface Foo` where `Foo` does not start with `I` (e.g. `IFoo`). + +## Why is this bad? + +Prefixing interfaces with `I` is the prevailing convention in Solidity codebases (`IERC20`, +`IERC721`, `IUniswapV2Pair`, ...). Following it makes the role of each type unambiguous at use +sites and aligns with the matching +[`interface-file-naming`](https://getfoundry.sh/forge/linting/interface-file-naming) lint. + +## Example + +### Bad + +```solidity +interface ERC20 { /* ... */ } +``` + +### Good + +```solidity +interface IERC20 { /* ... */ } +``` diff --git a/crates/lint/docs/missing-zero-check.md b/crates/lint/docs/missing-zero-check.md new file mode 100644 index 0000000000000..7eab1f3a00117 --- /dev/null +++ b/crates/lint/docs/missing-zero-check.md @@ -0,0 +1,39 @@ +# Missing zero-address check + +**Severity**: `Low` +**ID**: `missing-zero-check` + +Flags entry-point functions and constructors where an `address` parameter flows into a state write +or value transfer without a zero-address guard. + +## What it does + +Performs a taint analysis from each `address` parameter of an externally callable, state-mutating +function (or constructor) and reports a parameter that reaches a sink (state write, `transfer`, +`call{value: ...}`, etc.) without first being compared against `address(0)` in an `if`/`require`/ +`assert` predicate. + +## Why is this bad? + +Forgetting a zero-address check is a common source of value loss: tokens become permanently +unrecoverable, ownership is renounced unintentionally, or upgrades are bricked. Adding an explicit +guard is cheap and removes an entire class of operational mistakes. + +## Example + +### Bad + +```solidity +function setOwner(address newOwner) external onlyOwner { + owner = newOwner; // no zero-address check +} +``` + +### Good + +```solidity +function setOwner(address newOwner) external onlyOwner { + require(newOwner != address(0), "zero address"); + owner = newOwner; +} +``` diff --git a/crates/lint/docs/mixed-case-function.md b/crates/lint/docs/mixed-case-function.md new file mode 100644 index 0000000000000..9997dcb5691c7 --- /dev/null +++ b/crates/lint/docs/mixed-case-function.md @@ -0,0 +1,32 @@ +# Function names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-function` + +Flags function names that do not follow `mixedCase`. + +## What it does + +Reports functions whose names contain underscores, start with an uppercase letter, or otherwise +deviate from `mixedCase`. Test functions starting with `test`, `invariant_`, or `statefulFuzz` +and user-defined patterns (e.g. `ERC20`) are exempted. + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for function names. Consistent style makes call +sites uniform, helps editor tooling, and reduces friction in code review. + +## Example + +### Bad + +```solidity +function get_balance() external view returns (uint256); +function GetBalance() external view returns (uint256); +``` + +### Good + +```solidity +function getBalance() external view returns (uint256); +``` diff --git a/crates/lint/docs/mixed-case-variable.md b/crates/lint/docs/mixed-case-variable.md new file mode 100644 index 0000000000000..3341e1a0c48ad --- /dev/null +++ b/crates/lint/docs/mixed-case-variable.md @@ -0,0 +1,36 @@ +# Mutable variable names should use mixedCase + +**Severity**: `Info` +**ID**: `mixed-case-variable` + +Flags mutable variable names (locals, parameters, mutable state) that do not follow `mixedCase`. + +## What it does + +Reports mutable variable identifiers that contain underscores, start with an uppercase letter, +or otherwise deviate from `mixedCase`. + +`constant` and `immutable` state variables are not flagged by this lint — see +[`screaming-snake-case-const`](https://getfoundry.sh/forge/linting/screaming-snake-case-const) and +[`screaming-snake-case-immutable`](https://getfoundry.sh/forge/linting/screaming-snake-case-immutable). + +## Why is this bad? + +The Solidity style guide recommends `mixedCase` for mutable variables. Consistent style makes +code easier to scan and review. + +## Example + +### Bad + +```solidity +uint256 public total_supply; +address Owner; +``` + +### Good + +```solidity +uint256 public totalSupply; +address owner; +``` diff --git a/crates/lint/docs/multi-contract-file.md b/crates/lint/docs/multi-contract-file.md new file mode 100644 index 0000000000000..beabc827e4ea6 --- /dev/null +++ b/crates/lint/docs/multi-contract-file.md @@ -0,0 +1,37 @@ +# Multiple contracts in one file + +**Severity**: `Info` +**ID**: `multi-contract-file` + +Flags source files that declare more than one top-level contract, interface, or library. + +## What it does + +Reports each top-level `contract`, `interface`, or `library` definition (after the first) in a +file that contains more than one such declaration. + +## Why is this bad? + +Keeping one contract per file improves discoverability (`grep`, IDE jump-to-file), simplifies +import paths, and avoids unintentional bytecode bloat from artifacts that bundle unrelated +contracts. + +## Example + +### Bad + +```solidity +// File: Token.sol +contract TokenA { /* ... */ } +contract TokenB { /* ... */ } +``` + +### Good + +```solidity +// File: TokenA.sol +contract TokenA { /* ... */ } + +// File: TokenB.sol +contract TokenB { /* ... */ } +``` diff --git a/crates/lint/docs/named-struct-fields.md b/crates/lint/docs/named-struct-fields.md new file mode 100644 index 0000000000000..45713e2555ddc --- /dev/null +++ b/crates/lint/docs/named-struct-fields.md @@ -0,0 +1,31 @@ +# Prefer named struct fields + +**Severity**: `Info` +**ID**: `named-struct-fields` + +Flags struct construction expressions that pass fields positionally instead of by name. + +## What it does + +Reports `Struct(a, b, c)` style struct construction; suggests `Struct({ field1: a, field2: b, +field3: c })` instead. + +## Why is this bad? + +Positional struct construction is fragile: adding or reordering fields silently changes the +meaning of every existing call site. Named-field construction is self-documenting and resilient +to struct changes. + +## Example + +### Bad + +```solidity +User memory u = User(addr, 100, true); +``` + +### Good + +```solidity +User memory u = User({ wallet: addr, balance: 100, active: true }); +``` diff --git a/crates/lint/docs/pascal-case-struct.md b/crates/lint/docs/pascal-case-struct.md new file mode 100644 index 0000000000000..02a243bd56bf4 --- /dev/null +++ b/crates/lint/docs/pascal-case-struct.md @@ -0,0 +1,31 @@ +# Struct names should use PascalCase + +**Severity**: `Info` +**ID**: `pascal-case-struct` + +Flags struct definitions whose names do not follow `PascalCase`. + +## What it does + +Reports any `struct` whose identifier does not match the `PascalCase` convention. + +## Why is this bad? + +The Solidity style guide recommends `PascalCase` for type-like names (contracts, structs, +enums, libraries). Consistent casing makes code easier to scan and integrates with editor +features and external tooling. + +## Example + +### Bad + +```solidity +struct user_info { uint256 balance; } +struct USERINFO { uint256 balance; } +``` + +### Good + +```solidity +struct UserInfo { uint256 balance; } +``` diff --git a/crates/lint/docs/pragma-inconsistent.md b/crates/lint/docs/pragma-inconsistent.md new file mode 100644 index 0000000000000..095f45783773d --- /dev/null +++ b/crates/lint/docs/pragma-inconsistent.md @@ -0,0 +1,41 @@ +# Inconsistent pragma directives + +**Severity**: `Info` +**ID**: `pragma-inconsistent` + +Flags projects whose source files declare incompatible or differently-shaped Solidity version +pragmas. + +## What it does + +Inspects every `pragma solidity ...;` directive across all input source files and reports when +their version requirements are inconsistent (different exact versions, mixed caret/tilde/range +shapes, etc.). + +## Why is this bad? + +A project compiled under multiple Solidity versions can subtly change behavior between files +(e.g. checked arithmetic, default visibility, ABI encoding). Aligning pragmas across the project +removes a hidden source of integration bugs and makes upgrades coordinated. + +## Example + +### Bad + +```solidity +// A.sol +pragma solidity 0.8.18; + +// B.sol +pragma solidity ^0.8.20; + +// C.sol +pragma solidity >=0.7.0 <0.9.0; +``` + +### Good + +```solidity +// All files +pragma solidity 0.8.20; +``` diff --git a/crates/lint/docs/rtlo.md b/crates/lint/docs/rtlo.md new file mode 100644 index 0000000000000..58ce648752c6f --- /dev/null +++ b/crates/lint/docs/rtlo.md @@ -0,0 +1,32 @@ +# Right-to-left override character + +**Severity**: `High` +**ID**: `rtlo` + +Flags the presence of Unicode bidirectional override characters in source code, which can be used +to hide malicious behavior ("Trojan Source", [CVE-2021-42574](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574)). + +## What it does + +Detects the right-to-left override codepoint (`U+202E`) and other bidirectional control characters +embedded in identifiers, strings, and comments. + +## Why is this bad? + +These characters render source code in a different visual order than how the compiler reads it, +allowing an attacker to make malicious code look benign on review. Solidity contracts are public +and frequently audited visually; this attack vector must not be ignored. + +## Example + +### Bad + +```solidity +// transfer(victim‮, attacker)/* // U+202E hidden between args +``` + +### Good + +```solidity +// Avoid bidirectional override characters in code and comments. +``` diff --git a/crates/lint/docs/screaming-snake-case-const.md b/crates/lint/docs/screaming-snake-case-const.md new file mode 100644 index 0000000000000..72a16c5875fae --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-const.md @@ -0,0 +1,30 @@ +# Constants should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-const` + +Flags `constant` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `constant` whose identifier deviates from `SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for constants so they stand out from +mutable state and immutables at call sites. + +## Example + +### Bad + +```solidity +uint256 constant maxSupply = 1_000_000; +uint256 constant Max_Supply = 1_000_000; +``` + +### Good + +```solidity +uint256 constant MAX_SUPPLY = 1_000_000; +``` diff --git a/crates/lint/docs/screaming-snake-case-immutable.md b/crates/lint/docs/screaming-snake-case-immutable.md new file mode 100644 index 0000000000000..cee5590e16d27 --- /dev/null +++ b/crates/lint/docs/screaming-snake-case-immutable.md @@ -0,0 +1,31 @@ +# Immutables should use SCREAMING_SNAKE_CASE + +**Severity**: `Info` +**ID**: `screaming-snake-case-immutable` + +Flags `immutable` state variables whose names do not follow `SCREAMING_SNAKE_CASE`. + +## What it does + +Reports state variables declared `immutable` whose identifier deviates from +`SCREAMING_SNAKE_CASE`. + +## Why is this bad? + +The Solidity style guide recommends `SCREAMING_SNAKE_CASE` for `immutable` variables so they +visually align with `constant` ones and stand out from mutable state at call sites. + +## Example + +### Bad + +```solidity +address immutable owner; +address immutable Owner; +``` + +### Good + +```solidity +address immutable OWNER; +``` diff --git a/crates/lint/docs/too-many-digits.md b/crates/lint/docs/too-many-digits.md new file mode 100644 index 0000000000000..5decb67bec9c3 --- /dev/null +++ b/crates/lint/docs/too-many-digits.md @@ -0,0 +1,32 @@ +# Numeric literal with too many digits + +**Severity**: `Info` +**ID**: `too-many-digits` + +Flags numeric literals containing five or more consecutive zeros, which are easy to misread. + +## What it does + +Reports decimal numeric literals that contain a run of 5 or more `0` characters. + +## Why is this bad? + +Long sequences of zeros are difficult to count visually, and an off-by-one zero is a common bug +(e.g. funding `1_000_000` instead of `10_000_000`). Use scientific notation, sub-denominations, or +underscore separators to make the magnitude obvious. + +## Example + +### Bad + +```solidity +uint256 amount = 1000000000000000000; +``` + +### Good + +```solidity +uint256 amount = 1e18; +uint256 amount2 = 1 ether; +uint256 amount3 = 1_000_000_000_000_000_000; +``` diff --git a/crates/lint/docs/tx-origin.md b/crates/lint/docs/tx-origin.md new file mode 100644 index 0000000000000..26877cf9c0116 --- /dev/null +++ b/crates/lint/docs/tx-origin.md @@ -0,0 +1,34 @@ +# Use of tx.origin for authorization + +**Severity**: `Med` +**ID**: `tx-origin` + +Flags use of `tx.origin` inside authorization-like predicates such as `require`, `assert`, `if`, +`while`, and `for` conditions. + +## What it does + +Reports `tx.origin` reads when they are used as part of a guard condition. Plain reads outside of +guard predicates are not reported. + +## Why is this bad? + +`tx.origin` is the original externally owned account that started the whole transaction, not the +immediate caller. If authorization checks rely on `tx.origin`, a malicious contract can call the +protected contract while the legitimate owner is the transaction origin. + +Use `msg.sender` for authorization checks instead. + +## Example + +### Bad + +```solidity +require(tx.origin == owner, "not owner"); +``` + +### Good + +```solidity +require(msg.sender == owner, "not owner"); +``` diff --git a/crates/lint/docs/unaliased-plain-import.md b/crates/lint/docs/unaliased-plain-import.md new file mode 100644 index 0000000000000..be8c5120028d6 --- /dev/null +++ b/crates/lint/docs/unaliased-plain-import.md @@ -0,0 +1,34 @@ +# Unaliased plain import + +**Severity**: `Info` +**ID**: `unaliased-plain-import` + +Flags `import "path";` statements that pull in every top-level symbol from another file without +an alias. + +## What it does + +Reports plain imports of the form `import "path";`. Suggests using either named imports +(`import { A, B } from "path"`) or an aliased import (`import "path" as X`). + +## Why is this bad? + +Plain imports pollute the importing file's namespace and make the source of each symbol +non-obvious. Named or aliased imports make the dependency surface explicit and reduce the chance +of accidental name collisions. + +## Example + +### Bad + +```solidity +import "./Lib.sol"; +``` + +### Good + +```solidity +import { Foo, Bar } from "./Lib.sol"; +// or +import "./Lib.sol" as Lib; +``` diff --git a/crates/lint/docs/unchecked-call.md b/crates/lint/docs/unchecked-call.md new file mode 100644 index 0000000000000..9a0a4143a0e0e --- /dev/null +++ b/crates/lint/docs/unchecked-call.md @@ -0,0 +1,34 @@ +# Unchecked low-level call + +**Severity**: `High` +**ID**: `unchecked-call` + +Flags low-level calls (`call`, `delegatecall`, `staticcall`, `callcode`) whose `success` return +value is ignored. + +## What it does + +Warns when the boolean returned by a low-level call is discarded — either because the return value +is not assigned or because only the `bytes memory` payload is used. + +## Why is this bad? + +Low-level calls do **not** revert when the callee fails; they silently return `false`. Ignoring +the success flag means a failed call is indistinguishable from a successful one, leading to bugs +where state is updated on the assumption that an external interaction succeeded. + +## Example + +### Bad + +```solidity +target.call(data); // success ignored +(, bytes memory ret) = target.call(data); // only payload kept +``` + +### Good + +```solidity +(bool ok, ) = target.call(data); +require(ok, "call failed"); +``` diff --git a/crates/lint/docs/unsafe-cheatcode.md b/crates/lint/docs/unsafe-cheatcode.md new file mode 100644 index 0000000000000..0aef657b0b7be --- /dev/null +++ b/crates/lint/docs/unsafe-cheatcode.md @@ -0,0 +1,35 @@ +# Usage of unsafe cheatcodes + +**Severity**: `Info` +**ID**: `unsafe-cheatcode` + +Flags use of Foundry cheatcodes that perform dangerous side effects (filesystem access, network +activity, environment variable reads, etc.) so they cannot slip into production code unnoticed. + +## What it does + +Reports calls to cheatcodes whose effects extend beyond the EVM sandbox or that bypass typical +test invariants. The flagged set follows the cheatcode's +[`Safety::Unsafe`](https://book.getfoundry.sh/cheatcodes) classification. + +## Why is this bad? + +Unsafe cheatcodes can read/write files, hit the network, or fork external state. They are +appropriate in tests with explicit intent but should not be added without review, and must +never end up in shipped contract code. + +## Example + +### Bad + +```solidity +vm.writeFile("./out.txt", data); // unsafe — writes to host filesystem +vm.envString("PRIVATE_KEY"); // unsafe — reads host environment +``` + +### Good + +```solidity +// Use safe cheatcodes (vm.expectRevert, vm.prank, vm.warp, ...) and explicit +// inputs/fixtures instead of pulling state from the host environment. +``` diff --git a/crates/lint/docs/unsafe-typecast.md b/crates/lint/docs/unsafe-typecast.md new file mode 100644 index 0000000000000..89d493eec3c3f --- /dev/null +++ b/crates/lint/docs/unsafe-typecast.md @@ -0,0 +1,40 @@ +# Unsafe typecast + +**Severity**: `Med` +**ID**: `unsafe-typecast` + +Flags explicit numeric typecasts that can silently truncate or alter the value. + +## What it does + +Reports casts where the source value's type is wider than the target type +(e.g. `uint256 → uint128`, `int256 → uint128`), unless the cast is preceded by a check that +guarantees the value fits in the target. + +## Why is this bad? + +Solidity does **not** revert on narrowing casts; it silently keeps the lowest bits, which can +cause severe accounting bugs (e.g. amount overflows, wrong fees, broken invariants). Use a checked +cast helper such as OpenZeppelin's `SafeCast` whenever the source value is not provably bounded. + +## Example + +### Bad + +```solidity +function setAmount(uint256 amount) external { + smallAmount = uint128(amount); // silent truncation if amount >= 2**128 +} +``` + +### Good + +```solidity +function setAmount(uint256 amount) external { + require(amount <= type(uint128).max, "overflow"); + smallAmount = uint128(amount); +} + +// or +smallAmount = SafeCast.toUint128(amount); +``` diff --git a/crates/lint/docs/unused-import.md b/crates/lint/docs/unused-import.md new file mode 100644 index 0000000000000..08f2545a36587 --- /dev/null +++ b/crates/lint/docs/unused-import.md @@ -0,0 +1,40 @@ +# Unused import + +**Severity**: `Info` +**ID**: `unused-import` + +Flags imported symbols (or whole import statements) whose imported names are not referenced +anywhere in the source unit. + +## What it does + +Reports `import "..."`, `import "..." as X`, and `import { A, B } from "..."` statements where one +or more imported names are never used. Symbols brought in via `import * as X` are tracked through +`X.member` accesses. + +## Why is this bad? + +Unused imports add noise, slow down compilation, can cause name collisions, and frequently +indicate dead code or stale refactors. + +## Example + +### Bad + +```solidity +import { A, B } from "./Lib.sol"; // B is never used + +contract C { + A internal a; +} +``` + +### Good + +```solidity +import { A } from "./Lib.sol"; + +contract C { + A internal a; +} +``` diff --git a/crates/lint/docs/unused-state-variables.md b/crates/lint/docs/unused-state-variables.md new file mode 100644 index 0000000000000..758c6e58b911b --- /dev/null +++ b/crates/lint/docs/unused-state-variables.md @@ -0,0 +1,39 @@ +# Unused state variable + +**Severity**: `Gas` +**ID**: `unused-state-variables` + +Flags state variables that are declared but never read or written anywhere in the contract or its +descendants. + +## What it does + +Reports each state variable that has no read or write site across the project. + +## Why is this bad? + +Unused state variables waste storage slots, inflate deployment cost, and are a strong signal of +dead or stale code that should be removed. + +## Example + +### Bad + +```solidity +contract C { + uint256 unused; // never read or written + uint256 public total; // used elsewhere +} +``` + +### Good + +```solidity +contract C { + uint256 public total; +} +``` + +## Notes + +This is a `Gas`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/docs/unwrapped-modifier-logic.md b/crates/lint/docs/unwrapped-modifier-logic.md new file mode 100644 index 0000000000000..985c79962af07 --- /dev/null +++ b/crates/lint/docs/unwrapped-modifier-logic.md @@ -0,0 +1,51 @@ +# Unwrapped modifier logic + +**Severity**: `CodeSize` +**ID**: `unwrapped-modifier-logic` + +Flags modifiers whose body contains non-trivial logic that should be moved into a helper function +to reduce contract code size. + +## What it does + +Reports modifiers whose body contains statements other than a single placeholder, simple builtin +calls (`require`/`assert`), or a single library function call. Modifiers that use inline assembly +are exempted. + +## Why is this bad? + +Solidity inlines a modifier's body at every call site, so any non-trivial logic is duplicated +across all functions that use the modifier. Wrapping the logic in an internal function and calling +it from the modifier keeps the bytecode small while preserving behavior. + +## Example + +### Bad + +```solidity +modifier onlyAuth() { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; + _; +} +``` + +### Good + +```solidity +modifier onlyAuth() { + _checkAuth(); + _; +} + +function _checkAuth() internal { + if (!auth[msg.sender]) revert NotAuth(); + bytes32 nonce = keccak256(abi.encodePacked(msg.sender, block.number)); + seenNonce[nonce] = true; +} +``` + +## Notes + +This is a `CodeSize`-severity lint and is **not** applied to test or script files. diff --git a/crates/lint/src/linter/late.rs b/crates/lint/src/linter/late.rs index f7e97d00e7612..ffdc284cacc56 100644 --- a/crates/lint/src/linter/late.rs +++ b/crates/lint/src/linter/late.rs @@ -385,6 +385,7 @@ mod tests { false, LinterConfig { inline: &inline, lint_specific: &lint_specific }, Vec::new(), + None, ); let mut passes: Vec>> = vec![Box::new(RecordingPass { counts: counts.clone() })]; diff --git a/crates/lint/src/linter/mod.rs b/crates/lint/src/linter/mod.rs index fc140643eb68a..3e38a02726605 100644 --- a/crates/lint/src/linter/mod.rs +++ b/crates/lint/src/linter/mod.rs @@ -1,8 +1,10 @@ mod early; mod late; +mod project; pub use early::{EarlyLintPass, EarlyLintVisitor}; pub use late::{LateLintPass, LateLintVisitor}; +pub use project::{ProjectLintEmitter, ProjectLintPass, ProjectSource}; use foundry_common::comments::inline_config::InlineConfig; use foundry_compilers::Language; @@ -16,10 +18,11 @@ use solar::{ diagnostics::{ Applicability, DiagBuilder, DiagId, DiagMsg, MultiSpan, Style, SuggestionStyle, }, + source_map::SourceFile, }, sema::Compiler, }; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source /// code files. @@ -54,6 +57,8 @@ pub struct LintContext<'s, 'c> { with_json_emitter: bool, pub config: LinterConfig<'c>, active_lints: Vec<&'static str>, + /// The source file currently being linted, when known. + source_file: Option>, } pub struct LinterConfig<'s> { @@ -68,8 +73,9 @@ impl<'s, 'c> LintContext<'s, 'c> { with_json_emitter: bool, config: LinterConfig<'c>, active_lints: Vec<&'static str>, + source_file: Option>, ) -> Self { - Self { sess, with_description, with_json_emitter, config, active_lints } + Self { sess, with_description, with_json_emitter, config, active_lints, source_file } } fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> { @@ -81,6 +87,11 @@ impl<'s, 'c> LintContext<'s, 'c> { self.sess } + /// Returns the source file currently being linted, if any. + pub const fn source_file(&self) -> Option<&Arc> { + self.source_file.as_ref() + } + // Helper method to check if a lint id is enabled. // // For performance reasons, some passes check several lints at once. Thus, this method is @@ -108,6 +119,25 @@ impl<'s, 'c> LintContext<'s, 'c> { diag.emit(); } + /// Emit a diagnostic with a caller-provided message instead of the lint's description. + /// + /// Useful when the message must vary per occurrence (e.g. embedding the offending + /// codepoint detected by the `rtlo` lint). + pub fn emit_with_msg(&self, lint: &'static L, span: Span, msg: impl Into) { + if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) { + return; + } + + let diag: DiagBuilder<'_, ()> = self + .sess + .dcx + .diag(lint.severity().into(), msg.into()) + .code(DiagId::new_str(lint.id())) + .span(MultiSpan::from_span(span)); + + self.add_help(diag, lint.help()).emit(); + } + /// Emit a diagnostic with a code suggestion. /// /// If no span is provided for [`SuggestionKind::Fix`], it will use the lint's span. diff --git a/crates/lint/src/linter/project.rs b/crates/lint/src/linter/project.rs new file mode 100644 index 0000000000000..38fc1ad1ba59f --- /dev/null +++ b/crates/lint/src/linter/project.rs @@ -0,0 +1,92 @@ +use super::{Lint, LintContext, LinterConfig}; +use foundry_common::comments::inline_config::InlineConfig; +use foundry_config::lint::LintSpecificConfig; +use solar::{ + ast, + interface::{Session, Span, diagnostics::DiagMsg, source_map::SourceFile}, +}; +use std::{path::PathBuf, sync::Arc}; + +/// A single source unit visible to a project-wide lint pass, pre-loaded with its inline config so +/// emits respect `// forge-lint: disable-*` markers without rebuilding it per emit. +pub struct ProjectSource<'ast> { + pub path: PathBuf, + pub file: Arc, + pub ast: &'ast ast::SourceUnit<'ast>, + pub inline_config: InlineConfig>, +} + +/// Trait for lints that need to inspect every input source at once (e.g. cross-file checks). +/// +/// `check_project` runs once after all per-file [`super::EarlyLintPass`] / +/// [`super::LateLintPass`] passes have completed. +pub trait ProjectLintPass<'ast>: Send + Sync { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]); +} + +/// Helper passed to [`ProjectLintPass::check_project`] for emitting diagnostics against a specific +/// source. +pub struct ProjectLintEmitter<'s, 'c> { + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, +} + +impl<'s, 'c> ProjectLintEmitter<'s, 'c> { + pub const fn new( + sess: &'s Session, + with_description: bool, + with_json_emitter: bool, + lint_specific: &'c LintSpecificConfig, + active_lints: Vec<&'static str>, + ) -> Self { + Self { sess, with_description, with_json_emitter, lint_specific, active_lints } + } + + /// Returns `true` if the given lint id is enabled for this run. Project passes that perform + /// expensive analysis should guard their work behind this check. + pub fn is_lint_enabled(&self, id: &'static str) -> bool { + self.active_lints.contains(&id) + } + + /// Emits a diagnostic with the lint's default description as the message. + pub fn emit<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + ) where + 'c: 'a, + { + self.build_ctx(source).emit(lint, span); + } + + /// Emits a diagnostic with a caller-provided message. + pub fn emit_with_msg<'a, 'ast, L: Lint>( + &'a self, + source: &'a ProjectSource<'ast>, + lint: &'static L, + span: Span, + msg: impl Into, + ) where + 'c: 'a, + { + self.build_ctx(source).emit_with_msg(lint, span, msg); + } + + fn build_ctx<'a, 'ast>(&'a self, source: &'a ProjectSource<'ast>) -> LintContext<'s, 'a> + where + 'c: 'a, + { + LintContext::new( + self.sess, + self.with_description, + self.with_json_emitter, + LinterConfig { inline: &source.inline_config, lint_specific: self.lint_specific }, + self.active_lints.clone(), + Some(source.file.clone()), + ) + } +} diff --git a/crates/lint/src/sol/gas/immutable.rs b/crates/lint/src/sol/gas/immutable.rs new file mode 100644 index 0000000000000..5baba86996841 --- /dev/null +++ b/crates/lint/src/sol/gas/immutable.rs @@ -0,0 +1,406 @@ +use super::CouldBeImmutable; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{self, UnOpKind}, + interface::{kw, sym}, + sema::hir::{self, ExprKind, Res, StmtKind, TypeKind}, +}; +use std::collections::HashSet; + +declare_forge_lint!( + COULD_BE_IMMUTABLE, + Severity::Gas, + "could-be-immutable", + "state variable could be declared immutable" +); + +impl<'hir> LateLintPass<'hir> for CouldBeImmutable { + fn check_nested_contract( + &mut self, + ctx: &LintContext, + hir: &'hir hir::Hir<'hir>, + contract_id: hir::ContractId, + ) { + let contract = hir.contract(contract_id); + if contract.kind == ast::ContractKind::Interface { + return; + } + if !is_most_derived_contract(hir, contract_id) { + return; + } + + let candidates: Vec<_> = contract + .linearized_bases + .iter() + .flat_map(|&contract_id| hir.contract(contract_id).variables()) + .filter(|&id| is_immutable_candidate_type(hir.variable(id))) + .collect(); + + if candidates.is_empty() { + return; + } + let candidate_set: HashSet<_> = candidates.iter().copied().collect(); + + if contract_contains_unlowered_stmt(hir, contract) { + return; + } + + let mut constructor_writes = HashSet::new(); + let mut runtime_writes = HashSet::new(); + + for &var_id in &candidates { + let var = hir.variable(var_id); + if var.initializer.is_some_and(|expr| !is_compile_time_constant(hir, expr)) { + constructor_writes.insert(var_id); + } + } + + for &contract_id in contract.linearized_bases { + for function_id in hir.contract(contract_id).all_functions() { + let function = hir.function(function_id); + if function.is_constructor() { + collect_modifier_writes( + hir, + function, + &candidate_set, + &mut constructor_writes, + &mut runtime_writes, + &mut HashSet::new(), + ); + + if let Some(body) = function.body { + collect_state_writes(hir, body, &candidate_set, &mut constructor_writes); + } + } else { + // Immutable variables can only be assigned inline or directly in constructor + // bodies, so writes hidden behind internal helpers are not valid candidates. + let mut modifier_argument_writes = HashSet::new(); + collect_modifier_writes( + hir, + function, + &candidate_set, + &mut modifier_argument_writes, + &mut runtime_writes, + &mut HashSet::new(), + ); + runtime_writes.extend(modifier_argument_writes); + + if let Some(body) = function.body { + collect_state_writes(hir, body, &candidate_set, &mut runtime_writes); + } + } + } + } + + for &var_id in &candidates { + if constructor_writes.contains(&var_id) && !runtime_writes.contains(&var_id) { + let var = hir.variable(var_id); + ctx.emit(&COULD_BE_IMMUTABLE, var.name.map_or(var.span, |name| name.span)); + } + } + } +} + +fn is_most_derived_contract(hir: &hir::Hir<'_>, contract_id: hir::ContractId) -> bool { + !hir.contracts() + .any(|contract| contract.linearized_bases.iter().skip(1).any(|&id| id == contract_id)) +} + +fn collect_modifier_writes<'hir>( + hir: &'hir hir::Hir<'hir>, + function: &'hir hir::Function<'hir>, + candidates: &HashSet, + argument_writes: &mut HashSet, + body_writes: &mut HashSet, + visited_modifiers: &mut HashSet, +) { + for modifier in function.modifiers { + for expr in modifier.args.exprs() { + collect_expr_writes(expr, candidates, argument_writes); + } + + let Some(modifier_id) = modifier.id.as_function() else { continue }; + if !visited_modifiers.insert(modifier_id) { + continue; + } + + let modifier = hir.function(modifier_id); + let mut nested_argument_writes = HashSet::new(); + collect_modifier_writes( + hir, + modifier, + candidates, + &mut nested_argument_writes, + body_writes, + visited_modifiers, + ); + body_writes.extend(nested_argument_writes); + if let Some(body) = modifier.body { + collect_state_writes(hir, body, candidates, body_writes); + } + } +} + +fn is_immutable_candidate_type(var: &hir::Variable<'_>) -> bool { + var.is_state_variable() + && var.mutability.is_none() + && match var.ty.kind { + TypeKind::Elementary(ty) => ty.is_value_type(), + TypeKind::Custom(hir::ItemId::Contract(_)) => true, + _ => false, + } +} + +fn contract_contains_unlowered_stmt<'hir>( + hir: &'hir hir::Hir<'hir>, + contract: &'hir hir::Contract<'hir>, +) -> bool { + contract.linearized_bases.iter().any(|&contract_id| { + hir.contract(contract_id).all_functions().any(|function_id| { + hir.function(function_id).body.is_some_and(|body| block_contains_unlowered_stmt(body)) + }) + }) +} + +fn block_contains_unlowered_stmt(block: hir::Block<'_>) -> bool { + block.stmts.iter().any(stmt_contains_unlowered_stmt) +} + +fn stmt_contains_unlowered_stmt(stmt: &hir::Stmt<'_>) -> bool { + match &stmt.kind { + StmtKind::Err(_) => true, + StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => { + block_contains_unlowered_stmt(*block) + } + StmtKind::If(_, then_stmt, else_stmt) => { + stmt_contains_unlowered_stmt(then_stmt) + || else_stmt.is_some_and(stmt_contains_unlowered_stmt) + } + StmtKind::Try(stmt_try) => { + stmt_try.clauses.iter().any(|clause| block_contains_unlowered_stmt(clause.block)) + } + StmtKind::DeclSingle(_) + | StmtKind::DeclMulti(_, _) + | StmtKind::Emit(_) + | StmtKind::Revert(_) + | StmtKind::Return(_) + | StmtKind::Break + | StmtKind::Continue + | StmtKind::Expr(_) + | StmtKind::Placeholder => false, + } +} + +fn collect_state_writes<'hir>( + hir: &'hir hir::Hir<'hir>, + block: hir::Block<'hir>, + candidates: &HashSet, + writes: &mut HashSet, +) { + for stmt in block.stmts { + collect_stmt_writes(hir, stmt, candidates, writes); + } +} + +fn collect_stmt_writes<'hir>( + hir: &'hir hir::Hir<'hir>, + stmt: &'hir hir::Stmt<'hir>, + candidates: &HashSet, + writes: &mut HashSet, +) { + match &stmt.kind { + StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => { + collect_state_writes(hir, *block, candidates, writes); + } + StmtKind::If(condition, then_stmt, else_stmt) => { + collect_expr_writes(condition, candidates, writes); + collect_stmt_writes(hir, then_stmt, candidates, writes); + if let Some(else_stmt) = else_stmt { + collect_stmt_writes(hir, else_stmt, candidates, writes); + } + } + StmtKind::Try(stmt_try) => { + collect_expr_writes(&stmt_try.expr, candidates, writes); + for clause in stmt_try.clauses { + collect_state_writes(hir, clause.block, candidates, writes); + } + } + StmtKind::DeclSingle(var_id) => { + if let Some(initializer) = hir.variable(*var_id).initializer { + collect_expr_writes(initializer, candidates, writes); + } + } + StmtKind::DeclMulti(_, expr) + | StmtKind::Emit(expr) + | StmtKind::Revert(expr) + | StmtKind::Return(Some(expr)) + | StmtKind::Expr(expr) => collect_expr_writes(expr, candidates, writes), + StmtKind::Return(None) + | StmtKind::Break + | StmtKind::Continue + | StmtKind::Placeholder + | StmtKind::Err(_) => {} + } +} + +fn collect_expr_writes<'hir>( + expr: &'hir hir::Expr<'hir>, + candidates: &HashSet, + writes: &mut HashSet, +) { + match &expr.kind { + ExprKind::Assign(lhs, _, rhs) => { + collect_lvalue_writes(lhs, candidates, writes); + collect_expr_writes(lhs, candidates, writes); + collect_expr_writes(rhs, candidates, writes); + } + ExprKind::Delete(inner) => { + collect_lvalue_writes(inner, candidates, writes); + collect_expr_writes(inner, candidates, writes); + } + ExprKind::Unary(op, inner) => { + if op.kind.has_side_effects() { + collect_lvalue_writes(inner, candidates, writes); + } + collect_expr_writes(inner, candidates, writes); + } + ExprKind::Array(exprs) => { + for expr in *exprs { + collect_expr_writes(expr, candidates, writes); + } + } + ExprKind::Binary(lhs, _, rhs) => { + collect_expr_writes(lhs, candidates, writes); + collect_expr_writes(rhs, candidates, writes); + } + ExprKind::Call(callee, args, named_args) => { + collect_expr_writes(callee, candidates, writes); + for expr in args.exprs() { + collect_expr_writes(expr, candidates, writes); + } + if let Some(named_args) = named_args { + for arg in *named_args { + collect_expr_writes(&arg.value, candidates, writes); + } + } + } + ExprKind::Index(base, index) => { + collect_expr_writes(base, candidates, writes); + if let Some(index) = index { + collect_expr_writes(index, candidates, writes); + } + } + ExprKind::Slice(base, start, end) => { + collect_expr_writes(base, candidates, writes); + if let Some(start) = start { + collect_expr_writes(start, candidates, writes); + } + if let Some(end) = end { + collect_expr_writes(end, candidates, writes); + } + } + ExprKind::Member(base, _) | ExprKind::Payable(base) => { + collect_expr_writes(base, candidates, writes); + } + ExprKind::Ternary(condition, then_expr, else_expr) => { + collect_expr_writes(condition, candidates, writes); + collect_expr_writes(then_expr, candidates, writes); + collect_expr_writes(else_expr, candidates, writes); + } + ExprKind::Tuple(exprs) => { + for expr in exprs.iter().flatten() { + collect_expr_writes(expr, candidates, writes); + } + } + ExprKind::Ident(_) + | ExprKind::Lit(_) + | ExprKind::New(_) + | ExprKind::TypeCall(_) + | ExprKind::Type(_) + | ExprKind::Err(_) => {} + } +} + +fn collect_lvalue_writes( + expr: &hir::Expr<'_>, + candidates: &HashSet, + writes: &mut HashSet, +) { + match &expr.peel_parens().kind { + ExprKind::Ident([Res::Item(hir::ItemId::Variable(id)), ..]) if candidates.contains(id) => { + writes.insert(*id); + } + ExprKind::Tuple(exprs) => { + for expr in exprs.iter().flatten() { + collect_lvalue_writes(expr, candidates, writes); + } + } + ExprKind::Index(base, _) + | ExprKind::Slice(base, _, _) + | ExprKind::Member(base, _) + | ExprKind::Payable(base) => collect_lvalue_writes(base, candidates, writes), + _ => {} + } +} + +fn is_compile_time_constant(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool { + match &expr.kind { + ExprKind::Lit(_) | ExprKind::Type(_) | ExprKind::TypeCall(_) => true, + ExprKind::Ident(resolutions) => resolutions.iter().all(|res| match res { + Res::Item(hir::ItemId::Variable(var_id)) => hir.variable(*var_id).is_constant(), + _ => false, + }), + ExprKind::Unary(op, inner) => { + !matches!( + op.kind, + UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec + ) && is_compile_time_constant(hir, inner) + } + ExprKind::Binary(lhs, _, rhs) => { + is_compile_time_constant(hir, lhs) && is_compile_time_constant(hir, rhs) + } + ExprKind::Call(callee, args, named_args) => { + is_allowed_constant_call(callee) + && args.exprs().all(|expr| is_compile_time_constant(hir, expr)) + && named_args.is_none_or(|args| { + args.iter().all(|arg| is_compile_time_constant(hir, &arg.value)) + }) + } + ExprKind::Ternary(condition, then_expr, else_expr) => { + is_compile_time_constant(hir, condition) + && is_compile_time_constant(hir, then_expr) + && is_compile_time_constant(hir, else_expr) + } + ExprKind::Tuple(exprs) => { + exprs.iter().flatten().all(|expr| is_compile_time_constant(hir, expr)) + } + ExprKind::Array(_) + | ExprKind::Assign(_, _, _) + | ExprKind::Delete(_) + | ExprKind::Index(_, _) + | ExprKind::Slice(_, _, _) + | ExprKind::Member(_, _) + | ExprKind::New(_) + | ExprKind::Payable(_) + | ExprKind::Err(_) => false, + } +} + +fn is_allowed_constant_call(callee: &hir::Expr<'_>) -> bool { + match &callee.kind { + ExprKind::Type(_) => true, + ExprKind::Ident([Res::Builtin(builtin), ..]) => { + let name = builtin.name(); + name == kw::Keccak256 + || name == kw::Addmod + || name == kw::Mulmod + || name == sym::sha256 + || name == sym::ripemd160 + || name == sym::ecrecover + } + _ => false, + } +} diff --git a/crates/lint/src/sol/gas/mod.rs b/crates/lint/src/sol/gas/mod.rs index 9a4c37f925cd5..2d5ce2a1becc1 100644 --- a/crates/lint/src/sol/gas/mod.rs +++ b/crates/lint/src/sol/gas/mod.rs @@ -1,8 +1,17 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint}; mod custom_errors; +mod immutable; mod keccak; +mod unused_state_variables; use custom_errors::CUSTOM_ERRORS; +use immutable::COULD_BE_IMMUTABLE; use keccak::ASM_KECCAK256; +use unused_state_variables::UNUSED_STATE_VARIABLES; -register_lints!((CustomErrors, early, (CUSTOM_ERRORS)), (AsmKeccak256, late, (ASM_KECCAK256))); +register_lints!( + (AsmKeccak256, late, (ASM_KECCAK256)), + (CustomErrors, early, (CUSTOM_ERRORS)), + (CouldBeImmutable, late, (COULD_BE_IMMUTABLE)), + (UnusedStateVariables, late, (UNUSED_STATE_VARIABLES)), +); diff --git a/crates/lint/src/sol/gas/unused_state_variables.rs b/crates/lint/src/sol/gas/unused_state_variables.rs new file mode 100644 index 0000000000000..78d32c196b20c --- /dev/null +++ b/crates/lint/src/sol/gas/unused_state_variables.rs @@ -0,0 +1,90 @@ +use super::UnusedStateVariables; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::ContractKind, + interface::data_structures::Never, + sema::hir::{self, Visit as _}, +}; +use std::{collections::HashSet, ops::ControlFlow}; + +declare_forge_lint!( + UNUSED_STATE_VARIABLES, + Severity::Gas, + "unused-state-variables", + "state variable is never used" +); + +impl<'hir> LateLintPass<'hir> for UnusedStateVariables { + fn check_contract( + &mut self, + ctx: &LintContext, + hir: &'hir hir::Hir<'hir>, + contract: &'hir hir::Contract<'hir>, + ) { + // Skip interfaces, they cannot have mutable state variables. + if contract.kind == ContractKind::Interface { + return; + } + + // Collect state variable IDs, skipping constants and immutables + // (those are handled by the compiler and don't occupy storage slots). + let state_vars: Vec = contract + .variables() + .filter(|&var_id| { + let var = hir.variable(var_id); + !var.is_constant() && !var.is_immutable() + }) + .collect(); + + if state_vars.is_empty() { + return; + } + + // Walk the full contract — functions (including modifier call args, parameters, returns, + // and bodies) and state variable initializers — to collect every variable referenced + // anywhere in this contract. + let mut collector = UsedVarCollector { hir, used: HashSet::new() }; + for func_id in contract.all_functions() { + let _ = collector.visit_nested_function(func_id); + } + // State variables can reference other state variables in their initializers. + for var_id in contract.variables() { + let _ = collector.visit_nested_var(var_id); + } + + // Report any state variable that was never referenced. + for var_id in state_vars { + if !collector.used.contains(&var_id) { + let var = hir.variable(var_id); + ctx.emit(&UNUSED_STATE_VARIABLES, var.span); + } + } + } +} + +struct UsedVarCollector<'hir> { + hir: &'hir hir::Hir<'hir>, + used: HashSet, +} + +impl<'hir> hir::Visit<'hir> for UsedVarCollector<'hir> { + type BreakValue = Never; + + fn hir(&self) -> &'hir hir::Hir<'hir> { + self.hir + } + + fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow { + if let hir::ExprKind::Ident(resolutions) = &expr.kind { + for res in *resolutions { + if let hir::Res::Item(hir::ItemId::Variable(var_id)) = res { + self.used.insert(*var_id); + } + } + } + self.walk_expr(expr) + } +} diff --git a/crates/lint/src/sol/high/mod.rs b/crates/lint/src/sol/high/mod.rs index de199e88ff33a..09658ac232461 100644 --- a/crates/lint/src/sol/high/mod.rs +++ b/crates/lint/src/sol/high/mod.rs @@ -1,13 +1,16 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint}; mod incorrect_shift; +mod rtlo; mod unchecked_calls; use incorrect_shift::INCORRECT_SHIFT; +use rtlo::RTLO; use unchecked_calls::{ERC20_UNCHECKED_TRANSFER, UNCHECKED_CALL}; register_lints!( (IncorrectShift, early, (INCORRECT_SHIFT)), (UncheckedCall, early, (UNCHECKED_CALL)), - (UncheckedTransferERC20, late, (ERC20_UNCHECKED_TRANSFER)) + (UncheckedTransferERC20, late, (ERC20_UNCHECKED_TRANSFER)), + (Rtlo, early, (RTLO)) ); diff --git a/crates/lint/src/sol/high/rtlo.rs b/crates/lint/src/sol/high/rtlo.rs new file mode 100644 index 0000000000000..c121ac758da32 --- /dev/null +++ b/crates/lint/src/sol/high/rtlo.rs @@ -0,0 +1,58 @@ +use super::Rtlo; +use crate::{ + linter::{EarlyLintPass, Lint, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast, + interface::{BytePos, Span}, +}; + +declare_forge_lint!( + RTLO, + Severity::High, + "rtlo", + "unicode bidirectional override character can hide malicious code" +); + +impl<'ast> EarlyLintPass<'ast> for Rtlo { + fn check_full_source_unit( + &mut self, + ctx: &LintContext<'ast, '_>, + _unit: &'ast ast::SourceUnit<'ast>, + ) { + if !ctx.is_lint_enabled(RTLO.id()) { + return; + } + + // Scan the raw source so bidi chars in comments are also caught. + let Some(file) = ctx.source_file() else { return }; + + for (offset, ch) in file.src.char_indices() { + let Some(name) = bidi_char_name(ch) else { continue }; + + let lo = file.start_pos + BytePos::from_usize(offset); + let hi = lo + BytePos::from_usize(ch.len_utf8()); + let span = Span::new(lo, hi); + + ctx.emit_with_msg(&RTLO, span, format!("U+{:04X} ({name}) detected", ch as u32)); + } + } +} + +const fn bidi_char_name(ch: char) -> Option<&'static str> { + Some(match ch { + '\u{200E}' => "Left-to-Right Mark", + '\u{200F}' => "Right-to-Left Mark", + '\u{202A}' => "Left-to-Right Embedding", + '\u{202B}' => "Right-to-Left Embedding", + '\u{202C}' => "Pop Directional Formatting", + '\u{202D}' => "Left-to-Right Override", + '\u{202E}' => "Right-to-Left Override", + '\u{2066}' => "Left-to-Right Isolate", + '\u{2067}' => "Right-to-Left Isolate", + '\u{2068}' => "First Strong Isolate", + '\u{2069}' => "Pop Directional Isolate", + _ => return None, + }) +} diff --git a/crates/lint/src/sol/info/boolean_cst.rs b/crates/lint/src/sol/info/boolean_cst.rs new file mode 100644 index 0000000000000..50a7075338d3d --- /dev/null +++ b/crates/lint/src/sol/info/boolean_cst.rs @@ -0,0 +1,116 @@ +use super::BooleanCst; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{BinOp, BinOpKind, Expr, ExprKind, LitKind, Stmt, StmtKind, VariableDefinition}, + interface::SpannedOption, +}; + +declare_forge_lint!(BOOLEAN_CST, Severity::Med, "boolean-cst", "misuse of a boolean constant"); + +impl<'ast> EarlyLintPass<'ast> for BooleanCst { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + match &stmt.kind { + StmtKind::If(cond, ..) | StmtKind::DoWhile(_, cond) => { + check_expr(ctx, cond, ExprContext::Condition { allow_bare_true: false }); + } + StmtKind::While(cond, _) => { + check_expr(ctx, cond, ExprContext::Condition { allow_bare_true: true }); + } + StmtKind::For { cond: Some(cond), .. } => { + check_expr(ctx, cond, ExprContext::Condition { allow_bare_true: false }); + } + StmtKind::DeclMulti(_, expr) => check_allowed_bare_expr(ctx, expr), + StmtKind::Expr(expr) | StmtKind::Return(Some(expr)) => { + check_allowed_bare_expr(ctx, expr); + } + _ => {} + } + } + + fn check_variable_definition( + &mut self, + ctx: &LintContext, + var: &'ast VariableDefinition<'ast>, + ) { + if let Some(initializer) = &var.initializer { + check_allowed_bare_expr(ctx, initializer); + } + } +} + +#[derive(Clone, Copy)] +enum ExprContext { + Condition { allow_bare_true: bool }, + General, + AllowedBare, +} + +fn check_allowed_bare_expr(ctx: &LintContext, expr: &Expr<'_>) { + let context = + if bool_literal(expr).is_some() { ExprContext::AllowedBare } else { ExprContext::General }; + check_expr(ctx, expr, context); +} + +fn check_expr(ctx: &LintContext, expr: &Expr<'_>, context: ExprContext) { + if let Some(value) = bool_literal(expr) { + match context { + ExprContext::AllowedBare => {} + ExprContext::Condition { allow_bare_true: true } if value => {} + ExprContext::Condition { .. } | ExprContext::General => { + ctx.emit(&BOOLEAN_CST, expr.span); + } + } + return; + } + + match &expr.kind { + ExprKind::Assign(_, _, rhs) => check_allowed_bare_expr(ctx, rhs), + ExprKind::Binary(left, op, right) => check_binary_expr(ctx, left, *op, right), + ExprKind::Call(_, args) => { + for arg in args.exprs() { + check_allowed_bare_expr(ctx, arg); + } + } + ExprKind::Delete(expr) | ExprKind::Unary(_, expr) => { + check_expr(ctx, expr, ExprContext::General); + } + ExprKind::Ternary(cond, true_expr, false_expr) => { + check_expr(ctx, cond, ExprContext::Condition { allow_bare_true: false }); + check_expr(ctx, true_expr, ExprContext::General); + check_expr(ctx, false_expr, ExprContext::General); + } + ExprKind::Tuple(exprs) => { + for opt_expr in exprs.iter() { + if let SpannedOption::Some(expr) = opt_expr.as_ref() { + check_expr(ctx, expr, ExprContext::General); + } + } + } + _ => {} + } +} + +fn check_binary_expr(ctx: &LintContext, left: &Expr<'_>, op: BinOp, right: &Expr<'_>) { + if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) + && (bool_literal(left).is_some() || bool_literal(right).is_some()) + { + return; + } + + check_expr(ctx, left, ExprContext::General); + check_expr(ctx, right, ExprContext::General); +} + +fn bool_literal(expr: &Expr<'_>) -> Option { + let expr = expr.peel_parens(); + if let ExprKind::Lit(lit, _) = &expr.kind + && let LitKind::Bool(value) = lit.kind + { + Some(value) + } else { + None + } +} diff --git a/crates/lint/src/sol/info/boolean_equal.rs b/crates/lint/src/sol/info/boolean_equal.rs new file mode 100644 index 0000000000000..89cd7ec136f75 --- /dev/null +++ b/crates/lint/src/sol/info/boolean_equal.rs @@ -0,0 +1,108 @@ +use super::BooleanEqual; +use crate::{ + linter::{EarlyLintPass, LintContext, Suggestion}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{BinOp, BinOpKind, Expr, ExprKind, LitKind}, + interface::diagnostics::Applicability, +}; + +declare_forge_lint!( + BOOLEAN_EQUAL, + Severity::Info, + "boolean-equal", + "boolean comparisons to constants should be simplified" +); + +impl<'ast> EarlyLintPass<'ast> for BooleanEqual { + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + if let ExprKind::Binary( + left, + op @ BinOp { kind: BinOpKind::Eq | BinOpKind::Ne, .. }, + right, + ) = &expr.kind + { + match bool_comparison_suggestion(ctx, left, op.kind, right) { + BoolComparison::WithSuggestion(simplified) => { + ctx.emit_with_suggestion( + &BOOLEAN_EQUAL, + expr.span, + Suggestion::fix(simplified, Applicability::MachineApplicable) + .with_desc("consider simplifying to"), + ); + } + BoolComparison::WithoutSuggestion => ctx.emit(&BOOLEAN_EQUAL, expr.span), + BoolComparison::None => {} + } + } + } +} + +enum BoolComparison { + WithSuggestion(String), + WithoutSuggestion, + None, +} + +fn bool_comparison_suggestion( + ctx: &LintContext, + left: &Expr<'_>, + op: BinOpKind, + right: &Expr<'_>, +) -> BoolComparison { + let left_bool = bool_literal(left); + let right_bool = bool_literal(right); + + match (left_bool, right_bool) { + (Some(value), None) => simplify_expr(ctx, right, op, value), + (None, Some(value)) => simplify_expr(ctx, left, op, value), + (Some(_), Some(_)) => BoolComparison::WithoutSuggestion, + (None, None) => BoolComparison::None, + } +} + +fn bool_literal(expr: &Expr<'_>) -> Option { + let expr = expr.peel_parens(); + if let ExprKind::Lit(lit, _) = &expr.kind + && let LitKind::Bool(value) = lit.kind + { + Some(value) + } else { + None + } +} + +fn simplify_expr( + ctx: &LintContext, + expr: &Expr<'_>, + op: BinOpKind, + constant: bool, +) -> BoolComparison { + let Some(snippet) = ctx.span_to_snippet(expr.span) else { + return BoolComparison::WithoutSuggestion; + }; + + let simplified = match (op, constant) { + (BinOpKind::Eq, true) | (BinOpKind::Ne, false) => snippet, + (BinOpKind::Eq, false) | (BinOpKind::Ne, true) if can_negate_without_parens(expr) => { + format!("!{snippet}") + } + (BinOpKind::Eq, false) | (BinOpKind::Ne, true) => format!("!({snippet})"), + _ => return BoolComparison::None, + }; + + BoolComparison::WithSuggestion(simplified) +} + +fn can_negate_without_parens(expr: &Expr<'_>) -> bool { + matches!( + expr.peel_parens().kind, + ExprKind::Call(..) + | ExprKind::CallOptions(..) + | ExprKind::Ident(_) + | ExprKind::Index(..) + | ExprKind::Lit(..) + | ExprKind::Member(..) + ) +} diff --git a/crates/lint/src/sol/info/inline_assembly.rs b/crates/lint/src/sol/info/inline_assembly.rs new file mode 100644 index 0000000000000..1111129dada34 --- /dev/null +++ b/crates/lint/src/sol/info/inline_assembly.rs @@ -0,0 +1,71 @@ +use super::InlineAssembly; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Stmt, StmtKind}, + interface::{BytePos, Span}, +}; + +declare_forge_lint!( + INLINE_ASSEMBLY, + Severity::Info, + "inline-assembly", + "usage of inline assembly; assembly bypasses Solidity safety features and should be reviewed" +); + +const ASSEMBLY_KW_LEN: u32 = 8; +const NATSPEC_MEMORY_SAFE_MARKER: &str = "@solidity memory-safe-assembly"; + +impl<'ast> EarlyLintPass<'ast> for InlineAssembly { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + let StmtKind::Assembly(asm) = &stmt.kind else { return }; + + let kw_span = assembly_keyword_span(stmt.span); + + let memory_safe = asm.flags.iter().any(|f| f.value.as_str() == "memory-safe") + || has_memory_safe_natspec(ctx, stmt.span.lo()); + + let msg = if memory_safe { + "inline assembly (declared memory-safe); review business logic and side effects" + } else { + "inline assembly used; review for memory safety and side effects" + }; + + ctx.emit_with_msg(&INLINE_ASSEMBLY, kw_span, msg); + } +} + +/// Narrows a span to the leading `assembly` keyword to keep diagnostics readable. +fn assembly_keyword_span(span: Span) -> Span { + span.with_hi(span.lo() + BytePos(ASSEMBLY_KW_LEN)) +} + +/// Returns `true` when the lines immediately preceding `stmt_lo` form a `///` NatSpec block +/// containing `@solidity memory-safe-assembly`. +fn has_memory_safe_natspec(ctx: &LintContext, stmt_lo: BytePos) -> bool { + let Some(source_file) = ctx.source_file() else { return false }; + let src = source_file.src.as_str(); + let start_pos = source_file.start_pos.to_u32(); + let lo_abs = stmt_lo.to_u32(); + if lo_abs < start_pos { + return false; + } + let offset = (lo_abs - start_pos) as usize; + if offset > src.len() { + return false; + } + + for line in src[..offset].lines().rev() { + let trimmed = line.trim_start(); + if trimmed.is_empty() { + continue; + } + let Some(rest) = trimmed.strip_prefix("///") else { return false }; + if rest.trim_start().starts_with(NATSPEC_MEMORY_SAFE_MARKER) { + return true; + } + } + false +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index 290583cdc965a..913c5d2ea9da3 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -3,6 +3,12 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint}; mod mixed_case; use mixed_case::{MIXED_CASE_FUNCTION, MIXED_CASE_VARIABLE}; +mod boolean_cst; +use boolean_cst::BOOLEAN_CST; + +mod boolean_equal; +use boolean_equal::BOOLEAN_EQUAL; + mod pascal_case; use pascal_case::PASCAL_CASE_STRUCT; @@ -24,7 +30,18 @@ use multi_contract_file::MULTI_CONTRACT_FILE; mod interface_naming; use interface_naming::{INTERFACE_FILE_NAMING, INTERFACE_NAMING}; +mod too_many_digits; +use too_many_digits::TOO_MANY_DIGITS; + +mod pragma_directive; +use pragma_directive::PRAGMA_INCONSISTENT; + +mod inline_assembly; +use inline_assembly::INLINE_ASSEMBLY; + register_lints!( + (BooleanCst, early, (BOOLEAN_CST)), + (BooleanEqual, early, (BOOLEAN_EQUAL)), (PascalCaseStruct, early, (PASCAL_CASE_STRUCT)), (MixedCaseVariable, early, (MIXED_CASE_VARIABLE)), (MixedCaseFunction, early, (MIXED_CASE_FUNCTION)), @@ -34,4 +51,7 @@ register_lints!( (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)), (MultiContractFile, early, (MULTI_CONTRACT_FILE)), (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)), + (TooManyDigits, early, (TOO_MANY_DIGITS)), + (PragmaDirective, project, (PRAGMA_INCONSISTENT)), + (InlineAssembly, early, (INLINE_ASSEMBLY)), ); diff --git a/crates/lint/src/sol/info/pragma_directive.rs b/crates/lint/src/sol/info/pragma_directive.rs new file mode 100644 index 0000000000000..b66b6bcff6ade --- /dev/null +++ b/crates/lint/src/sol/info/pragma_directive.rs @@ -0,0 +1,71 @@ +use crate::{ + linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource}, + sol::{Severity, SolLint, info::PragmaDirective}, +}; +use solar::{ast, interface::Span}; + +declare_forge_lint!( + PRAGMA_INCONSISTENT, + Severity::Info, + "pragma-inconsistent", + "inconsistent Solidity pragma version requirements across the project" +); + +impl<'ast> ProjectLintPass<'ast> for PragmaDirective { + fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) { + if !ctx.is_lint_enabled(PRAGMA_INCONSISTENT.id()) { + return; + } + + // Collect every `pragma solidity` directive across input sources, with its rendered + // version-requirement string for grouping. Stores source index to avoid lifetime + // invariance issues with `&ProjectSource<'ast>`. + let mut entries: Vec<(usize, Span, String)> = Vec::new(); + for (idx, source) in sources.iter().enumerate() { + for (span, req) in solidity_pragmas(source.ast) { + entries.push((idx, span, req.to_string())); + } + } + + // Stable order for snapshots and JSON output. + entries.sort_by(|a, b| { + sources[a.0].path.cmp(&sources[b.0].path).then(a.1.lo().cmp(&b.1.lo())) + }); + + // Build the distinct list once and bail if all sources agree. + let mut distinct: Vec<&str> = entries.iter().map(|(_, _, s)| s.as_str()).collect(); + distinct.sort_unstable(); + distinct.dedup(); + if distinct.len() < 2 { + return; + } + + for (idx, span, req_str) in &entries { + let others = distinct + .iter() + .filter(|v| **v != req_str.as_str()) + .copied() + .collect::>() + .join(", "); + let msg = format!( + "'pragma solidity {req_str};' conflicts with other version requirements in the project: {others}" + ); + ctx.emit_with_msg(&sources[*idx], &PRAGMA_INCONSISTENT, *span, msg); + } + } +} + +/// Yields every top-level `pragma solidity ...;` directive in `unit`. +fn solidity_pragmas<'ast>( + unit: &'ast ast::SourceUnit<'ast>, +) -> impl Iterator)> + 'ast { + unit.items.iter().filter_map(|item| match &item.kind { + ast::ItemKind::Pragma(p) => match &p.tokens { + ast::PragmaTokens::Version(ident, req) if ident.as_str() == "solidity" => { + Some((item.span, req)) + } + _ => None, + }, + _ => None, + }) +} diff --git a/crates/lint/src/sol/info/too_many_digits.rs b/crates/lint/src/sol/info/too_many_digits.rs new file mode 100644 index 0000000000000..3ba9e8abba2de --- /dev/null +++ b/crates/lint/src/sol/info/too_many_digits.rs @@ -0,0 +1,50 @@ +use super::TooManyDigits; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::ast::{Expr, ExprKind, LitKind}; + +declare_forge_lint!( + TOO_MANY_DIGITS, + Severity::Info, + "too-many-digits", + "numeric literal with many digits is error-prone; \ + use scientific notation, sub-denominations, or underscore separators" +); + +impl<'ast> EarlyLintPass<'ast> for TooManyDigits { + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + let ExprKind::Lit(lit, sub_denom) = &expr.kind else { return }; + + // Only plain integer literals. `LitKind::Address` (40-hex-digit address) is a + // distinct variant and is therefore skipped automatically. + if !matches!(lit.kind, LitKind::Number(_)) { + return; + } + + // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`. + if sub_denom.is_some() { + return; + } + + let s = lit.symbol.as_str(); + + // Skip hex literals — long zero runs in hex are usually intentional (masks, + // selectors, bit patterns) and there is no scientific-notation alternative. + if s.starts_with("0x") || s.starts_with("0X") { + return; + } + + // Skip if the user already used scientific notation (`1e18`). + if s.contains('e') || s.contains('E') { + return; + } + + // 5+ consecutive zeros in the literal as written. Underscores are + // preserved, so `1_000_000` passes while `1_000000` is flagged. + if s.contains("00000") { + ctx.emit(&TOO_MANY_DIGITS, lit.span); + } + } +} diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index 00d764770374a..8540ab8b95b8f 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -9,9 +9,11 @@ /// - `$desc`: A short description of the lint. /// /// # Note -/// Each lint must have a `help` section in the foundry book. This help field is auto-generated by -/// the macro. Because of that, to ensure that new lint rules have their corresponding docs in the -/// book, the existence of the lint rule's help section is validated with a unit test. +/// Each lint must have a corresponding markdown documentation file at +/// `crates/lint/docs/.md`. The `help` URL is auto-generated by the macro and points to +/// the per-lint page on the Foundry docs site (`getfoundry.sh/forge/linting/`). To +/// ensure that new lint rules have their corresponding docs, the existence of every registered +/// lint's markdown file is validated by a unit test (see `crates/lint/src/sol/mod.rs`). #[macro_export] macro_rules! declare_forge_lint { ($id:ident, $severity:expr, $str_id:expr, $desc:expr) => { @@ -20,7 +22,7 @@ macro_rules! declare_forge_lint { id: $str_id, severity: $severity, description: $desc, - help: concat!("https://book.getfoundry.sh/reference/forge/forge-lint#", $str_id), + help: concat!("https://getfoundry.sh/forge/linting/", $str_id), }; }; } @@ -53,6 +55,7 @@ macro_rules! register_lints { register_lints!(@early_impl $pass_id, $pass_type); register_lints!(@late_impl $pass_id, $pass_type); + register_lints!(@project_impl $pass_id, $pass_type); } )* }; @@ -89,10 +92,22 @@ macro_rules! register_lints { .flatten() .collect() } + + pub fn create_project_lint_passes<'ast>() -> Vec<(Box>, &'static [SolLint])> { + [ + $( + register_lints!(@project_create $pass_id, $pass_type), + )* + ] + .into_iter() + .flatten() + .collect() + } }; // --- HELPERS ------------------------------------------------------------ (@early_impl $_pass_id:ident, late) => {}; + (@early_impl $_pass_id:ident, project) => {}; (@early_impl $pass_id:ident, $other:ident) => { pub fn as_early_lint_pass<'a>() -> Box> { Box::new(Self::default()) @@ -100,22 +115,41 @@ macro_rules! register_lints { }; (@late_impl $_pass_id:ident, early) => {}; + (@late_impl $_pass_id:ident, project) => {}; (@late_impl $pass_id:ident, $other:ident) => { pub fn as_late_lint_pass<'hir>() -> Box> { Box::new(Self::default()) } }; + (@project_impl $_pass_id:ident, early) => {}; + (@project_impl $_pass_id:ident, late) => {}; + (@project_impl $_pass_id:ident, both) => {}; + (@project_impl $pass_id:ident, $other:ident) => { + pub fn as_project_lint_pass<'ast>() -> Box> { + Box::new(Self::default()) + } + }; + (@early_create $_pass_id:ident, late) => { None }; + (@early_create $_pass_id:ident, project) => { None }; (@early_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_early_lint_pass(), $pass_id::LINTS)) }; (@late_create $_pass_id:ident, early) => { None }; + (@late_create $_pass_id:ident, project) => { None }; (@late_create $pass_id:ident, $_other:ident) => { Some(($pass_id::as_late_lint_pass(), $pass_id::LINTS)) }; + (@project_create $_pass_id:ident, early) => { None }; + (@project_create $_pass_id:ident, late) => { None }; + (@project_create $_pass_id:ident, both) => { None }; + (@project_create $pass_id:ident, $_other:ident) => { + Some(($pass_id::as_project_lint_pass(), $pass_id::LINTS)) + }; + // --- ENTRY POINT --------------------------------------------------------- ( $($tokens:tt)* ) => { register_lints! { @declare_structs $($tokens)* } diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs index ba7a09b0e9bac..2673ba23d3252 100644 --- a/crates/lint/src/sol/med/mod.rs +++ b/crates/lint/src/sol/med/mod.rs @@ -9,6 +9,9 @@ use incorrect_erc20_interface::INCORRECT_ERC20_INTERFACE; mod incorrect_erc721_interface; use incorrect_erc721_interface::INCORRECT_ERC721_INTERFACE; +mod tx_origin; +use tx_origin::TX_ORIGIN; + mod unsafe_typecast; use unsafe_typecast::UNSAFE_TYPECAST; @@ -16,5 +19,6 @@ register_lints!( (DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)), (IncorrectERC20Interface, late, (INCORRECT_ERC20_INTERFACE)), (IncorrectERC721Interface, late, (INCORRECT_ERC721_INTERFACE)), + (TxOrigin, early, (TX_ORIGIN)), (UnsafeTypecast, late, (UNSAFE_TYPECAST)) ); diff --git a/crates/lint/src/sol/med/tx_origin.rs b/crates/lint/src/sol/med/tx_origin.rs new file mode 100644 index 0000000000000..00ff5f939ebfb --- /dev/null +++ b/crates/lint/src/sol/med/tx_origin.rs @@ -0,0 +1,101 @@ +use super::TxOrigin; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast::{Expr, ExprKind, IndexKind, Stmt, StmtKind}, + interface::SpannedOption, +}; + +declare_forge_lint!( + TX_ORIGIN, + Severity::Med, + "tx-origin", + "`tx.origin` should not be used for authorization" +); + +impl<'ast> EarlyLintPass<'ast> for TxOrigin { + fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) { + match &stmt.kind { + StmtKind::If(cond, ..) | StmtKind::DoWhile(_, cond) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::While(cond, _) => { + emit_if_contains_tx_origin(ctx, cond); + } + StmtKind::For { cond: Some(cond), .. } => { + emit_if_contains_tx_origin(ctx, cond); + } + _ => {} + } + } + + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + if let ExprKind::Call(callee, args) = &expr.kind + && is_require_or_assert_call(callee) + && let Some(cond) = args.exprs().next() + { + emit_if_contains_tx_origin(ctx, cond); + } + } +} + +fn emit_if_contains_tx_origin(ctx: &LintContext, expr: &Expr<'_>) { + if contains_tx_origin(expr) { + ctx.emit(&TX_ORIGIN, expr.span); + } +} + +fn contains_tx_origin(expr: &Expr<'_>) -> bool { + if is_tx_origin(expr) { + return true; + } + match &expr.kind { + ExprKind::Unary(_, inner) => contains_tx_origin(inner), + ExprKind::Binary(lhs, _, rhs) => contains_tx_origin(lhs) || contains_tx_origin(rhs), + ExprKind::Index(base, index) => { + contains_tx_origin(base) + || match index { + IndexKind::Index(Some(index)) => contains_tx_origin(index), + IndexKind::Range(start, end) => { + start.as_ref().is_some_and(|start| contains_tx_origin(start)) + || end.as_ref().is_some_and(|end| contains_tx_origin(end)) + } + _ => false, + } + } + ExprKind::Tuple(elems) => elems.iter().any(|elem| { + if let SpannedOption::Some(inner) = elem.as_ref() { + contains_tx_origin(inner) + } else { + false + } + }), + ExprKind::Call(callee, args) => { + contains_tx_origin(callee) || args.exprs().any(contains_tx_origin) + } + ExprKind::Ternary(cond, then_expr, else_expr) => { + contains_tx_origin(cond) + || contains_tx_origin(then_expr) + || contains_tx_origin(else_expr) + } + _ => false, + } +} + +fn is_tx_origin(expr: &Expr<'_>) -> bool { + matches!( + &expr.kind, + ExprKind::Member(base, member) + if member.as_str() == "origin" + && matches!(&base.kind, ExprKind::Ident(ident) if ident.as_str() == "tx") + ) +} + +fn is_require_or_assert_call(callee: &Expr<'_>) -> bool { + matches!( + &callee.kind, + ExprKind::Ident(ident) if matches!(ident.as_str(), "require" | "assert") + ) +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 59de6a40df015..7ae073f2ea20b 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,6 +1,6 @@ use crate::linter::{ EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter, - LinterConfig, + LinterConfig, ProjectLintEmitter, ProjectLintPass, ProjectSource, }; use foundry_common::{ comments::{ @@ -21,6 +21,7 @@ use solar::{ interface::{ Session, diagnostics::{self, HumanEmitter, JsonEmitter}, + source_map::SourceFile, }, sema::{ Compiler, Gcx, @@ -29,7 +30,7 @@ use solar::{ }; use std::{ path::{Path, PathBuf}, - sync::LazyLock, + sync::{Arc, LazyLock}, }; use thiserror::Error; @@ -130,6 +131,7 @@ impl<'a> SolidityLinter<'a> { ast: &'gcx ast::SourceUnit<'gcx>, path: &Path, inline_config: &InlineConfig>, + source_file: Option>, ) -> Result<(), diagnostics::ErrorGuaranteed> { // Declare all available passes and lints let mut passes_and_lints = Vec::new(); @@ -168,6 +170,7 @@ impl<'a> SolidityLinter<'a> { self.with_json_emitter, self.config(inline_config), lints, + source_file, ); let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes); _ = early_visitor.visit_source_unit(ast); @@ -176,12 +179,69 @@ impl<'a> SolidityLinter<'a> { Ok(()) } + /// Runs all enabled project-wide lint passes against the given input sources. + fn process_project<'gcx>(&self, gcx: Gcx<'gcx>, input: &[PathBuf]) { + // Gather enabled project passes from every severity bucket. + let mut passes_and_lints: Vec<(Box>, &'static [SolLint])> = + Vec::new(); + passes_and_lints.extend(high::create_project_lint_passes()); + passes_and_lints.extend(med::create_project_lint_passes()); + passes_and_lints.extend(low::create_project_lint_passes()); + passes_and_lints.extend(info::create_project_lint_passes()); + passes_and_lints.extend(gas::create_project_lint_passes()); + passes_and_lints.extend(codesize::create_project_lint_passes()); + + let (mut passes, lint_ids): (Vec>>, Vec<_>) = passes_and_lints + .into_iter() + .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| { + let included: Vec<_> = lints + .iter() + .filter_map(|lint| self.include_lint(*lint).then_some(lint.id)) + .collect(); + if !included.is_empty() { + passes.push(pass); + ids.extend(included); + } + (passes, ids) + }); + + if passes.is_empty() { + return; + } + + // Pre-load every input source with its inline config, in input order. + let sources: Vec> = input + .iter() + .filter_map(|path| { + let path = self.path_config.root.join(path); + let (_, source) = gcx.get_ast_source(&path)?; + let ast = source.ast.as_ref()?; + let comments = + Comments::new(&source.file, gcx.sess.source_map(), false, false, None); + let inline_config = parse_inline_config(gcx.sess, &comments, ast); + Some(ProjectSource { path, file: source.file.clone(), ast, inline_config }) + }) + .collect(); + + let emitter = ProjectLintEmitter::new( + gcx.sess, + self.with_description, + self.with_json_emitter, + self.lint_specific, + lint_ids, + ); + for pass in &mut passes { + pass.check_project(&emitter, &sources); + } + } + fn process_source_hir<'gcx>( &self, gcx: Gcx<'gcx>, source_id: hir::SourceId, path: &Path, inline_config: &InlineConfig>, + source_file: Option>, ) -> Result<(), diagnostics::ErrorGuaranteed> { // Declare all available passes and lints let mut passes_and_lints = Vec::new(); @@ -220,6 +280,7 @@ impl<'a> SolidityLinter<'a> { self.with_json_emitter, self.config(inline_config), lints, + source_file, ); let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir); @@ -288,15 +349,30 @@ impl<'a> Linter for SolidityLinter<'a> { let inline_config = parse_inline_config(gcx.sess, &comments, ast); // Early lints. - let _ = self.process_source_ast(gcx.sess, ast, path, &inline_config); + let _ = self.process_source_ast( + gcx.sess, + ast, + path, + &inline_config, + Some(file.clone()), + ); // Late lints. let Some((hir_source_id, _)) = gcx.get_hir_source(path) else { panic!("HIR source not found for {}", path.display()); }; - let _ = self.process_source_hir(gcx, hir_source_id, path, &inline_config); + let _ = self.process_source_hir( + gcx, + hir_source_id, + path, + &inline_config, + Some(file.clone()), + ); }); + // Project-wide lints, run once after all per-file passes. + self.process_project(gcx, input); + convert_solar_errors(compiler.dcx()) })?; @@ -436,3 +512,75 @@ impl<'a> TryFrom<&'a str> for SolLint { Err(SolLintError::InvalidId(value.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every registered lint must have a markdown documentation file at + /// `crates/lint/docs/.md`. This test enforces that contract so that the `help` URL + /// generated by `declare_forge_lint!` always resolves to real documentation. + /// + /// When this test fails, add a new file at `crates/lint/docs/.md` describing the + /// lint. See [`crates/lint/docs/_template.md`](../../docs/_template.md) for the expected + /// structure. + #[test] + fn registered_lints_have_docs() { + let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs"); + assert!(docs_dir.is_dir(), "missing docs directory at {}", docs_dir.display()); + + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + let mut missing: Vec<&'static str> = Vec::new(); + let mut empty: Vec<&'static str> = Vec::new(); + for lint in &all_lints { + let path = docs_dir.join(format!("{}.md", lint.id())); + match std::fs::read_to_string(&path) { + Ok(content) => { + // Basic sanity: file should be non-trivial and reference the lint id. + if content.trim().is_empty() || !content.contains(lint.id()) { + empty.push(lint.id()); + } + } + Err(_) => missing.push(lint.id()), + } + } + + assert!( + missing.is_empty(), + "the following registered lints are missing a docs file at \ + `crates/lint/docs/.md`: {missing:?}\n\ + See `crates/lint/docs/_template.md` for the expected structure." + ); + assert!( + empty.is_empty(), + "the following lint docs files are empty or do not reference the lint id: {empty:?}" + ); + } + + /// The auto-generated `help` URL must point at the canonical Foundry docs site so that the + /// link printed in diagnostics resolves correctly. + #[test] + fn registered_lints_have_canonical_help_url() { + let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS + .iter() + .chain(med::REGISTERED_LINTS) + .chain(low::REGISTERED_LINTS) + .chain(info::REGISTERED_LINTS) + .chain(gas::REGISTERED_LINTS) + .chain(codesize::REGISTERED_LINTS) + .collect(); + + for lint in all_lints { + let expected = format!("https://getfoundry.sh/forge/linting/{}", lint.id()); + assert_eq!(lint.help(), expected, "lint `{}` has a non-canonical help URL", lint.id()); + } + } +} diff --git a/crates/lint/testdata/BlockTimestamp.stderr b/crates/lint/testdata/BlockTimestamp.stderr index 016f8fa2bdb2d..62ab588ae7340 100644 --- a/crates/lint/testdata/BlockTimestamp.stderr +++ b/crates/lint/testdata/BlockTimestamp.stderr @@ -4,7 +4,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -12,7 +12,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp == 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -20,7 +20,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp != 0; │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -28,7 +28,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp <= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -36,7 +36,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp >= deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -44,7 +44,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp < deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -52,7 +52,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return deadline > block.timestamp; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -60,7 +60,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return block.timestamp + 1 > deadline; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -68,7 +68,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return (block.timestamp / 3600) == 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -76,7 +76,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ require(block.timestamp > deadline); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -84,7 +84,7 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ if (block.timestamp > deadline) { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC @@ -92,5 +92,5 @@ warning[block-timestamp]: usage of `block.timestamp` in a comparison may be mani LL │ return foo(block.timestamp) > 0; │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + ╰ help: https://getfoundry.sh/forge/linting/block-timestamp diff --git a/crates/lint/testdata/BooleanCst.sol b/crates/lint/testdata/BooleanCst.sol new file mode 100644 index 0000000000000..2b7cdc32f6702 --- /dev/null +++ b/crates/lint/testdata/BooleanCst.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract BooleanCst { + function check(bool flag) public pure returns (bool) { + if (false) {} //~WARN: misuse of a boolean constant + if (flag || true) {} //~WARN: misuse of a boolean constant + if (flag ? true : false) {} + //~^WARN: misuse of a boolean constant + //~|WARN: misuse of a boolean constant + while (true) { + break; + } + + bool assigned = true; + return assigned && false; //~WARN: misuse of a boolean constant + } + + function allowedBareConstants(bool flag) public pure returns (bool) { + takesBool(true); + return true; + } + + function takesBool(bool value) internal pure {} +} diff --git a/crates/lint/testdata/BooleanCst.stderr b/crates/lint/testdata/BooleanCst.stderr new file mode 100644 index 0000000000000..75fdb0b57cea7 --- /dev/null +++ b/crates/lint/testdata/BooleanCst.stderr @@ -0,0 +1,40 @@ +warning[boolean-cst]: misuse of a boolean constant + ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC + │ +LL │ if (false) {} + │ ━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst + +warning[boolean-cst]: misuse of a boolean constant + ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC + │ +LL │ if (flag || true) {} + │ ━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst + +warning[boolean-cst]: misuse of a boolean constant + ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC + │ +LL │ if (flag ? true : false) {} + │ ━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst + +warning[boolean-cst]: misuse of a boolean constant + ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC + │ +LL │ if (flag ? true : false) {} + │ ━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst + +warning[boolean-cst]: misuse of a boolean constant + ╭▸ ROOT/testdata/BooleanCst.sol:LL:CC + │ +LL │ return assigned && false; + │ ━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-cst + diff --git a/crates/lint/testdata/BooleanEqual.sol b/crates/lint/testdata/BooleanEqual.sol new file mode 100644 index 0000000000000..30171e9c797ac --- /dev/null +++ b/crates/lint/testdata/BooleanEqual.sol @@ -0,0 +1,24 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract BooleanEqual { + function check(bool enabled, bool paused, bool ready, bool done) public pure { + if (enabled == true) {} //~NOTE: boolean comparisons to constants should be simplified + if (paused == false) {} //~NOTE: boolean comparisons to constants should be simplified + if (true != ready) {} //~NOTE: boolean comparisons to constants should be simplified + while (done != false) { //~NOTE: boolean comparisons to constants should be simplified + break; + } + for (; enabled == true && paused != false;) { + //~^NOTE: boolean comparisons to constants should be simplified + //~|NOTE: boolean comparisons to constants should be simplified + break; + } + } + + function returnedComparison(bool enabled) public pure returns (bool) { + return enabled == true; //~NOTE: boolean comparisons to constants should be simplified + } +} diff --git a/crates/lint/testdata/BooleanEqual.stderr b/crates/lint/testdata/BooleanEqual.stderr new file mode 100644 index 0000000000000..590a85b806fcf --- /dev/null +++ b/crates/lint/testdata/BooleanEqual.stderr @@ -0,0 +1,56 @@ +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ if (enabled == true) {} + │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ if (paused == false) {} + │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `!paused` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ if (true != ready) {} + │ ━━━━━━━━━━━━━ help: consider simplifying to: `!ready` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ while (done != false) { + │ ━━━━━━━━━━━━━ help: consider simplifying to: `done` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ for (; enabled == true && paused != false;) { + │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ for (; enabled == true && paused != false;) { + │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `paused` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + +note[boolean-equal]: boolean comparisons to constants should be simplified + ╭▸ ROOT/testdata/BooleanEqual.sol:LL:CC + │ +LL │ return enabled == true; + │ ━━━━━━━━━━━━━━━ help: consider simplifying to: `enabled` + │ + ╰ help: https://getfoundry.sh/forge/linting/boolean-equal + diff --git a/crates/lint/testdata/CouldBeImmutable.sol b/crates/lint/testdata/CouldBeImmutable.sol new file mode 100644 index 0000000000000..cd7e9aea13d01 --- /dev/null +++ b/crates/lint/testdata/CouldBeImmutable.sol @@ -0,0 +1,85 @@ +//@compile-flags: --only-lint could-be-immutable + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract CouldBeImmutable { + uint256 public constant MAX = 10; + uint256 public immutable ALREADY_IMMUTABLE; + + address public owner; + address public deployer = msg.sender; + uint256 private configured; + bytes32 internal salt = keccak256(abi.encodePacked(block.timestamp)); + CouldBeImmutable private peer; + + uint256 private mutableValue; + uint256 private assignedInInternal; + uint256 private compileTimeConstant = 1 + 2; + string private dynamicValue; + + constructor(uint256 configured_, CouldBeImmutable peer_, string memory value) { + ALREADY_IMMUTABLE = configured_; + owner = msg.sender; + configured = configured_; + peer = peer_; + mutableValue = 1; + setInternal(1); + dynamicValue = value; + } + + function setMutableValue(uint256 newValue) public { + mutableValue = newValue; + } + + function setInternal(uint256 newValue) internal { + assignedInInternal = newValue; + } +} + +contract BaseImmutableCandidate { + uint256 internal inheritedBase; +} + +contract DerivedImmutableCandidate is BaseImmutableCandidate { + constructor(uint256 value) { + inheritedBase = value; + } +} + +contract BaseConstructorImmutableCandidate { + uint256 internal baseConfigured; + + constructor(uint256 value) { + baseConfigured = value; + } +} + +contract DerivedConstructorImmutableCandidate is BaseConstructorImmutableCandidate { + constructor(uint256 value) BaseConstructorImmutableCandidate(value) {} +} + +contract ModifierBodyWrite { + uint256 private fromModifier; + + modifier initializesState() { + fromModifier = 1; + _; + } + + constructor() initializesState() {} +} + +contract AssemblyWrite { + uint256 private fromAssembly; + + constructor() { + fromAssembly = 1; + } + + function mutate() public { + assembly { + sstore(0, 2) + } + } +} diff --git a/crates/lint/testdata/CouldBeImmutable.stderr b/crates/lint/testdata/CouldBeImmutable.stderr new file mode 100644 index 0000000000000..170682baf89d3 --- /dev/null +++ b/crates/lint/testdata/CouldBeImmutable.stderr @@ -0,0 +1,56 @@ +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ address public owner; + │ ━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ address public deployer = msg.sender; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ uint256 private configured; + │ ━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ bytes32 internal salt = keccak256(abi.encodePacked(block.timestamp)); + │ ━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ CouldBeImmutable private peer; + │ ━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ uint256 internal inheritedBase; + │ ━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/CouldBeImmutable.sol:LL:CC + │ +LL │ uint256 internal baseConfigured; + │ ━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + diff --git a/crates/lint/testdata/CustomErrors.stderr b/crates/lint/testdata/CustomErrors.stderr index 66b3c11bc183c..286a649aee269 100644 --- a/crates/lint/testdata/CustomErrors.stderr +++ b/crates/lint/testdata/CustomErrors.stderr @@ -4,7 +4,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ require(a > 0, "Value must be greater than zero"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -12,7 +12,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ … require(a >= 0 && a <= 100 || b == 50, "Complex condition should be linted"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -20,7 +20,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert("Something went wrong"); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -28,7 +28,7 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(""); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors note[custom-errors]: prefer using custom errors on revert and require calls ╭▸ ROOT/testdata/CustomErrors.sol:LL:CC @@ -36,5 +36,5 @@ note[custom-errors]: prefer using custom errors on revert and require calls LL │ revert(); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#custom-errors + ╰ help: https://getfoundry.sh/forge/linting/custom-errors diff --git a/crates/lint/testdata/DivideBeforeMultiply.stderr b/crates/lint/testdata/DivideBeforeMultiply.stderr index c0e5ef78e2e1c..95022f65db874 100644 --- a/crates/lint/testdata/DivideBeforeMultiply.stderr +++ b/crates/lint/testdata/DivideBeforeMultiply.stderr @@ -4,7 +4,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2) * 3; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -12,7 +12,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 / 2) * 3) * 4; │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -20,7 +20,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ ((1 * 2) / 3) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -28,7 +28,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / 2 / 3) * 4; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -36,7 +36,7 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ (1 / (2 + 3)) * 4; │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply warning[divide-before-multiply]: multiplication should occur before division to avoid loss of precision ╭▸ ROOT/testdata/DivideBeforeMultiply.sol:LL:CC @@ -44,5 +44,5 @@ warning[divide-before-multiply]: multiplication should occur before division to LL │ 1 / ((2 / 3) * 3); │ ━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#divide-before-multiply + ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply diff --git a/crates/lint/testdata/Imports.stderr b/crates/lint/testdata/Imports.stderr index 8fa9800b27ded..1031f4f6f8ca0 100644 --- a/crates/lint/testdata/Imports.stderr +++ b/crates/lint/testdata/Imports.stderr @@ -4,7 +4,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsSomeFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." as X' ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -12,7 +12,7 @@ note[unaliased-plain-import]: use named imports '{A, B}' or alias 'import ".." a LL │ import "./auxiliary/ImportsAnotherFile.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unaliased-plain-import + ╰ help: https://getfoundry.sh/forge/linting/unaliased-plain-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -20,7 +20,7 @@ note[unused-import]: unused imports should be removed LL │ symbol2 as notUsed, │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -28,7 +28,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol, │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -36,7 +36,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbol2, │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -44,7 +44,7 @@ note[unused-import]: unused imports should be removed LL │ docSymbolWrongTag, │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -52,7 +52,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed, │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -60,7 +60,7 @@ note[unused-import]: unused imports should be removed LL │ IContractNotUsed │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -68,7 +68,7 @@ note[unused-import]: unused imports should be removed LL │ symbolNotUsed3 │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -76,7 +76,7 @@ note[unused-import]: unused imports should be removed LL │ CONSTANT_1 │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -84,7 +84,7 @@ note[unused-import]: unused imports should be removed LL │ YetAnotherType │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -92,7 +92,7 @@ note[unused-import]: unused imports should be removed LL │ import "./auxiliary/ImportsAnotherFile2.sol" as AnotherFile2; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import note[unused-import]: unused imports should be removed ╭▸ ROOT/testdata/Imports.sol:LL:CC @@ -100,5 +100,5 @@ note[unused-import]: unused imports should be removed LL │ import * as OtherUtils from "./auxiliary/ImportsUtils2.sol"; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + ╰ help: https://getfoundry.sh/forge/linting/unused-import diff --git a/crates/lint/testdata/IncorrectERC20Interface.stderr b/crates/lint/testdata/IncorrectERC20Interface.stderr index 3bb60ecce8320..33e2f1ca27d22 100644 --- a/crates/lint/testdata/IncorrectERC20Interface.stderr +++ b/crates/lint/testdata/IncorrectERC20Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 {} │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC20 { │ ━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Incorrect is IERC20 { │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Correct is IERC20 { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20NamedCorrect { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC20 { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transfer(address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function transferFrom(address from, address to, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function approve(address spender, uint256 value) external returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function allowance(address owner, address spender) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function balanceOf(address account) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface warning[incorrect-erc20-interface]: incorrect ERC20 function interface ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC @@ -116,5 +116,5 @@ warning[incorrect-erc20-interface]: incorrect ERC20 function interface LL │ function totalSupply() external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc20-interface diff --git a/crates/lint/testdata/IncorrectERC721Interface.stderr b/crates/lint/testdata/IncorrectERC721Interface.stderr index a88db93e39b10..2e68084c1cec1 100644 --- a/crates/lint/testdata/IncorrectERC721Interface.stderr +++ b/crates/lint/testdata/IncorrectERC721Interface.stderr @@ -4,7 +4,7 @@ note[interface-naming]: interface names should be prefixed with 'I' LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + ╰ help: https://getfoundry.sh/forge/linting/interface-naming note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721 {} │ ━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface ERC721 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Incorrect is IERC721 { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721Correct is IERC721 { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC721NamedCorrect { │ ━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface INotERC721 { │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -60,7 +60,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -68,7 +68,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -76,7 +76,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function balanceOf(address owner) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -84,7 +84,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function ownerOf(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -92,7 +92,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -100,7 +100,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function safeTransferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -108,7 +108,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function transferFrom(address from, address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -116,7 +116,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function approve(address to, uint256 tokenId) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -124,7 +124,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function setApprovalForAll(address operator, bool approved) external returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -132,7 +132,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function getApproved(uint256 tokenId) external view returns (bool); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -140,7 +140,7 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function isApprovedForAll(address owner, address operator) external view returns (address); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface warning[incorrect-erc721-interface]: incorrect ERC721 function interface ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC @@ -148,5 +148,5 @@ warning[incorrect-erc721-interface]: incorrect ERC721 function interface LL │ function supportsInterface(bytes4 interfaceId) external view returns (uint256); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + ╰ help: https://getfoundry.sh/forge/linting/incorrect-erc721-interface diff --git a/crates/lint/testdata/IncorrectShift.stderr b/crates/lint/testdata/IncorrectShift.stderr index bce84c98df432..dfff32db897bb 100644 --- a/crates/lint/testdata/IncorrectShift.stderr +++ b/crates/lint/testdata/IncorrectShift.stderr @@ -4,7 +4,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 2 << stateValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -12,7 +12,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 8 >> localValue; │ ━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -20,7 +20,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 16 << (stateValue + 1); │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -28,7 +28,7 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ result = 32 >> getAmount(); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift warning[incorrect-shift]: the order of args in a shift operation is incorrect ╭▸ ROOT/testdata/IncorrectShift.sol:LL:CC @@ -36,5 +36,5 @@ warning[incorrect-shift]: the order of args in a shift operation is incorrect LL │ … result = 1 << (localValue > 10 ? localShiftAmount : stateShiftAmount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-shift + ╰ help: https://getfoundry.sh/forge/linting/incorrect-shift diff --git a/crates/lint/testdata/InlineAssembly.sol b/crates/lint/testdata/InlineAssembly.sol new file mode 100644 index 0000000000000..05917ea22784c --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.sol @@ -0,0 +1,110 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract InlineAssembly { + function bare() public view returns (uint256 id) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + id := chainid() + } + } + + function withMemorySafe() public view returns (uint256 size) { + assembly ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + size := extcodesize(address()) + } + } + + function withDialectAndMemorySafe() public view returns (uint256 ptr) { + assembly "evmasm" ("memory-safe") { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + ptr := mload(0x40) + } + } + + function withNatspecMemorySafe() public view returns (uint256 v) { + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := chainid() + } + } + + function withNatspecMemorySafeAndOtherDocs() public view returns (uint256 v) { + /// @notice does a thing + /// @solidity memory-safe-assembly + assembly { //~NOTE: inline assembly (declared memory-safe); review business logic and side effects + v := gas() + } + } + + function plainCommentDoesNotCount() public view returns (uint256 v) { + // solidity memory-safe-assembly + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := chainid() + } + } + + function nestedInControlFlow(bool flag) public view returns (uint256 v) { + if (flag) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := gas() + } + } + + for (uint256 i = 0; i < 1; ++i) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInUnchecked(uint256 x) public pure returns (uint256 v) { + unchecked { + v = x + 1; + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := add(v, 1) + } + } + } + + function nestedInTryCatch() public returns (uint256 v) { + try this.bare() returns (uint256) { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 1 + } + } catch { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + v := 2 + } + } + } + + function suppressed() public view returns (uint256 id) { + // forge-lint: disable-next-line(inline-assembly) + assembly { + id := chainid() + } + } + + modifier guarded() { + assembly { //~NOTE: inline assembly used; review for memory safety and side effects + if iszero(caller()) { revert(0, 0) } + } + _; + } + + function suppressedRegion() public view returns (uint256 a, uint256 b) { + // forge-lint: disable-start(inline-assembly) + assembly { + a := chainid() + } + assembly ("memory-safe") { + b := gas() + } + // forge-lint: disable-end(inline-assembly) + } + + function noAssembly() public pure returns (uint256) { + return 42; + } +} diff --git a/crates/lint/testdata/InlineAssembly.stderr b/crates/lint/testdata/InlineAssembly.stderr new file mode 100644 index 0000000000000..12f8bcbacd14e --- /dev/null +++ b/crates/lint/testdata/InlineAssembly.stderr @@ -0,0 +1,96 @@ +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly "evmasm" ("memory-safe") { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly (declared memory-safe); review business logic and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + +note[inline-assembly]: inline assembly used; review for memory safety and side effects + ╭▸ ROOT/testdata/InlineAssembly.sol:LL:CC + │ +LL │ assembly { + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/inline-assembly + diff --git a/crates/lint/testdata/Keccak256.sol b/crates/lint/testdata/Keccak256.sol index 41f856336b5b3..2457aed96d601 100644 --- a/crates/lint/testdata/Keccak256.sol +++ b/crates/lint/testdata/Keccak256.sol @@ -52,6 +52,7 @@ contract AsmKeccak256 { function assemblyHash(uint256 a, uint256 b) public pure returns (bytes32) { //optimized + // forge-lint: disable-next-line(inline-assembly) assembly { mstore(0x00, a) mstore(0x20, b) diff --git a/crates/lint/testdata/Keccak256.stderr b/crates/lint/testdata/Keccak256.stderr index 05d1dcf5641a8..a81e429e389a1 100644 --- a/crates/lint/testdata/Keccak256.stderr +++ b/crates/lint/testdata/Keccak256.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `mixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Another_MixedCase = 2; │ ━━━━━━━━━━━━━━━━━ help: consider using: `anotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 YetAnother_MixedCase = 3; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `yetAnotherMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Enabled_MixedCase_Variable = 1; │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `enabledMixedCaseVariable` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -44,7 +44,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract AsmKeccak256 { │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -52,7 +52,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract OtherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -60,7 +60,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract YetAnotherAsmKeccak256 { │ ━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -68,7 +68,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 hash = keccak256(abi.encodePacked(a, b, bytes32(bytes20(c)))); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -76,7 +76,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 afterDisabledBlock = keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -84,7 +84,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromCalldata = keccak256(z); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -92,7 +92,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 loadsFromMemory = keccak256(y); │ ━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -100,7 +100,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 lintWithoutFix = keccak256(abi.encodePacked(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -108,7 +108,15 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(a, b, c)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 + +note[unused-state-variables]: state variable is never used + ╭▸ ROOT/testdata/Keccak256.sol:LL:CC + │ +LL │ uint256 Enabled_MixedCase_Variable; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -116,7 +124,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesNotUseScratchSpace = keccak256(abi.encode(x, y, x, y, x, y)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -124,7 +132,7 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ bytes32 doesUseScratchSpace = keccak256(abi.encode(x)); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline assembly ╭▸ ROOT/testdata/Keccak256.sol:LL:CC @@ -132,5 +140,5 @@ note[asm-keccak256]: use of inefficient hashing mechanism; consider using inline LL │ return keccak256(abi.encode(doesUseScratchSpace, doesNotUseScratchSpace)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#asm-keccak256 + ╰ help: https://getfoundry.sh/forge/linting/asm-keccak256 diff --git a/crates/lint/testdata/MissingZeroCheck.stderr b/crates/lint/testdata/MissingZeroCheck.stderr index b55a902547fcf..81a9179e79c94 100644 --- a/crates/lint/testdata/MissingZeroCheck.stderr +++ b/crates/lint/testdata/MissingZeroCheck.stderr @@ -4,7 +4,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwner(address newOwner) external { │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -12,7 +12,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ constructor(address initialOwner) { │ ━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -20,7 +20,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function pay(address payable to) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -28,7 +28,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function lowLevel(address payable to, bytes calldata data) external { │ ━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -36,7 +36,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function withUselessModifier(address a) external doesNothing(a) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -44,7 +44,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaAlias(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -52,7 +52,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaReassign(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -60,7 +60,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function setOwnerViaCast(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -68,7 +68,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payViaAlias(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -76,7 +76,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function mixedParams(address a, address b) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -84,7 +84,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function bothSinks(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -92,7 +92,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function ternaryAlias(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -100,7 +100,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function payableWrap(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -108,7 +108,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function modifierWithExpr(address a) external nonZero(addrIdentity(a)) { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -116,7 +116,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function delegateCallSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -124,7 +124,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkStmt(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -132,7 +132,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function sendSinkDecl(address payable a) external { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -140,7 +140,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function multiHopTaint(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -148,7 +148,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardAfterSink(address a) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -156,7 +156,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardOnOneBranch(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -164,7 +164,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInForLoop(address a, uint256 n) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -172,7 +172,7 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInWhileLoop(address a, bool flag) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC @@ -180,5 +180,5 @@ warning[missing-zero-check]: address parameter is used in a state write or value LL │ function guardInTryClause(address a, address payable target) external { │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check diff --git a/crates/lint/testdata/MixedCase.stderr b/crates/lint/testdata/MixedCase.stderr index d290af5cdb5a8..2db30559ba5a6 100644 --- a/crates/lint/testdata/MixedCase.stderr +++ b/crates/lint/testdata/MixedCase.stderr @@ -4,7 +4,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 Variablemixedcase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variablemixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -12,7 +12,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VARIABLE_MIXED_CASE; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -20,7 +20,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 VariableMixedCase; │ ━━━━━━━━━━━━━━━━━ help: consider using: `variableMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -28,7 +28,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 testVAL; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -36,7 +36,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TestVal; │ ━━━━━━━ help: consider using: `testVal` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-variable]: mutable variables should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -44,7 +44,7 @@ note[mixed-case-variable]: mutable variables should use mixedCase LL │ uint256 TESTVAL; │ ━━━━━━━ help: consider using: `testval` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-variable + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-variable note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -52,7 +52,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function Functionmixedcase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionmixedcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -60,7 +60,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FUNCTION_MIXED_CASE() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -68,7 +68,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function FunctionMixedCase() public {} │ ━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -76,7 +76,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function function_mixed_case() public {} │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `functionMixedCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -84,7 +84,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantBalance_MixedCase_Enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantBalanceMixedCaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -92,7 +92,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function invariantbalance_mixedcase_enabled() public {} │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `invariantbalanceMixedcaseEnabled` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -100,7 +100,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function ERC20_DoSomething() public {} // invalid because of the underscore │ ━━━━━━━━━━━━━━━━━ help: consider using: `erc20DoSomething` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -108,7 +108,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_PARAMS(address addr) external view returns (uint256) {} │ ━━━━━━━━━━ help: consider using: `hasParams` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -116,7 +116,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_NO_RETURN() external view {} │ ━━━━━━━━━━━━━ help: consider using: `hasNoReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -124,7 +124,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function HAS_MORE_THAN_ONE_RETURN() external view returns (uint256, uint256) {} │ ━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `hasMoreThanOneReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[mixed-case-function]: function names should use mixedCase ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -132,7 +132,7 @@ note[mixed-case-function]: function names should use mixedCase LL │ function NOT_ELEMENTARY_RETURN() external view returns (uint256[] memory) {} │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `notElementaryReturn` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#mixed-case-function + ╰ help: https://getfoundry.sh/forge/linting/mixed-case-function note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -140,7 +140,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MixedCase.sol:LL:CC @@ -148,5 +148,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract MixedCaseTest { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile.stderr b/crates/lint/testdata/MultiContractFile.stderr index c6e4e32a2df55..e25f3d72ad01a 100644 --- a/crates/lint/testdata/MultiContractFile.stderr +++ b/crates/lint/testdata/MultiContractFile.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract A {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract B {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile.sol:LL:CC @@ -36,5 +36,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L {} │ ━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr index 41fc439ea7d1b..1912f16863712 100644 --- a/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr +++ b/crates/lint/testdata/MultiContractFile_InterfaceLibrary.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface I1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library L1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/MultiContractFile_InterfaceLibrary.sol:LL:CC @@ -20,5 +20,5 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract C1 {} │ ━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file diff --git a/crates/lint/testdata/NamedStructFields.stderr b/crates/lint/testdata/NamedStructFields.stderr index 6ee2160791cd2..cfb35637176bd 100644 --- a/crates/lint/testdata/NamedStructFields.stderr +++ b/crates/lint/testdata/NamedStructFields.stderr @@ -4,5 +4,5 @@ note[named-struct-fields]: prefer initializing structs with named fields LL │ Person memory person = Person("Alice", 25, address(0)); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ help: consider using named fields: `Person({ name: "Alice", age: 25, wallet: address(0) })` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#named-struct-fields + ╰ help: https://getfoundry.sh/forge/linting/named-struct-fields diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol new file mode 100644 index 0000000000000..bfc993baab794 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 +pragma solidity 0.8.18; //~NOTE: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr new file mode 100644 index 0000000000000..c2c967dee792f --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretAboveExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: 0.8.18 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.18;' conflicts with other version requirements in the project: ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentCaretAboveExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.18; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol new file mode 100644 index 0000000000000..75bc17988accc --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr new file mode 100644 index 0000000000000..f60361718ba9b --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretMatchesExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretMatchesExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol new file mode 100644 index 0000000000000..37b06040c33a6 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; //~NOTE: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 +pragma solidity ~0.8.20; //~NOTE: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr new file mode 100644 index 0000000000000..6c46f2478208d --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentCaretVsTilde.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: ~0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ^0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.20;' conflicts with other version requirements in the project: ^0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentCaretVsTilde.sol:LL:CC + │ +LL │ pragma solidity ~0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.sol b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol new file mode 100644 index 0000000000000..f85a477cc8744 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20 || 0.8.21; //~NOTE: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr new file mode 100644 index 0000000000000..acf6bd7c2d6e0 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentOrVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity 0.8.20 || 0.8.21;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20 || 0.8.21; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: 0.8.20 || 0.8.21 + ╭▸ ROOT/testdata/PragmaInconsistentOrVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol new file mode 100644 index 0000000000000..d8fcb7a0eb4b1 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.sol @@ -0,0 +1,7 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; //~NOTE: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 +pragma solidity 0.8.20; //~NOTE: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr new file mode 100644 index 0000000000000..5ac221b924c9a --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentRangeVsExact.stderr @@ -0,0 +1,16 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0 <0.9.0;' conflicts with other version requirements in the project: 0.8.20 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0 <0.9.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: >=0.8.0 <0.9.0 + ╭▸ ROOT/testdata/PragmaInconsistentRangeVsExact.sol:LL:CC + │ +LL │ pragma solidity 0.8.20; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol new file mode 100644 index 0000000000000..fe208e15efb63 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.sol @@ -0,0 +1,8 @@ +//@compile-flags: --only-lint pragma-inconsistent + +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; //~NOTE: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 +pragma solidity ^0.8.0; //~NOTE: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 +pragma solidity ~0.8.0; //~NOTE: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + +contract Main {} diff --git a/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr new file mode 100644 index 0000000000000..e1e5ad7333fb2 --- /dev/null +++ b/crates/lint/testdata/PragmaInconsistentThreeDistinct.stderr @@ -0,0 +1,24 @@ +note[pragma-inconsistent]: 'pragma solidity >=0.8.0;' conflicts with other version requirements in the project: ^0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity >=0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ^0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ~0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ^0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + +note[pragma-inconsistent]: 'pragma solidity ~0.8.0;' conflicts with other version requirements in the project: >=0.8.0, ^0.8.0 + ╭▸ ROOT/testdata/PragmaInconsistentThreeDistinct.sol:LL:CC + │ +LL │ pragma solidity ~0.8.0; + │ ━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent + diff --git a/crates/lint/testdata/Rtlo.sol b/crates/lint/testdata/Rtlo.sol new file mode 100644 index 0000000000000..82f7e68a41548 --- /dev/null +++ b/crates/lint/testdata/Rtlo.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +// Tests for the `rtlo` lint, which detects "Trojan Source" bidirectional +// formatting characters (CVE-2021-42574). These have no legitimate use in +// Solidity source and can be used to hide malicious code. +// +// Note: solc itself rejects unbalanced directional override markers (error +// 8936), so each test uses a balanced opening/closing pair. Our lint flags +// each occurrence individually regardless of balance. + +contract Rtlo { + // SHOULD FAIL: every codepoint in the Trojan-Source set is flagged. + // Each line below contains two bidi characters (an opener and its closer) + // and produces two diagnostics. + + string public lre = unicode"‪_‬"; + //~^WARN: U+202A (Left-to-Right Embedding) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + string public rle = unicode"‫_‬"; + //~^WARN: U+202B (Right-to-Left Embedding) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + string public pdf = unicode"‪‬"; + //~^WARN: U+202A (Left-to-Right Embedding) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + string public lro = unicode"‭_‬"; + //~^WARN: U+202D (Left-to-Right Override) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + string public rlo = unicode"‮_‬"; + //~^WARN: U+202E (Right-to-Left Override) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + string public lri = unicode"⁦_⁩"; + //~^WARN: U+2066 (Left-to-Right Isolate) detected + //~|WARN: U+2069 (Pop Directional Isolate) detected + + string public rli = unicode"⁧_⁩"; + //~^WARN: U+2067 (Right-to-Left Isolate) detected + //~|WARN: U+2069 (Pop Directional Isolate) detected + + string public fsi = unicode"⁨_⁩"; + //~^WARN: U+2068 (First Strong Isolate) detected + //~|WARN: U+2069 (Pop Directional Isolate) detected + + string public pdi = unicode"⁦⁩"; + //~^WARN: U+2066 (Left-to-Right Isolate) detected + //~|WARN: U+2069 (Pop Directional Isolate) detected + + // SHOULD FAIL: bidi controls inside a block comment are also detected. + /* hidden‮ /* text ‬ */ uint256 inBlockComment; + //~^WARN: U+202E (Right-to-Left Override) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + // SHOULD FAIL: bidi controls inside a line comment are also detected. The + // expectation markers must come on separate lines because the ui-test + // parser only treats the first comment on a line as a marker. + // sneaky‮ payload ‬ trailing + //~^WARN: U+202E (Right-to-Left Override) detected + //~|WARN: U+202C (Pop Directional Formatting) detected + + // SHOULD PASS: inline-config disable suppresses the diagnostic. + // forge-lint: disable-next-line(rtlo) + string public suppressedLine = unicode"‮_‬"; + + // forge-lint: disable-start(rtlo) + string public suppressedA = unicode"‮_‬"; + string public suppressedB = unicode"⁦_⁩"; + // forge-lint: disable-end(rtlo) + + // SHOULD PASS: plain ASCII source, no bidi controls. + string public clean = "no bidi here"; + + // SHOULD FAIL: LRM/RLM marks (U+200E/U+200F) are also flagged. + string public marks = unicode"left‎right‏end"; + //~^WARN: U+200E (Left-to-Right Mark) detected + //~|WARN: U+200F (Right-to-Left Mark) detected +} diff --git a/crates/lint/testdata/Rtlo.stderr b/crates/lint/testdata/Rtlo.stderr new file mode 100644 index 0000000000000..93f5bb191532f --- /dev/null +++ b/crates/lint/testdata/Rtlo.stderr @@ -0,0 +1,192 @@ +warning[rtlo]: U+202A (Left-to-Right Embedding) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lre = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lre = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202B (Right-to-Left Embedding) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rle = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rle = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202A (Left-to-Right Embedding) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public pdf = unicode"��"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public pdf = unicode"��"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202D (Left-to-Right Override) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lro = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lro = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202E (Right-to-Left Override) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rlo = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rlo = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2066 (Left-to-Right Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lri = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2069 (Pop Directional Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public lri = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2067 (Right-to-Left Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rli = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2069 (Pop Directional Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public rli = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2068 (First Strong Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public fsi = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2069 (Pop Directional Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public fsi = unicode"�_�"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2066 (Left-to-Right Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public pdi = unicode"��"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+2069 (Pop Directional Isolate) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public pdi = unicode"��"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202E (Right-to-Left Override) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ /* hidden� /* text � */ uint256 inBlockComment; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ /* hidden� /* text � */ uint256 inBlockComment; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202E (Right-to-Left Override) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ // sneaky� payload � trailing + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ // sneaky� payload � trailing + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+200E (Left-to-Right Mark) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public marks = unicode"left‎right‏end"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+200F (Right-to-Left Mark) detected + ╭▸ ROOT/testdata/Rtlo.sol:LL:CC + │ +LL │ string public marks = unicode"left‎right‏end"; + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + diff --git a/crates/lint/testdata/RtloCommentsOnly.sol b/crates/lint/testdata/RtloCommentsOnly.sol new file mode 100644 index 0000000000000..82a1fbd7fc58e --- /dev/null +++ b/crates/lint/testdata/RtloCommentsOnly.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +// Bidi chars that only appear in comments (outside any item) must still be +// reported. + +// hidden‮ payload ‬ trailing +//~^WARN: U+202E (Right-to-Left Override) detected +//~|WARN: U+202C (Pop Directional Formatting) detected + +/* block‮ comment ‬ end */ +//~^WARN: U+202E (Right-to-Left Override) detected +//~|WARN: U+202C (Pop Directional Formatting) detected + +contract RtloCommentsOnly {} diff --git a/crates/lint/testdata/RtloCommentsOnly.stderr b/crates/lint/testdata/RtloCommentsOnly.stderr new file mode 100644 index 0000000000000..5a7ec9ee6e69d --- /dev/null +++ b/crates/lint/testdata/RtloCommentsOnly.stderr @@ -0,0 +1,32 @@ +warning[rtlo]: U+202E (Right-to-Left Override) detected + ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC + │ +LL │ // hidden� payload � trailing + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC + │ +LL │ // hidden� payload � trailing + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202E (Right-to-Left Override) detected + ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC + │ +LL │ /* block� comment � end */ + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + +warning[rtlo]: U+202C (Pop Directional Formatting) detected + ╭▸ ROOT/testdata/RtloCommentsOnly.sol:LL:CC + │ +LL │ /* block� comment � end */ + │ ━ + │ + ╰ help: https://getfoundry.sh/forge/linting/rtlo + diff --git a/crates/lint/testdata/ScreamingSnakeCase.stderr b/crates/lint/testdata/ScreamingSnakeCase.stderr index a740506ed74d8..36305bb268d9a 100644 --- a/crates/lint/testdata/ScreamingSnakeCase.stderr +++ b/crates/lint/testdata/ScreamingSnakeCase.stderr @@ -4,7 +4,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -12,7 +12,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant screaming_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -20,7 +20,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant ScreamingSnakeCase = 0; │ ━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -28,7 +28,7 @@ note[screaming-snake-case-const]: constants should use SCREAMING_SNAKE_CASE LL │ uint256 constant SCREAMING_snake_case = 0; │ ━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-const + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-const note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -36,7 +36,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -44,7 +44,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable screaming_snake_case0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -52,7 +52,7 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable ScreamingSnakeCase0 = 0; │ ━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE ╭▸ ROOT/testdata/ScreamingSnakeCase.sol:LL:CC @@ -60,5 +60,5 @@ note[screaming-snake-case-immutable]: immutables should use SCREAMING_SNAKE_CASE LL │ uint256 immutable SCREAMING_snake_case_0 = 0; │ ━━━━━━━━━━━━━━━━━━━━━━ help: consider using: `SCREAMING_SNAKE_CASE_0` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#screaming-snake-case-immutable + ╰ help: https://getfoundry.sh/forge/linting/screaming-snake-case-immutable diff --git a/crates/lint/testdata/StructPascalCase.stderr b/crates/lint/testdata/StructPascalCase.stderr index 1c7bfa13ba84b..255c1c4d5d74b 100644 --- a/crates/lint/testdata/StructPascalCase.stderr +++ b/crates/lint/testdata/StructPascalCase.stderr @@ -4,7 +4,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct _PascalCase { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -12,7 +12,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalCase { │ ━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -20,7 +20,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascalcase { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -28,7 +28,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct pascal_case { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -36,7 +36,7 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCAL_CASE { │ ━━━━━━━━━━━ help: consider using: `PascalCase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct note[pascal-case-struct]: structs should use PascalCase ╭▸ ROOT/testdata/StructPascalCase.sol:LL:CC @@ -44,5 +44,5 @@ note[pascal-case-struct]: structs should use PascalCase LL │ struct PASCALCASE { │ ━━━━━━━━━━ help: consider using: `Pascalcase` │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#pascal-case-struct + ╰ help: https://getfoundry.sh/forge/linting/pascal-case-struct diff --git a/crates/lint/testdata/TooManyDigits.sol b/crates/lint/testdata/TooManyDigits.sol new file mode 100644 index 0000000000000..a56ad67fe379e --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.sol @@ -0,0 +1,73 @@ +//@compile-flags: --severity info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TooManyDigits { + // SHOULD FAIL: plain decimal integer literals with 5+ consecutive zeros. + + uint256 stateA = 1000000000000000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + uint256 stateB = 100000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + function asReturn() public pure returns (uint256) { + return 10000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asComparison(uint256 x) public pure returns (bool) { + return x == 1000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArg(address to) public { + _send(to, 50000000000); //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + function asArraySize() public pure { + uint256[100000] memory _arr; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + } + + // Zero-run in the middle (not just trailing). + uint256 middleZeros = 123000007; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscores that don't actually break up the zero run. + uint256 badGrouping = 1_000000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // Underscore right after a single digit, leaving a 5-zero group. + uint256 badGrouping2 = 1_00000; //~NOTE: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + + // SHOULD PASS: + + // Boundary: 4 consecutive zeros (one short of the threshold). + uint256 fourZeros = 10000; + + // Uppercase scientific notation. + uint256 sciUpper = 1E18; + + // Scientific notation. + uint256 sci = 1e18; + + // Underscore-separated digit groups. + uint256 grouped = 1_000_000_000_000_000_000; + + // Sub-denominations. + uint256 oneEther = 1 ether; + uint256 oneGwei = 1 gwei; + uint256 fiveMin = 5 minutes; + + // Address literal (distinct AST kind, not flagged). + address adr = 0x1234567890123456789012345678901234567890; + + // Hex literal — intentional zero patterns (mask / padded value). + bytes32 mask = 0x0000000000000000000000000000000000000000000000000000000000000001; + uint256 hexNum = 0x100000; + + // Small literals (< 5 consecutive zeros). + uint256 small1 = 100; + uint256 small2 = 9999; + uint256 small3 = 1234; + uint256 spread = 101010; + + // Boolean literal. + bool flag = true; + + function _send(address, uint256) internal pure {} +} diff --git a/crates/lint/testdata/TooManyDigits.stderr b/crates/lint/testdata/TooManyDigits.stderr new file mode 100644 index 0000000000000..7e21a530776c2 --- /dev/null +++ b/crates/lint/testdata/TooManyDigits.stderr @@ -0,0 +1,72 @@ +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateA = 1000000000000000000; + │ ━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 stateB = 100000; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return 10000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … return x == 1000000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … _send(to, 50000000000); + │ ━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ … uint256[100000] memory _arr; + │ ━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 middleZeros = 123000007; + │ ━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping = 1_000000; + │ ━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + +note[too-many-digits]: numeric literal with many digits is error-prone; use scientific notation, sub-denominations, or underscore separators + ╭▸ ROOT/testdata/TooManyDigits.sol:LL:CC + │ +LL │ uint256 badGrouping2 = 1_00000; + │ ━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/too-many-digits + diff --git a/crates/lint/testdata/TxOrigin.sol b/crates/lint/testdata/TxOrigin.sol new file mode 100644 index 0000000000000..9728a7e528e5b --- /dev/null +++ b/crates/lint/testdata/TxOrigin.sol @@ -0,0 +1,65 @@ +//@compile-flags: --only-lint tx-origin +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract TxOrigin { + address public owner; + mapping(address => bool) public allowed; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(tx.origin == owner, "not owner"); //~WARN: `tx.origin` should not be used for authorization + _; + } + + function guardedByIf() external view { + if (tx.origin != owner) { //~WARN: `tx.origin` should not be used for authorization + revert("not owner"); + } + } + + function guardedByPredicate() external view { + assert(isOwner(tx.origin)); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByWhile() external view { + while (tx.origin == owner) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByFor() external view { + for (; tx.origin == owner;) { //~WARN: `tx.origin` should not be used for authorization + break; + } + } + + function guardedByDoWhile() external view { + do { + } while (tx.origin == owner); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByMapping() external view { + require(allowed[tx.origin], "not allowed"); //~WARN: `tx.origin` should not be used for authorization + require(allowed[tx.origin] == true, "not allowed"); //~WARN: `tx.origin` should not be used for authorization + } + + function guardedByTernary() external view { + require(tx.origin == owner ? true : false, "not owner"); //~WARN: `tx.origin` should not be used for authorization + } + + function readForLogging() external view returns (address) { + return tx.origin; + } + + function explicitSenderCheck() external view { + require(msg.sender == owner, "not owner"); + } + + function isOwner(address account) internal view returns (bool) { + return account == owner; + } +} diff --git a/crates/lint/testdata/TxOrigin.stderr b/crates/lint/testdata/TxOrigin.stderr new file mode 100644 index 0000000000000..7c2e70225b76d --- /dev/null +++ b/crates/lint/testdata/TxOrigin.stderr @@ -0,0 +1,72 @@ +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner, "not owner"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ if (tx.origin != owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ assert(isOwner(tx.origin)); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ while (tx.origin == owner) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ for (; tx.origin == owner;) { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ } while (tx.origin == owner); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin], "not allowed"); + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(allowed[tx.origin] == true, "not allowed"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + +warning[tx-origin]: `tx.origin` should not be used for authorization + ╭▸ ROOT/testdata/TxOrigin.sol:LL:CC + │ +LL │ require(tx.origin == owner ? true : false, "not owner"); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/tx-origin + diff --git a/crates/lint/testdata/UncheckedCall.stderr b/crates/lint/testdata/UncheckedCall.stderr index afb8ade4ea89b..8a8a9fa9b5e17 100644 --- a/crates/lint/testdata/UncheckedCall.stderr +++ b/crates/lint/testdata/UncheckedCall.stderr @@ -4,7 +4,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call(data); │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -12,7 +12,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.call{value: value}(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -20,7 +20,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.delegatecall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -28,7 +28,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target.staticcall(data); │ ━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -36,7 +36,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target1.call(""); │ ━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -44,7 +44,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ target2.delegatecall(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -52,7 +52,7 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, bytes memory data) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call warning[unchecked-call]: Low-level calls should check the success return value ╭▸ ROOT/testdata/UncheckedCall.sol:LL:CC @@ -60,5 +60,5 @@ warning[unchecked-call]: Low-level calls should check the success return value LL │ (, existingData) = target.call(""); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unchecked-call + ╰ help: https://getfoundry.sh/forge/linting/unchecked-call diff --git a/crates/lint/testdata/UncheckedTransferERC20.stderr b/crates/lint/testdata/UncheckedTransferERC20.stderr index 733d22ce610d1..2c2caa69e7215 100644 --- a/crates/lint/testdata/UncheckedTransferERC20.stderr +++ b/crates/lint/testdata/UncheckedTransferERC20.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20 { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ interface IERC20Wrapper { │ ━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -20,7 +20,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransfer { │ ━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -28,7 +28,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ library Currency { │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -36,7 +36,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UncheckedTransferUsingCurrencyLib { │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -44,7 +44,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ IERC20(address(token)).transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -52,7 +52,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -60,7 +60,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -68,7 +68,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transferFrom(from, to, amount); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -76,7 +76,7 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ … IERC20(address(token)).transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value ╭▸ ROOT/testdata/UncheckedTransferERC20.sol:LL:CC @@ -84,5 +84,5 @@ warning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls sho LL │ token.transfer(recipients[i], amounts[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer + ╰ help: https://getfoundry.sh/forge/linting/erc20-unchecked-transfer diff --git a/crates/lint/testdata/UnsafeCheatcodes.stderr b/crates/lint/testdata/UnsafeCheatcodes.stderr index e66a4d72c70de..5b8b429942e80 100644 --- a/crates/lint/testdata/UnsafeCheatcodes.stderr +++ b/crates/lint/testdata/UnsafeCheatcodes.stderr @@ -4,7 +4,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -12,7 +12,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -20,7 +20,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readLine("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -28,7 +28,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeFile("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -36,7 +36,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.writeLine("test.txt", "data"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -44,7 +44,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.removeFile("test.txt"); │ ━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -52,7 +52,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.closeFile("test.txt"); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -60,7 +60,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -68,7 +68,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.deriveKey("mnemonic", 0); │ ━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -76,7 +76,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ bytes memory result = vm.ffi(inputs); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -84,7 +84,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.ffi(new string[](1)); │ ━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -92,7 +92,7 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.setEnv("KEY", "value"); │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous operations ╭▸ ROOT/testdata/UnsafeCheatcodes.sol:LL:CC @@ -100,5 +100,5 @@ note[unsafe-cheatcode]: usage of unsafe cheatcodes that can perform dangerous op LL │ vm.readFile("test.txt"); │ ━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-cheatcode + ╰ help: https://getfoundry.sh/forge/linting/unsafe-cheatcode diff --git a/crates/lint/testdata/UnsafeTypecast.stderr b/crates/lint/testdata/UnsafeTypecast.stderr index b3e0334d63d43..d909b90973e00 100644 --- a/crates/lint/testdata/UnsafeTypecast.stderr +++ b/crates/lint/testdata/UnsafeTypecast.stderr @@ -4,7 +4,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract UnsafeTypecast { │ ━━━━━━━━━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file note[multi-contract-file]: prefer having only one contract, interface or library per file ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -12,7 +12,7 @@ note[multi-contract-file]: prefer having only one contract, interface or library LL │ contract Repros { │ ━━━━━━ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + ╰ help: https://getfoundry.sh/forge/linting/multi-contract-file warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -26,7 +26,7 @@ LL │ uint248 b = uint248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -40,7 +40,7 @@ LL │ uint240 c = uint240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -54,7 +54,7 @@ LL │ uint232 d = uint232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -68,7 +68,7 @@ LL │ uint224 e = uint224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -82,7 +82,7 @@ LL │ uint216 f = uint216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -96,7 +96,7 @@ LL │ uint208 g = uint208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -110,7 +110,7 @@ LL │ uint200 h = uint200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -124,7 +124,7 @@ LL │ uint192 i = uint192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -138,7 +138,7 @@ LL │ uint184 j = uint184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -152,7 +152,7 @@ LL │ uint176 k = uint176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -166,7 +166,7 @@ LL │ uint168 l = uint168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -180,7 +180,7 @@ LL │ uint160 m = uint160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -194,7 +194,7 @@ LL │ uint152 n = uint152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -208,7 +208,7 @@ LL │ uint144 o = uint144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -222,7 +222,7 @@ LL │ uint136 p = uint136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -236,7 +236,7 @@ LL │ uint128 q = uint128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -250,7 +250,7 @@ LL │ uint120 r = uint120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -264,7 +264,7 @@ LL │ uint112 s = uint112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -278,7 +278,7 @@ LL │ uint104 t = uint104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -292,7 +292,7 @@ LL │ uint96 u = uint96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -306,7 +306,7 @@ LL │ uint88 v = uint88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -320,7 +320,7 @@ LL │ uint80 w = uint80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -334,7 +334,7 @@ LL │ uint72 x = uint72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -348,7 +348,7 @@ LL │ uint64 y = uint64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -362,7 +362,7 @@ LL │ uint56 z = uint56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -376,7 +376,7 @@ LL │ uint48 A = uint48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -390,7 +390,7 @@ LL │ uint40 B = uint40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -404,7 +404,7 @@ LL │ uint32 C = uint32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -418,7 +418,7 @@ LL │ uint24 D = uint24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -432,7 +432,7 @@ LL │ uint16 E = uint16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -446,7 +446,7 @@ LL │ uint8 F = uint8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -460,7 +460,7 @@ LL │ int248 b = int248(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -474,7 +474,7 @@ LL │ int240 c = int240(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -488,7 +488,7 @@ LL │ int232 d = int232(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -502,7 +502,7 @@ LL │ int224 e = int224(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -516,7 +516,7 @@ LL │ int216 f = int216(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -530,7 +530,7 @@ LL │ int208 g = int208(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -544,7 +544,7 @@ LL │ int200 h = int200(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -558,7 +558,7 @@ LL │ int192 i = int192(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -572,7 +572,7 @@ LL │ int184 j = int184(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -586,7 +586,7 @@ LL │ int176 k = int176(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -600,7 +600,7 @@ LL │ int168 l = int168(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -614,7 +614,7 @@ LL │ int160 m = int160(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -628,7 +628,7 @@ LL │ int152 n = int152(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -642,7 +642,7 @@ LL │ int144 o = int144(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -656,7 +656,7 @@ LL │ int136 p = int136(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -670,7 +670,7 @@ LL │ int128 q = int128(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -684,7 +684,7 @@ LL │ int120 r = int120(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -698,7 +698,7 @@ LL │ int112 s = int112(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -712,7 +712,7 @@ LL │ int104 t = int104(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -726,7 +726,7 @@ LL │ int96 u = int96(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -740,7 +740,7 @@ LL │ int88 v = int88(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -754,7 +754,7 @@ LL │ int80 w = int80(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -768,7 +768,7 @@ LL │ int72 x = int72(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -782,7 +782,7 @@ LL │ int64 y = int64(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -796,7 +796,7 @@ LL │ int56 z = int56(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -810,7 +810,7 @@ LL │ int48 A = int48(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -824,7 +824,7 @@ LL │ int40 B = int40(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -838,7 +838,7 @@ LL │ int32 C = int32(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -852,7 +852,7 @@ LL │ int24 D = int24(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -866,7 +866,7 @@ LL │ int16 E = int16(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -880,7 +880,7 @@ LL │ int8 F = int8(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -894,7 +894,7 @@ LL │ bytes31 b = bytes31(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -908,7 +908,7 @@ LL │ bytes30 c = bytes30(b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -922,7 +922,7 @@ LL │ bytes29 d = bytes29(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -936,7 +936,7 @@ LL │ bytes28 e = bytes28(d); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -950,7 +950,7 @@ LL │ bytes27 f = bytes27(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -964,7 +964,7 @@ LL │ bytes26 g = bytes26(f); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -978,7 +978,7 @@ LL │ bytes25 h = bytes25(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -992,7 +992,7 @@ LL │ bytes24 i = bytes24(h); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1006,7 +1006,7 @@ LL │ bytes23 j = bytes23(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1020,7 +1020,7 @@ LL │ bytes22 k = bytes22(j); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1034,7 +1034,7 @@ LL │ bytes21 l = bytes21(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1048,7 +1048,7 @@ LL │ bytes20 m = bytes20(l); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1062,7 +1062,7 @@ LL │ bytes19 n = bytes19(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1076,7 +1076,7 @@ LL │ bytes18 o = bytes18(n); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1090,7 +1090,7 @@ LL │ bytes17 p = bytes17(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1104,7 +1104,7 @@ LL │ bytes16 q = bytes16(p); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1118,7 +1118,7 @@ LL │ bytes15 r = bytes15(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1132,7 +1132,7 @@ LL │ bytes14 s = bytes14(r); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1146,7 +1146,7 @@ LL │ bytes13 t = bytes13(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1160,7 +1160,7 @@ LL │ bytes12 u = bytes12(t); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1174,7 +1174,7 @@ LL │ bytes11 v = bytes11(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1188,7 +1188,7 @@ LL │ bytes10 w = bytes10(v); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1202,7 +1202,7 @@ LL │ bytes9 x = bytes9(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1216,7 +1216,7 @@ LL │ bytes8 y = bytes8(x); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1230,7 +1230,7 @@ LL │ bytes7 z = bytes7(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1244,7 +1244,7 @@ LL │ bytes6 A = bytes6(z); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1258,7 +1258,7 @@ LL │ bytes5 B = bytes5(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1272,7 +1272,7 @@ LL │ bytes4 C = bytes4(B); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1286,7 +1286,7 @@ LL │ bytes3 D = bytes3(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1300,7 +1300,7 @@ LL │ bytes2 E = bytes2(D); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1314,7 +1314,7 @@ LL │ bytes1 F = bytes1(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1328,7 +1328,7 @@ LL │ int256 b = int256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1342,7 +1342,7 @@ LL │ int248 d = int248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1356,7 +1356,7 @@ LL │ int240 f = int240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1370,7 +1370,7 @@ LL │ int232 h = int232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1384,7 +1384,7 @@ LL │ int224 j = int224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1398,7 +1398,7 @@ LL │ int216 l = int216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1412,7 +1412,7 @@ LL │ int208 n = int208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1426,7 +1426,7 @@ LL │ int200 p = int200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1440,7 +1440,7 @@ LL │ int192 r = int192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1454,7 +1454,7 @@ LL │ int184 t = int184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1468,7 +1468,7 @@ LL │ int176 v = int176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1482,7 +1482,7 @@ LL │ int168 x = int168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1496,7 +1496,7 @@ LL │ int160 z = int160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1510,7 +1510,7 @@ LL │ int152 B = int152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1524,7 +1524,7 @@ LL │ int144 D = int144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1538,7 +1538,7 @@ LL │ int136 F = int136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1552,7 +1552,7 @@ LL │ int128 H = int128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1566,7 +1566,7 @@ LL │ int120 J = int120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1580,7 +1580,7 @@ LL │ int112 L = int112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1594,7 +1594,7 @@ LL │ int104 N = int104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1608,7 +1608,7 @@ LL │ int96 P = int96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1622,7 +1622,7 @@ LL │ int88 R = int88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1636,7 +1636,7 @@ LL │ int80 T = int80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1650,7 +1650,7 @@ LL │ int72 V = int72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1664,7 +1664,7 @@ LL │ int64 X = int64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1678,7 +1678,7 @@ LL │ int56 Z = int56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1692,7 +1692,7 @@ LL │ int48 BB = int48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1706,7 +1706,7 @@ LL │ int40 DD = int40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1720,7 +1720,7 @@ LL │ int32 FF = int32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1734,7 +1734,7 @@ LL │ int24 HH = int24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1748,7 +1748,7 @@ LL │ int16 JJ = int16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1762,7 +1762,7 @@ LL │ int8 LL = int8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1776,7 +1776,7 @@ LL │ uint256 b = uint256(a); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1790,7 +1790,7 @@ LL │ uint248 d = uint248(c); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1804,7 +1804,7 @@ LL │ uint240 f = uint240(e); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1818,7 +1818,7 @@ LL │ uint232 h = uint232(g); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1832,7 +1832,7 @@ LL │ uint224 j = uint224(i); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1846,7 +1846,7 @@ LL │ uint216 l = uint216(k); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1860,7 +1860,7 @@ LL │ uint208 n = uint208(m); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1874,7 +1874,7 @@ LL │ uint200 p = uint200(o); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1888,7 +1888,7 @@ LL │ uint192 r = uint192(q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1902,7 +1902,7 @@ LL │ uint184 t = uint184(s); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1916,7 +1916,7 @@ LL │ uint176 v = uint176(u); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1930,7 +1930,7 @@ LL │ uint168 x = uint168(w); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1944,7 +1944,7 @@ LL │ uint160 z = uint160(y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1958,7 +1958,7 @@ LL │ uint152 B = uint152(A); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1972,7 +1972,7 @@ LL │ uint144 D = uint144(C); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -1986,7 +1986,7 @@ LL │ uint136 F = uint136(E); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2000,7 +2000,7 @@ LL │ uint128 H = uint128(G); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2014,7 +2014,7 @@ LL │ uint120 J = uint120(I); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2028,7 +2028,7 @@ LL │ uint112 L = uint112(K); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2042,7 +2042,7 @@ LL │ uint104 N = uint104(M); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2056,7 +2056,7 @@ LL │ uint96 P = uint96(O); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2070,7 +2070,7 @@ LL │ uint88 R = uint88(Q); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2084,7 +2084,7 @@ LL │ uint80 T = uint80(S); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2098,7 +2098,7 @@ LL │ uint72 V = uint72(U); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2112,7 +2112,7 @@ LL │ uint64 X = uint64(W); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2126,7 +2126,7 @@ LL │ uint56 Z = uint56(Y); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2140,7 +2140,7 @@ LL │ uint48 BB = uint48(AA); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2154,7 +2154,7 @@ LL │ uint40 DD = uint40(CC); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2168,7 +2168,7 @@ LL │ uint32 FF = uint32(EE); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2182,7 +2182,7 @@ LL │ uint24 HH = uint24(GG); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2196,7 +2196,7 @@ LL │ uint16 JJ = uint16(II); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2210,7 +2210,7 @@ LL │ uint8 LL = uint8(KK); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2224,7 +2224,7 @@ LL │ bytes32 dataSlice = bytes32(data); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2238,7 +2238,7 @@ LL │ bytes32 strSlice = bytes32(bytes(str)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2252,7 +2252,7 @@ LL │ uint128 aPlusB = uint128(int128(uint128(a)) + b); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2266,7 +2266,7 @@ LL │ uint64 unsafe = uint64(aPlusB); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2280,7 +2280,7 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast warning[unsafe-typecast]: typecasts that can truncate values should be checked ╭▸ ROOT/testdata/UnsafeTypecast.sol:LL:CC @@ -2294,5 +2294,5 @@ LL │ return uint64(uint128(int128(uint128(a)) + b)); │ // forge-lint: disable-next-line(unsafe-typecast) │ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unsafe-typecast + ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast diff --git a/crates/lint/testdata/UnusedStateVariables.sol b/crates/lint/testdata/UnusedStateVariables.sol new file mode 100644 index 0000000000000..f68650d2a8d01 --- /dev/null +++ b/crates/lint/testdata/UnusedStateVariables.sol @@ -0,0 +1,52 @@ +//@compile-flags: --severity gas + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract UnusedVars { + uint256 unused; //~NOTE: state variable is never used + uint256 usedInRead; + uint256 usedInWrite; + address usedInBoth; + uint256 constant CONST = 1; // skip constant + uint256 immutable IMMUT; // skip immutable + + constructor() { + usedInBoth = msg.sender; + } + + function read() external view returns (uint256) { + return usedInRead; + } + + function write(uint256 v) external { + usedInWrite = v; + } + + function both() external view returns (address) { + return usedInBoth; + } +} + +// State variables used only as modifier call arguments must not be flagged. +contract UsedInModifierArg { + uint256 limit; + uint256 unused; //~NOTE: state variable is never used + + modifier limitedBy(uint256 max) { + require(msg.value <= max); + _; + } + + function foo() external payable limitedBy(limit) {} +} + +contract MultiUnused { + uint256 firstUnused; //~NOTE: state variable is never used + uint256 secondUnused; //~NOTE: state variable is never used + uint256 usedVar; + + function use() external view returns (uint256) { + return usedVar; + } +} diff --git a/crates/lint/testdata/UnusedStateVariables.stderr b/crates/lint/testdata/UnusedStateVariables.stderr new file mode 100644 index 0000000000000..92a0082bb8293 --- /dev/null +++ b/crates/lint/testdata/UnusedStateVariables.stderr @@ -0,0 +1,40 @@ +note[could-be-immutable]: state variable could be declared immutable + ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC + │ +LL │ address usedInBoth; + │ ━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/could-be-immutable + +note[unused-state-variables]: state variable is never used + ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC + │ +LL │ uint256 unused; + │ ━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables + +note[unused-state-variables]: state variable is never used + ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC + │ +LL │ uint256 unused; + │ ━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables + +note[unused-state-variables]: state variable is never used + ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC + │ +LL │ uint256 firstUnused; + │ ━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables + +note[unused-state-variables]: state variable is never used + ╭▸ ROOT/testdata/UnusedStateVariables.sol:LL:CC + │ +LL │ uint256 secondUnused; + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/unused-state-variables + diff --git a/crates/lint/testdata/UnwrappedModifierLogic.stderr b/crates/lint/testdata/UnwrappedModifierLogic.stderr index 5e1bf754e60e4..dc5514c2d5e98 100644 --- a/crates/lint/testdata/UnwrappedModifierLogic.stderr +++ b/crates/lint/testdata/UnwrappedModifierLogic.stderr @@ -9,7 +9,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforePlaceholder() { @@ -35,7 +35,7 @@ LL │ ┃ checkInternal(msg.sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleAfterPlaceholder() { @@ -62,7 +62,7 @@ LL │ ┃ checkPublic(sender); LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier multipleBeforeAfterPlaceholder(address sender) { @@ -91,7 +91,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwner() { @@ -113,7 +113,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRole(bytes32 role) { @@ -135,7 +135,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrOpenRole(bytes32 role) { @@ -157,7 +157,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyRoleOrAdmin(bytes32 role, address admin) { @@ -180,7 +180,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier assign(address sender) { @@ -204,7 +204,7 @@ LL │ ┃ sender; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier uncheckedBlock(address sender) { @@ -228,7 +228,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier emitEvent(address sender) { @@ -250,7 +250,7 @@ LL │ ┃ _; LL │ ┃ } │ ┗━━━━━┛ │ - ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic + ╰ help: https://getfoundry.sh/forge/linting/unwrapped-modifier-logic help: wrap modifier logic to reduce code size ╭╴ LL ± modifier onlyOwnerContract(address sender) { diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 9970616900e6b..15363a6a40bb0 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -23,10 +23,10 @@ alloy-rpc-types-eth.workspace = true alloy-serde.workspace = true alloy-signer.workspace = true alloy-evm.workspace = true -op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"] } -op-alloy-rpc-types.workspace = true -alloy-op-evm.workspace = true -op-revm.workspace = true +op-alloy-consensus = { workspace = true, features = ["serde", "alloy-compat"], optional = true } +op-alloy-rpc-types = { workspace = true, optional = true } +alloy-op-evm = { workspace = true, optional = true } +op-revm = { workspace = true, optional = true } revm.workspace = true serde_json.workspace = true serde = { version = "1.0", features = ["derive"] } @@ -34,3 +34,12 @@ derive_more.workspace = true tempo-primitives.workspace = true tempo-alloy.workspace = true tempo-revm.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "dep:op-alloy-consensus", + "dep:op-alloy-rpc-types", + "dep:alloy-op-evm", + "dep:op-revm", +] diff --git a/crates/primitives/src/network/mod.rs b/crates/primitives/src/network/mod.rs index 7000b580b6a73..0b840185b0383 100644 --- a/crates/primitives/src/network/mod.rs +++ b/crates/primitives/src/network/mod.rs @@ -1,12 +1,20 @@ use alloy_network::Network; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; use alloy_provider::fillers::{ BlobGasFiller, ChainIdFiller, GasFiller, JoinFill, NonceFiller, RecommendedFillers, }; +#[cfg(feature = "optimism")] +pub use optimism::FoundryTransactionResponse; pub use receipt::*; +/// Default JSON-RPC transaction response when the `optimism` feature is disabled. +#[cfg(not(feature = "optimism"))] +pub type FoundryTransactionResponse = alloy_rpc_types_eth::Transaction; + /// Foundry network type. /// /// This network type supports Foundry-specific transaction types, including @@ -36,7 +44,7 @@ impl Network for FoundryNetwork { type TransactionRequest = crate::FoundryTransactionRequest; - type TransactionResponse = op_alloy_rpc_types::Transaction; + type TransactionResponse = FoundryTransactionResponse; type ReceiptResponse = crate::FoundryTxReceipt; diff --git a/crates/primitives/src/network/optimism.rs b/crates/primitives/src/network/optimism.rs new file mode 100644 index 0000000000000..aff30a755663f --- /dev/null +++ b/crates/primitives/src/network/optimism.rs @@ -0,0 +1,47 @@ +//! OP-stack-specific helpers and type aliases used by [`super::FoundryNetwork`] and +//! [`super::FoundryTxReceipt`]. + +use alloy_consensus::{Receipt, ReceiptWithBloom, TxReceipt}; +use alloy_primitives::U64; +use alloy_rpc_types::Log; +use alloy_serde::OtherFields; +use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; + +use crate::FoundryReceiptEnvelope; + +/// JSON-RPC transaction response type used by [`super::FoundryNetwork`]. +pub type FoundryTransactionResponse = op_alloy_rpc_types::Transaction; + +/// Build a [`FoundryReceiptEnvelope::Deposit`] from a `ReceiptWithBloom` plus the OP +/// deposit-specific fields decoded from the [`OtherFields`] of an `AnyTransactionReceipt`. +pub(super) fn build_deposit_receipt_envelope( + receipt_with_bloom: ReceiptWithBloom>, + other: &OtherFields, +) -> FoundryReceiptEnvelope { + // These fields may not be present in all receipts, so missing/invalid values are None. + let deposit_nonce = other + .get_deserialized::("depositNonce") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + let deposit_receipt_version = other + .get_deserialized::("depositReceiptVersion") + .transpose() + .ok() + .flatten() + .map(|v| v.to::()); + + FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { + status: alloy_consensus::Eip658Value::Eip658(receipt_with_bloom.status()), + cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), + logs: receipt_with_bloom.receipt.logs, + }, + deposit_nonce, + deposit_receipt_version, + }, + logs_bloom: receipt_with_bloom.logs_bloom, + }) +} diff --git a/crates/primitives/src/network/receipt.rs b/crates/primitives/src/network/receipt.rs index b727b4c39b345..6b01f9eaa9ee9 100644 --- a/crates/primitives/src/network/receipt.rs +++ b/crates/primitives/src/network/receipt.rs @@ -1,13 +1,13 @@ -use alloy_consensus::{Receipt, TxReceipt}; use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt, ReceiptResponse}; -use alloy_primitives::{Address, B256, BlockHash, TxHash, U64}; +use alloy_primitives::{Address, B256, BlockHash, TxHash}; use alloy_rpc_types::{ConversionError, Log, TransactionReceipt}; use alloy_serde::WithOtherFields; use derive_more::AsRef; -use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom}; use serde::{Deserialize, Serialize}; use tempo_primitives::TEMPO_TX_TYPE_ID; +#[cfg(feature = "optimism")] +use super::optimism::build_deposit_receipt_envelope; use crate::FoundryReceiptEnvelope; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AsRef)] @@ -144,38 +144,8 @@ impl TryFrom for FoundryTxReceipt { 0x03 => FoundryReceiptEnvelope::Eip4844(receipt_with_bloom), 0x04 => FoundryReceiptEnvelope::Eip7702(receipt_with_bloom), TEMPO_TX_TYPE_ID => FoundryReceiptEnvelope::Tempo(receipt_with_bloom), - 0x7E => { - // Construct the deposit receipt, extracting optional deposit fields - // These fields may not be present in all receipts, so missing/invalid - // values are None - let deposit_nonce = other - .get_deserialized::("depositNonce") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - let deposit_receipt_version = other - .get_deserialized::("depositReceiptVersion") - .transpose() - .ok() - .flatten() - .map(|v| v.to::()); - - FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { - receipt: OpDepositReceipt { - inner: Receipt { - status: alloy_consensus::Eip658Value::Eip658( - receipt_with_bloom.status(), - ), - cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(), - logs: receipt_with_bloom.receipt.logs, - }, - deposit_nonce, - deposit_receipt_version, - }, - logs_bloom: receipt_with_bloom.logs_bloom, - }) - } + #[cfg(feature = "optimism")] + 0x7E => build_deposit_receipt_envelope(receipt_with_bloom, &other), _ => { let tx_type = r#type; return Err(ConversionError::Custom(format!( diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index ab4aafcc294c0..0a009a1931f06 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,6 +1,7 @@ +#[cfg(feature = "optimism")] +use alloy_consensus::{Sealed, Transaction as _}; use alloy_consensus::{ - Sealed, Signed, Transaction as _, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, - TxLegacy, TxType, Typed2718, + Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType, Typed2718, crypto::RecoveryError, transaction::{ SignerRecoverable, TxEip7702, TxHashRef, @@ -9,14 +10,10 @@ use alloy_consensus::{ }; use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; -use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, TxHash}; use alloy_rpc_types::ConversionError; -use op_alloy_consensus::{ - DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, POST_EXEC_TX_TYPE_ID, TxDeposit, - TxPostExec, -}; -use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +#[cfg(feature = "optimism")] +use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit, TxPostExec}; use revm::context::TxEnv; use tempo_primitives::{AASigned, TempoTransaction}; use tempo_revm::TempoTxEnv; @@ -57,9 +54,11 @@ pub enum FoundryTxEnvelope { /// OP stack deposit transaction. /// /// See . + #[cfg(feature = "optimism")] #[envelope(ty = 126)] Deposit(Sealed), /// OP stack post-execution synthetic transaction. + #[cfg(feature = "optimism")] #[envelope(ty = 0x7D)] PostExec(Sealed), /// Tempo transaction type. @@ -80,7 +79,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)), Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)), Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)), + #[cfg(feature = "optimism")] Self::Deposit(_) => Err(self), + #[cfg(feature = "optimism")] Self::PostExec(_) => Err(self), Self::Tempo(_) => Err(self), } @@ -109,7 +110,9 @@ impl FoundryTxEnvelope { Self::Eip1559(t) => *t.hash(), Self::Eip4844(t) => *t.hash(), Self::Eip7702(t) => *t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.tx_hash(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.tx_hash(), Self::Tempo(t) => *t.hash(), } @@ -128,7 +131,9 @@ impl FoundryTxEnvelope { Self::Eip1559(tx) => tx.recover_signer()?, Self::Eip4844(tx) => tx.recover_signer()?, Self::Eip7702(tx) => tx.recover_signer()?, + #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.from, + #[cfg(feature = "optimism")] Self::PostExec(tx) => tx.inner().signer_address(), Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?, }) @@ -143,7 +148,9 @@ impl TxHashRef for FoundryTxEnvelope { Self::Eip1559(t) => t.hash(), Self::Eip4844(t) => t.hash(), Self::Eip7702(t) => t.hash(), + #[cfg(feature = "optimism")] Self::Deposit(t) => t.hash_ref(), + #[cfg(feature = "optimism")] Self::PostExec(t) => t.hash_ref(), Self::Tempo(t) => t.hash(), } @@ -160,23 +167,6 @@ impl SignerRecoverable for FoundryTxEnvelope { } } -impl OpTransactionTrait for FoundryTxEnvelope { - fn is_deposit(&self) -> bool { - matches!(self, Self::Deposit(_)) - } - - fn as_deposit(&self) -> Option<&Sealed> { - match self { - Self::Deposit(tx) => Some(tx), - _ => None, - } - } - - fn as_post_exec(&self) -> Option<&Sealed> { - if let Self::PostExec(tx) = self { Some(tx) } else { None } - } -} - impl TryFrom for TxEnvelope { type Error = FoundryTxEnvelope; @@ -197,19 +187,6 @@ impl From for FoundryTxEnvelope { } } -impl From for FoundryTxEnvelope { - fn from(tx: op_alloy_consensus::OpTxEnvelope) -> Self { - match tx { - op_alloy_consensus::OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), - op_alloy_consensus::OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), - op_alloy_consensus::OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), - op_alloy_consensus::OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), - op_alloy_consensus::OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), - op_alloy_consensus::OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), - } - } -} - impl From for FoundryTxEnvelope { fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self { match tx { @@ -236,33 +213,50 @@ impl TryFrom for FoundryTxEnvelope { TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)), TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)), }, - AnyTxEnvelope::Unknown(mut tx) => { - // Try to convert to deposit transaction - if tx.ty() == DEPOSIT_TX_TYPE_ID { - tx.inner.fields.insert("from".to_string(), serde_json::to_value(from).unwrap()); - let deposit_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize deposit tx: {e}" - )) - })?; - - return Ok(Self::Deposit(Sealed::new(deposit_tx))); + AnyTxEnvelope::Unknown(tx) => { + #[cfg(feature = "optimism")] + { + let mut tx = tx; + let _ = from; + // Try to convert to deposit transaction + if tx.ty() == DEPOSIT_TX_TYPE_ID { + tx.inner + .fields + .insert("from".to_string(), serde_json::to_value(from).unwrap()); + let deposit_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize deposit tx: {e}" + )) + })?; + + return Ok(Self::Deposit(Sealed::new(deposit_tx))); + } + + if tx.ty() == POST_EXEC_TX_TYPE_ID { + let post_exec_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize post-exec tx: {e}" + )) + })?; + + return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + } + + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - if tx.ty() == POST_EXEC_TX_TYPE_ID { - let post_exec_tx = - tx.inner.fields.deserialize_into::().map_err(|e| { - ConversionError::Custom(format!( - "Failed to deserialize post-exec tx: {e}" - )) - })?; - - return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + #[cfg(not(feature = "optimism"))] + { + let _ = from; + let tx_type = tx.ty(); + Err(ConversionError::Custom(format!( + "Unknown transaction type: 0x{tx_type:02X}" + ))) } - - let tx_type = tx.ty(); - Err(ConversionError::Custom(format!("Unknown transaction type: 0x{tx_type:02X}"))) } } } @@ -276,6 +270,7 @@ impl FromRecoveredTx for TxEnv { FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller), FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -288,6 +283,7 @@ impl FromRecoveredTx for TxEnv { ..Default::default() } } + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => { let tx = sealed_tx.inner(); Self { @@ -303,63 +299,6 @@ impl FromRecoveredTx for TxEnv { } } -impl FromRecoveredTx for OpTransaction { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: None, deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: None, deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl FromTxWithEncoded for TxEnv { fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self { Self::from_recovered_tx(tx, sender) @@ -384,7 +323,9 @@ impl FromRecoveredTx for TempoTxEnv { FoundryTxEnvelope::Eip7702(signed_tx) => { Self::from(TxEnv::from_recovered_tx(signed_tx, caller)) } + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"), FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller), } @@ -397,75 +338,6 @@ impl FromTxWithEncoded for TempoTxEnv { } } -impl FromRecoveredTx for OpTx { - fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { - Self(OpTransaction::::from_recovered_tx(tx, caller)) - } -} - -impl FromTxWithEncoded for OpTx { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) - } -} - -impl FromTxWithEncoded for OpTransaction { - fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { - match tx { - FoundryTxEnvelope::Legacy(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip2930(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip1559(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip4844(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Eip7702(signed_tx) => { - let base = TxEnv::from_recovered_tx(signed_tx, caller); - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Deposit(sealed_tx) => { - let deposit_tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: deposit_tx.ty(), - caller, - gas_limit: deposit_tx.gas_limit, - kind: deposit_tx.to, - value: deposit_tx.value, - data: deposit_tx.input.clone(), - ..Default::default() - }; - let deposit = DepositTransactionParts { - source_hash: deposit_tx.source_hash, - mint: Some(deposit_tx.mint), - is_system_transaction: deposit_tx.is_system_transaction, - }; - Self { base, enveloped_tx: Some(encoded), deposit } - } - FoundryTxEnvelope::PostExec(sealed_tx) => { - let tx = sealed_tx.inner(); - let base = TxEnv { - tx_type: tx.ty(), - caller, - kind: tx.kind(), - data: tx.input.clone(), - ..Default::default() - }; - Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } - } - FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), - } - } -} - impl std::fmt::Display for FoundryTxType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -474,7 +346,9 @@ impl std::fmt::Display for FoundryTxType { Self::Eip1559 => write!(f, "eip1559"), Self::Eip4844 => write!(f, "eip4844"), Self::Eip7702 => write!(f, "eip7702"), + #[cfg(feature = "optimism")] Self::Deposit => write!(f, "deposit"), + #[cfg(feature = "optimism")] Self::PostExec => write!(f, "post-exec"), Self::Tempo => write!(f, "tempo"), } @@ -501,7 +375,9 @@ impl From for FoundryTypedTx { FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()), FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()), FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()), + #[cfg(feature = "optimism")] FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.into_inner()), FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()), } @@ -609,28 +485,6 @@ mod tests { assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2")); } - #[test] - fn test_decode_encode_deposit_tx() { - // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" - .parse::() - .unwrap(); - - // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 - let raw_tx = alloy_primitives::hex::decode( - "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", - ) - .unwrap(); - let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); - - let mut encoded = Vec::new(); - dep_tx.encode_2718(&mut encoded); - - assert_eq!(raw_tx, encoded); - - assert_eq!(tx_hash, dep_tx.hash()); - } - #[test] fn can_recover_sender_not_normalized() { let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap(); @@ -707,11 +561,6 @@ mod tests { assert_eq!(tx_env.caller, sender); assert_eq!(tx_env.gas_limit, 0x5208); assert_eq!(tx_env.gas_price, 1); - - // Test OpTransaction conversion via FromRecoveredTx trait - let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); - assert_eq!(op_tx.base.caller, sender); - assert_eq!(op_tx.base.gas_limit, 0x5208); } // Test vector from Tempo testnet: diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 7dccf3e30752f..18f39c437bfbc 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1,7 +1,11 @@ mod envelope; +#[cfg(feature = "optimism")] +mod optimism; mod receipt; mod request; pub use envelope::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; +#[cfg(feature = "optimism")] +pub use optimism::get_deposit_tx_parts; pub use receipt::FoundryReceiptEnvelope; -pub use request::{FoundryTransactionRequest, get_deposit_tx_parts}; +pub use request::FoundryTransactionRequest; diff --git a/crates/primitives/src/transaction/optimism.rs b/crates/primitives/src/transaction/optimism.rs new file mode 100644 index 0000000000000..5952b5d6a9020 --- /dev/null +++ b/crates/primitives/src/transaction/optimism.rs @@ -0,0 +1,300 @@ +//! OP-stack-specific impls for [`FoundryTxEnvelope`] and [`FoundryTransactionRequest`]. + +use alloy_consensus::{Sealed, Transaction as _, Typed2718}; +use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::OpTx; +use alloy_primitives::{Address, B256, Bytes, U256}; +use alloy_serde::OtherFields; +use op_alloy_consensus::{ + OpDepositReceipt, OpDepositReceiptWithBloom, OpTransaction as OpTransactionTrait, OpTxEnvelope, + TxDeposit, TxPostExec, +}; +use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; +use revm::context::TxEnv; + +use super::{FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope}; + +impl OpTransactionTrait for FoundryTxEnvelope { + fn is_deposit(&self) -> bool { + matches!(self, Self::Deposit(_)) + } + + fn as_deposit(&self) -> Option<&Sealed> { + match self { + Self::Deposit(tx) => Some(tx), + _ => None, + } + } + + fn as_post_exec(&self) -> Option<&Sealed> { + if let Self::PostExec(tx) = self { Some(tx) } else { None } + } +} + +impl From for FoundryTxEnvelope { + fn from(tx: OpTxEnvelope) -> Self { + match tx { + OpTxEnvelope::Legacy(tx) => Self::Legacy(tx), + OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx), + OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), + OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), + OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), + OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), + } + } +} + +impl FromRecoveredTx for OpTransaction { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: None, deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: None, deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl FromRecoveredTx for OpTx { + fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self { + Self(OpTransaction::::from_recovered_tx(tx, caller)) + } +} + +impl FromTxWithEncoded for OpTx { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + Self(OpTransaction::::from_encoded_tx(tx, caller, encoded)) + } +} + +impl FromTxWithEncoded for OpTransaction { + fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self { + match tx { + FoundryTxEnvelope::Legacy(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip2930(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip1559(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip4844(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Eip7702(signed_tx) => { + let base = TxEnv::from_recovered_tx(signed_tx, caller); + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Deposit(sealed_tx) => { + let deposit_tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: deposit_tx.ty(), + caller, + gas_limit: deposit_tx.gas_limit, + kind: deposit_tx.to, + value: deposit_tx.value, + data: deposit_tx.input.clone(), + ..Default::default() + }; + let deposit = DepositTransactionParts { + source_hash: deposit_tx.source_hash, + mint: Some(deposit_tx.mint), + is_system_transaction: deposit_tx.is_system_transaction, + }; + Self { base, enveloped_tx: Some(encoded), deposit } + } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } + FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), + } + } +} + +impl From> for FoundryTransactionRequest { + fn from(tx: op_alloy_rpc_types::Transaction) -> Self { + tx.inner.into_inner().into() + } +} + +/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields. +pub fn get_deposit_tx_parts( + other: &OtherFields, +) -> Result> { + let mut missing = Vec::new(); + let source_hash = + other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("sourceHash"); + Default::default() + }, + ); + let mint = other + .get_deserialized::("mint") + .transpose() + .unwrap_or_else(|_| { + missing.push("mint"); + Default::default() + }) + .map(|value| value.to::()); + let is_system_transaction = + other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( + || { + missing.push("isSystemTx"); + Default::default() + }, + ); + if missing.is_empty() { + Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) + } else { + Err(missing) + } +} + +/// OP-stack-specific accessors on [`FoundryReceiptEnvelope`]. +impl FoundryReceiptEnvelope { + /// Return the receipt's deposit_nonce if it is a deposit receipt. + pub fn deposit_nonce(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_nonce) + } + + /// Return the receipt's deposit version if it is a deposit receipt. + pub fn deposit_receipt_version(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { + match self { + Self::Deposit(t) => Some(t), + _ => None, + } + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { + match self { + Self::Deposit(t) => Some(&t.receipt), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use alloy_network::eip2718::Encodable2718; + use alloy_primitives::TxHash; + use alloy_rlp::Decodable; + + use super::*; + + #[test] + fn test_from_recovered_tx_legacy_op() { + use alloy_consensus::transaction::SignerRecoverable; + + let tx = r#" + { + "type": "0x0", + "chainId": "0x1", + "nonce": "0x0", + "gas": "0x5208", + "gasPrice": "0x1", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "value": "0x1", + "input": "0x", + "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0", + "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd", + "v": "0x1b", + "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515" + }"#; + + let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap(); + let sender = typed_tx.recover_signer().unwrap(); + + // Test OpTransaction conversion via FromRecoveredTx trait + let op_tx = OpTransaction::::from_recovered_tx(&typed_tx, sender); + assert_eq!(op_tx.base.caller, sender); + assert_eq!(op_tx.base.gas_limit, 0x5208); + } + + #[test] + fn test_decode_encode_deposit_tx() { + // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7" + .parse::() + .unwrap(); + + // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7 + let raw_tx = alloy_primitives::hex::decode( + "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080", + ) + .unwrap(); + let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); + + let mut encoded = Vec::new(); + dep_tx.encode_2718(&mut encoded); + + assert_eq!(raw_tx, encoded); + + assert_eq!(tx_hash, dep_tx.hash()); + } +} diff --git a/crates/primitives/src/transaction/receipt.rs b/crates/primitives/src/transaction/receipt.rs index fe209fc72c907..78bbcd1efa2fb 100644 --- a/crates/primitives/src/transaction/receipt.rs +++ b/crates/primitives/src/transaction/receipt.rs @@ -8,6 +8,7 @@ use alloy_network::eip2718::{ use alloy_primitives::{Bloom, Log, TxHash, logs_bloom}; use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes}; use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{ DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID, }; @@ -29,8 +30,10 @@ pub enum FoundryReceiptEnvelope { Eip4844(ReceiptWithBloom>), #[serde(rename = "0x4", alias = "0x04")] Eip7702(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7D", alias = "0x7d")] PostExec(ReceiptWithBloom>), + #[cfg(feature = "optimism")] #[serde(rename = "0x7E", alias = "0x7e")] Deposit(OpDepositReceiptWithBloom), #[serde(rename = "0x76")] @@ -44,7 +47,8 @@ impl FoundryReceiptEnvelope { cumulative_gas_used: u64, logs: impl IntoIterator, tx_type: FoundryTxType, - deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_nonce: Option, + #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_receipt_version: Option, ) -> Self { let logs = logs.into_iter().collect::>(); @@ -67,9 +71,11 @@ impl FoundryReceiptEnvelope { FoundryTxType::Eip7702 => { Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::PostExec => { Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + #[cfg(feature = "optimism")] FoundryTxType::Deposit => { let inner = OpDepositReceiptWithBloom { receipt: OpDepositReceipt { @@ -112,13 +118,18 @@ impl FoundryReceiptEnvelope { removed: false, }) .collect::>(); + #[cfg(feature = "optimism")] + let (deposit_nonce, deposit_receipt_version) = + (self.deposit_nonce(), self.deposit_receipt_version()); + #[cfg(not(feature = "optimism"))] + let (deposit_nonce, deposit_receipt_version) = (None, None); FoundryReceiptEnvelope::::from_parts( self.status(), self.cumulative_gas_used(), logs, self.tx_type(), - self.deposit_nonce(), - self.deposit_receipt_version(), + deposit_nonce, + deposit_receipt_version, ) } } @@ -132,7 +143,9 @@ impl FoundryReceiptEnvelope { Self::Eip1559(_) => FoundryTxType::Eip1559, Self::Eip4844(_) => FoundryTxType::Eip4844, Self::Eip7702(_) => FoundryTxType::Eip7702, + #[cfg(feature = "optimism")] Self::PostExec(_) => FoundryTxType::PostExec, + #[cfg(feature = "optimism")] Self::Deposit(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -158,8 +171,12 @@ impl FoundryReceiptEnvelope { Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)), Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)), Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)), + #[cfg(feature = "optimism")] Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)), - Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(r.map_receipt(|r| r.map_logs(f))), + #[cfg(feature = "optimism")] + Self::Deposit(r) => FoundryReceiptEnvelope::Deposit( + r.map_receipt(|r: OpDepositReceipt| r.map_logs(f)), + ), Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)), } } @@ -182,38 +199,14 @@ impl FoundryReceiptEnvelope { Self::Eip1559(t) => &t.logs_bloom, Self::Eip4844(t) => &t.logs_bloom, Self::Eip7702(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::PostExec(t) => &t.logs_bloom, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.logs_bloom, Self::Tempo(t) => &t.logs_bloom, } } - /// Return the receipt's deposit_nonce if it is a deposit receipt. - pub fn deposit_nonce(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_nonce) - } - - /// Return the receipt's deposit version if it is a deposit receipt. - pub fn deposit_receipt_version(&self) -> Option { - self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { - match self { - Self::Deposit(t) => Some(t), - _ => None, - } - } - - /// Returns the deposit receipt if it is a deposit receipt. - pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { - match self { - Self::Deposit(t) => Some(&t.receipt), - _ => None, - } - } - /// Consumes the type and returns the underlying [`Receipt`]. pub fn into_receipt(self) -> Receipt { match self { @@ -222,8 +215,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => t.receipt.into_inner(), } } @@ -236,8 +231,10 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) - | Self::PostExec(t) | Self::Tempo(t) => &t.receipt, + #[cfg(feature = "optimism")] + Self::PostExec(t) => &t.receipt, + #[cfg(feature = "optimism")] Self::Deposit(t) => &t.receipt.inner, } } @@ -287,7 +284,9 @@ impl Encodable for FoundryReceiptEnvelope { Self::Eip1559(r) => r.length() + 1, Self::Eip4844(r) => r.length() + 1, Self::Eip7702(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::PostExec(r) => r.length() + 1, + #[cfg(feature = "optimism")] Self::Deposit(r) => r.length() + 1, Self::Tempo(r) => r.length() + 1, _ => unreachable!("receipt already matched"), @@ -314,11 +313,13 @@ impl Encodable for FoundryReceiptEnvelope { EIP7702_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::PostExec(r) => { Header { list: true, payload_length: payload_len }.encode(out); POST_EXEC_TX_TYPE_ID.encode(out); r.encode(out); } + #[cfg(feature = "optimism")] Self::Deposit(r) => { Header { list: true, payload_length: payload_len }.encode(out); DEPOSIT_TX_TYPE_ID.encode(out); @@ -371,18 +372,23 @@ impl Decodable for FoundryReceiptEnvelope { buf.advance(1); ::decode(buf) .map(FoundryReceiptEnvelope::Eip7702) - } else if receipt_type == POST_EXEC_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::PostExec) - } else if receipt_type == DEPOSIT_TX_TYPE_ID { - buf.advance(1); - ::decode(buf) - .map(FoundryReceiptEnvelope::Deposit) } else if receipt_type == TEMPO_TX_TYPE_ID { buf.advance(1); ::decode(buf).map(FoundryReceiptEnvelope::Tempo) } else { + #[cfg(feature = "optimism")] + { + if receipt_type == POST_EXEC_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::PostExec); + } + if receipt_type == DEPOSIT_TX_TYPE_ID { + buf.advance(1); + return ::decode(buf) + .map(FoundryReceiptEnvelope::Deposit); + } + } Err(alloy_rlp::Error::Custom("invalid receipt type")) } } @@ -404,7 +410,9 @@ impl Typed2718 for FoundryReceiptEnvelope { Self::Eip1559(_) => EIP1559_TX_TYPE_ID, Self::Eip4844(_) => EIP4844_TX_TYPE_ID, Self::Eip7702(_) => EIP7702_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::PostExec(_) => POST_EXEC_TX_TYPE_ID, + #[cfg(feature = "optimism")] Self::Deposit(_) => DEPOSIT_TX_TYPE_ID, Self::Tempo(_) => TEMPO_TX_TYPE_ID, } @@ -419,7 +427,9 @@ impl Encodable2718 for FoundryReceiptEnvelope { Self::Eip1559(r) => 1 + r.length(), Self::Eip4844(r) => 1 + r.length(), Self::Eip7702(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::PostExec(r) => 1 + r.length(), + #[cfg(feature = "optimism")] Self::Deposit(r) => 1 + r.length(), Self::Tempo(r) => 1 + r.length(), } @@ -435,8 +445,10 @@ impl Encodable2718 for FoundryReceiptEnvelope { | Self::Eip1559(r) | Self::Eip4844(r) | Self::Eip7702(r) - | Self::PostExec(r) | Self::Tempo(r) => r.encode(out), + #[cfg(feature = "optimism")] + Self::PostExec(r) => r.encode(out), + #[cfg(feature = "optimism")] Self::Deposit(r) => r.encode(out), } } @@ -444,15 +456,18 @@ impl Encodable2718 for FoundryReceiptEnvelope { impl Decodable2718 for FoundryReceiptEnvelope { fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { - if ty == DEPOSIT_TX_TYPE_ID { - return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + #[cfg(feature = "optimism")] + { + if ty == DEPOSIT_TX_TYPE_ID { + return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?)); + } + if ty == POST_EXEC_TX_TYPE_ID { + return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); + } } if ty == TEMPO_TX_TYPE_ID { return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?)); } - if ty == POST_EXEC_TX_TYPE_ID { - return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); - } match ReceiptEnvelope::typed_decode(ty, buf)? { ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)), ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)), @@ -646,8 +661,11 @@ mod tests { assert!(receipt.status()); assert_eq!(receipt.cumulative_gas_used(), 100000); assert!(receipt.logs().is_empty()); - assert!(receipt.deposit_nonce().is_none()); - assert!(receipt.deposit_receipt_version().is_none()); + #[cfg(feature = "optimism")] + { + assert!(receipt.deposit_nonce().is_none()); + assert!(receipt.deposit_receipt_version().is_none()); + } } #[test] diff --git a/crates/primitives/src/transaction/request.rs b/crates/primitives/src/transaction/request.rs index 2c4dbae8fdcd4..8ae31efbd5cb1 100644 --- a/crates/primitives/src/transaction/request.rs +++ b/crates/primitives/src/transaction/request.rs @@ -3,15 +3,19 @@ use alloy_network::{ BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder, TransactionBuilder4844, TransactionBuilderError, }; -use alloy_primitives::{Address, B256, ChainId, TxKind, U256}; +use alloy_primitives::{Address, ChainId, TxKind, U256}; use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest}; use alloy_serde::{OtherFields, WithOtherFields}; +#[cfg(feature = "optimism")] use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit}; +#[cfg(feature = "optimism")] use op_revm::transaction::deposit::DepositTransactionParts; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionRequest; use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType}; +#[cfg(feature = "optimism")] +use super::optimism::get_deposit_tx_parts; use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; use crate::FoundryNetwork; @@ -28,6 +32,7 @@ use crate::FoundryNetwork; #[derive(Clone, Debug, PartialEq, Eq)] pub enum FoundryTransactionRequest { Ethereum(TransactionRequest), + #[cfg(feature = "optimism")] Op(WithOtherFields), Tempo(Box), } @@ -44,6 +49,7 @@ impl FoundryTransactionRequest { pub fn into_inner(self) -> TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx.inner, Self::Tempo(tx) => tx.inner, } @@ -55,6 +61,7 @@ impl FoundryTransactionRequest { /// # Returns /// - Ok(deposit_tx_parts) if all necessary keys are present to build a deposit transaction. /// - Err(missing) if some keys are missing to build a deposit transaction. + #[cfg(feature = "optimism")] pub fn get_deposit_tx_parts(&self) -> Result> { match self { Self::Op(tx) => get_deposit_tx_parts(&tx.other), @@ -69,9 +76,11 @@ impl FoundryTransactionRequest { pub fn preferred_type(&self) -> FoundryTxType { match self { Self::Ethereum(tx) => tx.preferred_type().into(), + #[cfg(feature = "optimism")] Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => { FoundryTxType::PostExec } + #[cfg(feature = "optimism")] Self::Op(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -95,6 +104,7 @@ impl FoundryTransactionRequest { /// Check if all necessary keys are present to build a Deposit transaction, returning a list of /// keys that are missing. + #[cfg(feature = "optimism")] pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> { self.get_deposit_tx_parts().map(|_| ()) } @@ -123,7 +133,9 @@ impl FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } { @@ -138,9 +150,10 @@ impl FoundryTransactionRequest { /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction /// types. pub fn build_typed_tx(self) -> Result { + #[cfg(feature = "optimism")] if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() { // Build deposit transaction - Ok(FoundryTypedTx::Deposit(TxDeposit { + return Ok(FoundryTypedTx::Deposit(TxDeposit { from: self.from().unwrap_or_default(), source_hash: deposit_tx_parts.source_hash, to: self.kind().unwrap_or_default(), @@ -149,8 +162,9 @@ impl FoundryTransactionRequest { gas_limit: self.gas_limit().unwrap_or_default(), is_system_transaction: deposit_tx_parts.is_system_transaction, input: self.input().cloned().unwrap_or_default(), - })) - } else if self.complete_tempo().is_ok() + })); + } + if self.complete_tempo().is_ok() && let Self::Tempo(tx_req) = self { // Build Tempo transaction @@ -192,6 +206,7 @@ impl Serialize for FoundryTransactionRequest { { match self { Self::Ethereum(tx) => tx.serialize(serializer), + #[cfg(feature = "optimism")] Self::Op(tx) => tx.serialize(serializer), Self::Tempo(tx) => tx.serialize(serializer), } @@ -211,6 +226,7 @@ impl AsRef for FoundryTransactionRequest { fn as_ref(&self) -> &TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_ref(), } @@ -221,6 +237,7 @@ impl AsMut for FoundryTransactionRequest { fn as_mut(&mut self) -> &mut TransactionRequest { match self { Self::Ethereum(tx) => tx, + #[cfg(feature = "optimism")] Self::Op(tx) => tx, Self::Tempo(tx) => tx.as_mut(), } @@ -244,15 +261,16 @@ impl From> for FoundryTransactionRequest { { tempo_tx_req.set_nonce_key(nonce_key); } - Self::Tempo(Box::new(tempo_tx_req)) - } else if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) + return Self::Tempo(Box::new(tempo_tx_req)); + } + #[cfg(feature = "optimism")] + if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID) || get_deposit_tx_parts(&tx.other).is_ok() { - Self::Op(tx) - } else { - Self::Ethereum(tx.into_inner()) + return Self::Op(tx); } + Self::Ethereum(tx.into_inner()) } } @@ -264,6 +282,7 @@ impl From for FoundryTransactionRequest { FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::::into(tx)), + #[cfg(feature = "optimism")] FoundryTypedTx::Deposit(tx) => { let other = OtherFields::from_iter([ ("sourceHash", tx.source_hash.to_string().into()), @@ -272,6 +291,7 @@ impl From for FoundryTransactionRequest { ]); WithOtherFields { inner: Into::::into(tx), other }.into() } + #[cfg(feature = "optimism")] FoundryTypedTx::PostExec(tx) => WithOtherFields { inner: Into::::into(tx), other: OtherFields::default(), @@ -307,8 +327,9 @@ impl From for FoundryTransactionRequest { } } -impl From> for FoundryTransactionRequest { - fn from(tx: op_alloy_rpc_types::Transaction) -> Self { +#[cfg(not(feature = "optimism"))] +impl From> for FoundryTransactionRequest { + fn from(tx: alloy_rpc_types_eth::Transaction) -> Self { tx.inner.into_inner().into() } } @@ -437,7 +458,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559(), FoundryTxType::Eip4844 => self.as_ref().complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } @@ -448,9 +471,14 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { } fn can_build(&self) -> bool { - self.as_ref().can_build() - || self.complete_deposit().is_ok() - || self.complete_tempo().is_ok() + if self.as_ref().can_build() || self.complete_tempo().is_ok() { + return true; + } + #[cfg(feature = "optimism")] + if self.complete_deposit().is_ok() { + return true; + } + false } fn output_tx_type(&self) -> FoundryTxType { @@ -465,7 +493,9 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(), FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), + #[cfg(feature = "optimism")] FoundryTxType::Deposit => self.complete_deposit().ok(), + #[cfg(feature = "optimism")] FoundryTxType::PostExec => self.complete_type(pref).ok(), FoundryTxType::Tempo => self.complete_tempo().ok(), }?; @@ -479,11 +509,21 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { let inner = self.as_mut(); inner.transaction_type = Some(preferred_type as u8); inner.gas.is_none().then(|| inner.set_gas_limit(Default::default())); - if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) { + let is_deposit = { + #[cfg(feature = "optimism")] + { + preferred_type == FoundryTxType::Deposit + } + #[cfg(not(feature = "optimism"))] + { + false + } + }; + if !is_deposit && preferred_type != FoundryTxType::Tempo { inner.trim_conflicting_keys(); inner.populate_blob_hashes(); } - if preferred_type != FoundryTxType::Deposit { + if !is_deposit { inner.nonce.is_none().then(|| inner.set_nonce(Default::default())); } if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) { @@ -548,42 +588,10 @@ impl TransactionBuilder4844 for FoundryTransactionRequest { } } -/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields -pub fn get_deposit_tx_parts( - other: &OtherFields, -) -> Result> { - let mut missing = Vec::new(); - let source_hash = - other.get_deserialized::("sourceHash").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("sourceHash"); - Default::default() - }, - ); - let mint = other - .get_deserialized::("mint") - .transpose() - .unwrap_or_else(|_| { - missing.push("mint"); - Default::default() - }) - .map(|value| value.to::()); - let is_system_transaction = - other.get_deserialized::("isSystemTx").transpose().ok().flatten().unwrap_or_else( - || { - missing.push("isSystemTx"); - Default::default() - }, - ); - if missing.is_empty() { - Ok(DepositTransactionParts { source_hash, mint, is_system_transaction }) - } else { - Err(missing) - } -} - #[cfg(test)] mod tests { + use alloy_primitives::B256; + use super::*; fn default_tx_req() -> TransactionRequest { @@ -618,6 +626,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_routing_op_by_deposit_fields() { let tx = default_tx_req(); let mut other = OtherFields::default(); @@ -669,6 +678,7 @@ mod tests { } #[test] + #[cfg(feature = "optimism")] fn test_serialization_op() { let tx = default_tx_req(); let mut other = OtherFields::default(); diff --git a/crates/script-sequence/Cargo.toml b/crates/script-sequence/Cargo.toml index 7f112ce1bbda8..ce94945fb27cf 100644 --- a/crates/script-sequence/Cargo.toml +++ b/crates/script-sequence/Cargo.toml @@ -27,3 +27,7 @@ revm-inspectors.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 332420d0d1c42..d243814c4b148 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -63,3 +63,15 @@ tempo-primitives.workspace = true [dev-dependencies] tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-common/optimism", + "foundry-cheatcodes/optimism", + "foundry-cli/optimism", + "forge-script-sequence/optimism", + "forge-verify/optimism", +] diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 5ac7e4c669ade..f1bda2ccdcf55 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -1,8 +1,8 @@ -use std::{cmp::Ordering, sync::Arc, time::Duration}; +use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration}; use crate::{ - ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress, - sequence::ScriptSequenceKind, verify::BroadcastedState, + ScriptArgs, ScriptConfig, build::LinkedBuildData, needs_script_rpc_estimate, + progress::ScriptProgress, sequence::ScriptSequenceKind, verify::BroadcastedState, }; use alloy_chains::{Chain, NamedChain}; use alloy_consensus::{SignableTransaction, Signed}; @@ -21,11 +21,12 @@ use alloy_signer::Signature; use eyre::{Context, Result, bail}; use forge_verify::provider::VerificationProviderType; use foundry_cheatcodes::Wallets; -use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; +use foundry_cli::utils::has_batch_support; use foundry_common::{ FoundryTransactionBuilder, TransactionMaybeSigned, provider::{ProviderBuilder, try_get_http_provider}, shell, + tempo::TempoSponsor, }; use foundry_config::Config; use foundry_evm::core::evm::{FoundryEvmNetwork, TempoEvmNetwork}; @@ -100,7 +101,17 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result<()> { + let access_key_authorization = match self { + Self::AccessKey(_, _, access_key) => Some(( + access_key.wallet_address, + access_key.key_address, + access_key.key_authorization.clone(), + )), + _ => None, + }; + if let Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) @@ -137,11 +148,28 @@ where } } + if let Some((wallet_address, key_address, key_authorization)) = + access_key_authorization.as_ref() + { + tx.prepare_access_key_authorization( + provider, + *wallet_address, + *key_address, + key_authorization.as_ref(), + ) + .await?; + } + // Chains which use `eth_estimateGas` are being sent sequentially and require their // gas to be re-estimated right before broadcasting. if !is_fixed_gas_limit && estimate_via_rpc { estimate_gas(tx, provider, estimate_multiplier).await?; } + + if let Some(sponsor) = tempo_sponsor { + let from = tx.from().expect("no sender"); + sponsor.attach_and_print::(tx, from).await?; + } } Ok(()) @@ -211,6 +239,7 @@ where is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, + tempo_sponsor: Option<&TempoSponsor>, ) -> Result { self.prepare( &provider, @@ -218,6 +247,7 @@ where is_fixed_gas_limit, estimate_via_rpc, estimate_multiplier, + tempo_sponsor, ) .await?; @@ -387,6 +417,27 @@ impl BundledState { SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys } }; + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new); + if tempo_sponsor.is_some() && self.script_config.tempo.sponsor_sig.is_some() { + let remaining = self + .sequence + .sequences() + .iter() + .map(|sequence| { + sequence + .transactions() + .skip(sequence.receipts.len()) + .filter(|tx| tx.is_unsigned()) + .count() + }) + .sum::(); + if remaining > 1 { + eyre::bail!( + "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts" + ); + } + } + let progress = ScriptProgress::default(); for i in 0..self.sequence.sequences().len() { @@ -464,6 +515,11 @@ impl BundledState { let kind = match tx_with_metadata.tx().clone() { TransactionMaybeSigned::Signed { tx, .. } => { + if tempo_sponsor.is_some() { + eyre::bail!( + "cannot attach Tempo sponsor signature to an already signed script transaction" + ); + } SendTransactionKind::Signed(tx) } TransactionMaybeSigned::Unsigned(mut tx) => { @@ -487,6 +543,8 @@ impl BundledState { tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); } + self.script_config.tempo.apply::(&mut tx, None); + send_kind.for_sender(&from, tx)? } }; @@ -495,9 +553,13 @@ impl BundledState { }) .collect::>>()?; - let estimate_via_rpc = has_different_gas_calc(sequence.chain) - || self.script_config.evm_opts.networks.is_tempo() - || self.args.skip_simulation; + let estimate_via_rpc = needs_script_rpc_estimate( + sequence.chain, + self.script_config.evm_opts.networks.is_tempo(), + self.script_config.batch, + &self.script_config.tempo, + self.args.skip_simulation, + ); // We only wait for a transaction receipt before sending the next transaction, if // there is more than one signer. There would be no way of assuring @@ -525,6 +587,7 @@ impl BundledState { let pending_transactions = batch.iter().map(|(kind, is_fixed_gas_limit)| { let provider = provider.clone(); + let tempo_sponsor = tempo_sponsor.clone(); async move { let res = kind .clone() @@ -534,22 +597,36 @@ impl BundledState { *is_fixed_gas_limit, estimate_via_rpc, self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), ) .await; - (res, kind, 0, None) + (res, kind, *is_fixed_gas_limit, 0, None) } .boxed() }); let mut buffer = pending_transactions.collect::>(); - 'send: while let Some((res, kind, attempt, original_res)) = - buffer.next().await + 'send: while let Some(( + res, + kind, + is_fixed_gas_limit, + attempt, + original_res, + )) = buffer.next().await { - if res.is_err() && attempt <= 3 { + if res.is_err() + && self.script_config.tempo.sponsor_sig.is_some() + && attempt == 0 + { + debug!( + "not retrying transaction because --tempo.sponsor-sig is a static signature" + ); + } else if res.is_err() && attempt <= 3 { // Try to resubmit the transaction let provider = provider.clone(); let progress = seq_progress.inner.clone(); + let tempo_sponsor = tempo_sponsor.clone(); buffer.push(Box::pin(async move { debug!(err=?res, ?attempt, "retrying transaction "); let attempt = attempt + 1; @@ -557,8 +634,24 @@ impl BundledState { "retrying transaction {res:?} (attempt {attempt})" )); tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - let r = kind.clone().send(provider).await; - (r, kind, attempt, original_res.or(Some(res))) + let r = kind + .clone() + .prepare_and_send( + provider, + sequential_broadcast, + is_fixed_gas_limit, + estimate_via_rpc, + self.args.gas_estimate_multiplier, + tempo_sponsor.as_deref(), + ) + .await; + ( + r, + kind, + is_fixed_gas_limit, + attempt, + original_res.or(Some(res)), + ) })); continue 'send; @@ -675,6 +768,7 @@ impl BundledState { let sequence = self.sequence.sequences_mut().get_mut(0).unwrap(); let provider = Arc::new(ProviderBuilder::::new(sequence.rpc_url()).build()?); + let tempo_sponsor = self.script_config.tempo.sponsor_config().await?; // Collect sender addresses - batch mode requires single sender let senders: AddressHashSet = sequence @@ -794,16 +888,35 @@ impl BundledState { max_priority_fee_per_gas: Some(max_priority_fee_per_gas), ..Default::default() }, - fee_token: self.script_config.fee_token, + fee_token: self.script_config.tempo.common.fee_token, calls: calls.clone(), + nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX), + valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new), ..Default::default() }; + self.script_config.tempo.apply::(&mut batch_tx, None); + + if let BatchSigner::TempoKeychain(_, ak) = &batch_signer { + batch_tx.key_id = Some(ak.key_address); + batch_tx + .prepare_access_key_authorization( + provider.as_ref(), + ak.wallet_address, + ak.key_address, + ak.key_authorization.as_ref(), + ) + .await?; + } // Estimate gas for the batch transaction estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier).await?; sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?; + if let Some(sponsor) = &tempo_sponsor { + sponsor.attach_and_print::(&mut batch_tx, sender).await?; + } + // Sign and send let tx_hash = match batch_signer { BatchSigner::Wallet(wallet) => { @@ -816,8 +929,6 @@ impl BundledState { *pending.tx_hash() } BatchSigner::TempoKeychain(signer, access_key) => { - batch_tx.key_id = Some(access_key.key_address); - let raw_tx = batch_tx .sign_with_access_key( provider.as_ref(), diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index f5e4d46de0344..ea906c9b872e1 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -28,8 +28,8 @@ use eyre::{ContextCompat, Result}; use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::{RetryArgs, VerifierArgs}; use foundry_cli::{ - opts::{BuildOpts, EvmArgs, GlobalArgs}, - utils::{LoadConfig, parse_fee_token_address}, + opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts}, + utils::{LoadConfig, has_different_gas_calc}, }; use foundry_common::{ CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN, @@ -44,11 +44,13 @@ use foundry_config::{ value::{Dict, Map}, }, }; +#[cfg(feature = "optimism")] +use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ backend::Backend, core::{ Breakpoints, FoundryTransaction, - evm::{EthEvmNetwork, FoundryEvmNetwork, OpEvmNetwork, TempoEvmNetwork, TxEnvFor}, + evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor}, tempo::PATH_USD_ADDRESS, }, executors::ExecutorBuilder, @@ -140,9 +142,9 @@ pub struct ScriptArgs { #[arg(long, requires = "batch", default_value = "100")] pub batch_size: usize, - /// Tempo fee token address for paying transaction fees. - #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)] - pub fee_token: Option

, + /// Tempo transaction options. + #[command(flatten)] + pub tempo: TempoOpts, /// Skips on-chain simulation. #[arg(long)] @@ -246,13 +248,43 @@ pub struct ScriptArgs { pub retry: RetryArgs, } +const fn should_default_tempo_fee_token( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + // Plain `--network tempo` should stay an ordinary transaction; only Tempo AA opts get defaults. + is_tempo_network && tempo.common.fee_token.is_none() && (batch || tempo.is_tempo()) +} + +const fn needs_tempo_aa_rpc_estimate( + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, +) -> bool { + is_tempo_network && (batch || tempo.is_tempo()) +} + +pub(crate) fn needs_script_rpc_estimate( + chain_id: u64, + is_tempo_network: bool, + batch: bool, + tempo: &TempoOpts, + skip_simulation: bool, +) -> bool { + // Tempo AA needs RPC estimation; plain Tempo scripts can use the local simulation result. + (has_different_gas_calc(chain_id) && !is_tempo_network) + || needs_tempo_aa_rpc_estimate(is_tempo_network, batch, tempo) + || skip_simulation +} + impl ScriptArgs { /// Loads config, resolves evm_opts (including network inference from fork), and returns them. async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> { let (config, mut evm_opts) = self.load_config_and_evm_opts()?; - if self.fee_token.is_some() { - // If fee token is set directly select tempo + if self.tempo.is_tempo() { + // If fee token or expiry is set directly select tempo evm_opts.networks = NetworkConfigs::with_tempo(); } else { // Auto-detect network from fork chain ID when not explicitly configured. @@ -285,13 +317,14 @@ impl ScriptArgs { } } - let fee_token = if evm_opts.networks.is_tempo() && self.fee_token.is_none() { - Some(PATH_USD_ADDRESS) - } else { - self.fee_token - }; + let mut tempo = self.tempo.clone(); + tempo.resolve_expires(); + + if should_default_tempo_fee_token(evm_opts.networks.is_tempo(), self.batch, &tempo) { + tempo.common.fee_token = Some(PATH_USD_ADDRESS); + } - let script_config = ScriptConfig::new(config, evm_opts, self.batch, fee_token).await?; + let script_config = ScriptConfig::new(config, evm_opts, self.batch, tempo).await?; Ok(PreprocessedState { args: self, script_config, script_wallets, browser_wallet }) } @@ -320,12 +353,15 @@ impl ScriptArgs { if broadcasted.args.verify { broadcasted.verify().await?; } - Ok(()) - } else if evm_opts.networks.is_optimism() { - self.run_generic_script::(config, evm_opts).await - } else { - self.run_generic_script::(config, evm_opts).await + return Ok(()); } + + #[cfg(feature = "optimism")] + if evm_opts.networks.is_optimism() { + return self.run_generic_script::(config, evm_opts).await; + } + + self.run_generic_script::(config, evm_opts).await } /// Prepares the bundled state (compile, simulate, bundle) and returns it @@ -708,8 +744,8 @@ pub struct ScriptConfig { pub backends: HashMap>, /// Whether to batch all broadcast transactions into a single Tempo batch transaction. pub batch: bool, - /// Tempo fee token address for paying transaction fees. - pub fee_token: Option
, + /// Tempo transaction options applied to broadcast transactions. + pub tempo: TempoOpts, } impl ScriptConfig { @@ -717,7 +753,7 @@ impl ScriptConfig { config: Config, evm_opts: EvmOpts, batch: bool, - fee_token: Option
, + tempo: TempoOpts, ) -> Result { let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() { next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await? @@ -726,7 +762,7 @@ impl ScriptConfig { 1 }; - Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, fee_token }) + Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default(), batch, tempo }) } pub async fn update_sender(&mut self, sender: Address) -> Result<()> { @@ -802,7 +838,7 @@ impl ScriptConfig { self.evm_opts.clone(), Some(known_contracts), Some(target), - self.fee_token, + self.tempo.common.fee_token, ) .into(), ) @@ -813,7 +849,7 @@ impl ScriptConfig { // Propagate fee token to the transaction environment so that internal EVM calls // (e.g. script deployment, setUp) use the correct fee token for Tempo networks. - tx_env.set_fee_token(self.fee_token); + tx_env.set_fee_token(self.tempo.common.fee_token); Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())) } @@ -823,6 +859,7 @@ impl ScriptConfig { mod tests { use super::*; use alloy_network::Ethereum; + use alloy_primitives::address; use foundry_config::{NamedChain, UnresolvedEnvVarError}; use std::fs; use tempfile::tempdir; @@ -834,6 +871,50 @@ mod tests { assert_eq!(args.sig, sig); } + #[test] + fn can_parse_shared_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.fee-token", + "1", + "--tempo.expires", + "10", + ]); + + assert_eq!( + args.tempo.common.fee_token, + Some(address!("0x20C0000000000000000000000000000000000001")) + ); + assert_eq!(args.tempo.common.expires, Some(10)); + } + + #[test] + fn can_parse_sponsor_tempo_opts() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "Contract.sol", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signer", + "env://TEMPO_SPONSOR_PK", + ]); + + assert_eq!( + args.tempo.sponsor, + Some(address!("0x1111111111111111111111111111111111111111")) + ); + assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK")); + } + + #[test] + fn can_parse_full_tempo_opts() { + let args = + ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]); + + assert_eq!(args.tempo.nonce_key, Some(U256::from(1))); + } + #[test] fn can_parse_unlocked() { let args = ScriptArgs::parse_from([ diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index e2404d60ce2b9..b085f8eaf4545 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -6,7 +6,7 @@ use alloy_network::TransactionBuilder; use alloy_primitives::{Address, Bytes, U256}; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; -use foundry_common::{FoundryTransactionBuilder, TransactionMaybeSigned}; +use foundry_common::TransactionMaybeSigned; use foundry_config::Config; use foundry_evm::{ constants::CALLER, @@ -84,9 +84,7 @@ impl ScriptRunner { .with_input(code.clone()) .with_nonce(sender_nonce + library_transactions.len() as u64); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), @@ -122,9 +120,7 @@ impl ScriptRunner { .with_nonce(sender_nonce + library_transactions.len() as u64) .with_to(create2_deployer); - if let Some(fee_token) = script_config.fee_token { - tx_req.set_fee_token(fee_token); - } + script_config.tempo.apply::(&mut tx_req, None); library_transactions.push_back(BroadcastableTransaction { rpc: self.evm_opts.fork_url.clone(), diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index ef10a1ce94082..fe1e9345fa23c 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -129,7 +129,7 @@ impl VerifyBundle { path: Some(artifact.source.to_string_lossy().to_string()), name: artifact .name - .strip_suffix(&format!(".{}", &artifact.profile)) + .strip_suffix(&format!(".{}", artifact.profile)) .unwrap_or_else(|| &artifact.name) .to_string(), }; diff --git a/crates/sol-macro-gen/Cargo.toml b/crates/sol-macro-gen/Cargo.toml index 69ea952d4d040..d3ad56a96cdd2 100644 --- a/crates/sol-macro-gen/Cargo.toml +++ b/crates/sol-macro-gen/Cargo.toml @@ -27,3 +27,7 @@ prettyplease.workspace = true eyre.workspace = true heck.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index d29f6358e93d2..8dc652bcb32bd 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -44,3 +44,7 @@ idna_adapter.workspace = true [dev-dependencies] tokio.workspace = true + +[features] +default = ["optimism"] +optimism = ["foundry-common/optimism"] diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 0a3fad720ef57..fa065425b5281 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -49,7 +49,7 @@ static TEMPLATE_LOCK: LazyLock = LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template.lock")); /// The default Solc version used when compiling tests. -pub const SOLC_VERSION: &str = "0.8.33"; +pub const SOLC_VERSION: &str = "0.8.35"; /// Another Solc version used when compiling tests. /// diff --git a/crates/verify/Cargo.toml b/crates/verify/Cargo.toml index 65a202911509f..e3372a2494f34 100644 --- a/crates/verify/Cargo.toml +++ b/crates/verify/Cargo.toml @@ -48,3 +48,12 @@ url.workspace = true tokio = { workspace = true, features = ["macros"] } foundry-test-utils.workspace = true tempfile.workspace = true + +[features] +default = ["optimism"] +optimism = [ + "foundry-common/optimism", + "foundry-evm/optimism", + "foundry-evm-networks/optimism", + "foundry-cli/optimism", +] diff --git a/crates/wallets/src/tempo.rs b/crates/wallets/src/tempo.rs new file mode 100644 index 0000000000000..a86b568fdea2b --- /dev/null +++ b/crates/wallets/src/tempo.rs @@ -0,0 +1,196 @@ +use alloy_eips::Encodable2718; +use alloy_primitives::{Address, hex}; +use alloy_rlp::Decodable; +use alloy_signer::Signer; +use eyre::Result; +use std::path::PathBuf; +use tempo_alloy::rpc::TempoTransactionRequest; +use tempo_primitives::transaction::{ + KeychainSignature, PrimitiveSignature, SignedKeyAuthorization, TempoSignature, +}; + +use crate::{WalletSigner, utils}; + +/// Wallet type: how this wallet was created. +#[derive(Clone, Copy, Default, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum WalletType { + #[default] + Local, + Passkey, +} + +/// Cryptographic key type. +#[derive(Clone, Copy, Default, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum KeyType { + #[default] + Secp256k1, + P256, + WebAuthn, +} + +/// A single entry from Tempo's `keys.toml`. +#[derive(serde::Deserialize)] +#[allow(dead_code)] +struct KeyEntry { + #[serde(default)] + wallet_type: WalletType, + #[serde(default)] + wallet_address: Address, + #[serde(default)] + chain_id: u64, + #[serde(default)] + key_type: KeyType, + #[serde(default)] + key_address: Option
, + #[serde(default)] + key: Option, + #[serde(default)] + key_authorization: Option, + #[serde(default)] + expiry: Option, + #[serde(default)] + limits: Vec, +} + +/// Per-token spending limit stored in `keys.toml`. +#[derive(serde::Deserialize)] +struct StoredTokenLimit { + #[allow(dead_code)] + currency: Address, + #[allow(dead_code)] + limit: String, +} + +/// The top-level structure of `~/.tempo/wallet/keys.toml`. +#[derive(serde::Deserialize)] +struct KeysFile { + #[serde(default)] + keys: Vec, +} + +/// Configuration for a Tempo access key (keychain mode). +/// +/// When a Tempo wallet entry uses keychain mode (`wallet_address != key_address`), the signer +/// is an access key that signs on behalf of the root wallet. This struct carries the metadata +/// needed to construct the correct transaction. +#[derive(Debug, Clone)] +pub struct TempoAccessKeyConfig { + /// The root wallet address (the `from` address for transactions). + pub wallet_address: Address, + /// The access key's address (derived from the private key that actually signs). + pub key_address: Address, + /// Decoded key authorization for on-chain provisioning. + /// + /// When present, callers should check whether the key is already provisioned on-chain + /// (via the AccountKeychain precompile) before including this in a transaction. + pub key_authorization: Option, +} + +/// Result of looking up an address in Tempo's key store. +pub enum TempoLookup { + /// A direct (EOA) signer was found — `wallet_address == key_address`. + Direct(WalletSigner), + /// A keychain (access key) signer was found — `wallet_address != key_address`. + Keychain(WalletSigner, Box), + /// No matching entry was found. + NotFound, +} + +/// Returns the path to Tempo's keys file. +/// +/// Respects `TEMPO_HOME` env var, defaulting to `~/.tempo`. +fn keys_path() -> Option { + let base = std::env::var_os("TEMPO_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|h| h.join(".tempo")))?; + Some(base.join("wallet").join("keys.toml")) +} + +/// Decodes a hex-encoded, RLP-encoded [`SignedKeyAuthorization`]. +fn decode_key_authorization(hex_str: &str) -> Result { + let bytes = hex::decode(hex_str)?; + let auth = SignedKeyAuthorization::decode(&mut bytes.as_slice())?; + Ok(auth) +} + +/// Looks up a signer for the given address in Tempo's `keys.toml`. +/// +/// Returns [`TempoLookup::Direct`] if a direct-mode (EOA) key is found, +/// [`TempoLookup::Keychain`] if a keychain-mode access key is found, +/// or [`TempoLookup::NotFound`] if no entry matches. +pub fn lookup_signer(from: Address) -> Result { + let path = match keys_path() { + Some(p) if p.is_file() => p, + _ => return Ok(TempoLookup::NotFound), + }; + + let contents = std::fs::read_to_string(&path)?; + let file: KeysFile = toml::from_str(&contents)?; + + for entry in &file.keys { + if entry.wallet_address != from { + continue; + } + + let Some(key) = &entry.key else { + continue; + }; + + // Direct mode: wallet_address == key_address (or key_address is absent). + let is_direct = + entry.key_address.is_none() || entry.key_address == Some(entry.wallet_address); + + let signer = utils::create_private_key_signer(key)?; + + if is_direct { + return Ok(TempoLookup::Direct(signer)); + } + + // Keychain mode: the key is an access key signing on behalf of wallet_address. + let key_authorization = + entry.key_authorization.as_deref().map(decode_key_authorization).transpose()?; + + let config = TempoAccessKeyConfig { + wallet_address: entry.wallet_address, + // SAFETY: `is_direct` was false, so `key_address` is `Some` and != wallet_address + key_address: entry.key_address.unwrap(), + key_authorization, + }; + return Ok(TempoLookup::Keychain(signer, Box::new(config))); + } + + Ok(TempoLookup::NotFound) +} + +/// Signs a Tempo transaction request using an access key (keychain V2 mode). +/// +/// Bypasses the standard `EthereumWallet` signing path and instead: +/// 1. Builds the `TempoTransaction` from the request +/// 2. Computes the V2 keychain signing hash +/// 3. Signs with the access key +/// 4. Wraps in a `KeychainSignature` and encodes to EIP-2718 wire format +pub async fn sign_with_access_key( + tx_request: impl Into, + signer: &impl Signer, + wallet_address: Address, +) -> Result> { + let tx_request: TempoTransactionRequest = tx_request.into(); + let tempo_tx = tx_request + .build_aa() + .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?; + + let sig_hash = tempo_tx.signature_hash(); + let signing_hash = KeychainSignature::signing_hash(sig_hash, wallet_address); + let raw_sig = signer.sign_hash(&signing_hash).await?; + + let keychain_sig = + KeychainSignature::new(wallet_address, PrimitiveSignature::Secp256k1(raw_sig)); + let aa_signed = tempo_tx.into_signed(TempoSignature::Keychain(keychain_sig)); + + let mut buf = Vec::new(); + aa_signed.encode_2718(&mut buf); + + Ok(buf) +} diff --git a/deny.toml b/deny.toml index 1a0e1e8e53005..0f891df4bfbfe 100644 --- a/deny.toml +++ b/deny.toml @@ -100,7 +100,10 @@ unknown-git = "deny" allow-git = [ "https://github.com/alloy-rs/alloy", "https://github.com/alloy-rs/evm", + "https://github.com/foundry-rs/compilers", + "https://github.com/foundry-rs/foundry-fork-db", "https://github.com/foundry-rs/foundry-core", + "https://github.com/foundry-rs/optimism", "https://github.com/paradigmxyz/revm-inspectors", "https://github.com/paradigmxyz/solar", "https://github.com/bluealloy/revm", @@ -111,7 +114,5 @@ allow-git = [ "https://github.com/tempoxyz/mpp-rs", # Transitive dependency of Tempo "https://github.com/paradigmxyz/reth", - "https://github.com/paradigmxyz/reth-core", - # Temporary: upstream OP crates until release is published. - "https://github.com/ethereum-optimism/optimism", + "https://github.com/stevencartavia/reth", ] diff --git a/docs/dev/lintrules.md b/docs/dev/lintrules.md index 6f5dbbd850784..969d7effe142f 100644 --- a/docs/dev/lintrules.md +++ b/docs/dev/lintrules.md @@ -60,6 +60,8 @@ Next, choose whether you want an [early or late lint pass](#choosing-between-ear - Implement the appropriate trait logic (`EarlyLintPass` or `LateLintPass`) for your lint. Do it in a new file within the relevant severity module (e.g., `src/sol/med/my_new_lint.rs`). +- Add a markdown documentation file for the lint at `crates/lint/docs/.md`. The file is referenced by the lint's `help` URL (`https://getfoundry.sh/forge/linting/`) and is consumed by the [Foundry book](https://github.com/foundry-rs/book) to render the lint reference page. Use [`crates/lint/docs/_template.md`](../../crates/lint/docs/_template.md) as a starting point. The presence of this file is enforced by the `registered_lints_have_docs` unit test in `crates/lint/src/sol/mod.rs`. + ### Choosing Between Early and Late Passes - **Use `EarlyLintPass`** for: diff --git a/flake.lock b/flake.lock index 27f426f491da6..343a79c0cda3d 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1777102577, - "narHash": "sha256-ycoy9svZOQgyInu/lwO7IEQtlP5liqYhEcF9m9hPRbM=", + "lastModified": 1777708550, + "narHash": "sha256-Qif3UXT0l5OQq8H9pRWt4/ia4gF48MWK2oHKL8uVx8U=", "owner": "nix-community", "repo": "fenix", - "rev": "f37403486c59376cd285f9685a8ef8ff25c09a3c", + "rev": "74c1591efaff494756b8d35ebe357c6c2bbdca96", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1777641297, + "narHash": "sha256-WNGcmeOZ8Tr9dq6ztCspYbzWFswr2mPebM9LpsfGxPk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "c6d65881c5624c9cae5ea6cedef24699b0c0a4c0", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1776800521, - "narHash": "sha256-f8YJfwAOsLFpIoqZuX3yF69UvMLrkx7iVzMH1pJU7cM=", + "lastModified": 1777639980, + "narHash": "sha256-6d7Hdurvbjc5uwJuc0YiK7rZBGj6Gs3uzfBFcTs+xCc=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "8954b66d43225e62c92e8bbcc8500191b5cceb1e", + "rev": "64cdaeb06f69b6b769a492edd88b022ae88e8ca2", "type": "github" }, "original": { diff --git a/foundryup/README.md b/foundryup/README.md index 29e91378929cc..2cf61f8725227 100644 --- a/foundryup/README.md +++ b/foundryup/README.md @@ -30,10 +30,10 @@ To install the latest **nightly** version: foundryup --install nightly ``` -To install a specific version (e.g. `v1.6.0`): +To install a specific version (e.g. `v1.7.0`): ```sh -foundryup --install v1.6.0 +foundryup --install v1.7.0 ``` To **list** all **versions** installed: diff --git a/foundryup/foundryup b/foundryup/foundryup index 9bbaea82e7e21..5fb086a75f8c1 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -3,7 +3,7 @@ set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -FOUNDRYUP_INSTALLER_VERSION="1.8.0" +FOUNDRYUP_INSTALLER_VERSION="1.8.3" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} @@ -15,6 +15,13 @@ FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" FOUNDRYUP_JOBS="" FOUNDRYUP_IGNORE_VERIFICATION=false +# Retry/backoff settings used for `fetch` (GitHub API calls). +# Recovers from transient HTTP 403/429/5xx responses returned by +# api.github.com under heavy load or per-IP rate limiting. +FOUNDRYUP_MAX_RETRIES=5 +FOUNDRYUP_RETRY_DELAY=2 +FOUNDRYUP_RETRY_MAX_TIME=60 + BINS=(forge cast anvil chisel) HASH_NAMES=() HASH_VALUES=() @@ -111,43 +118,7 @@ main() { # Install by downloading binaries if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-latest} - - # Normalize versions (handle channels, versions without v prefix) - if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then - # Resolve to the latest release (non-prerelease) via the GitHub API - say "fetching latest release from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' - /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } - ') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a latest release for ${FOUNDRYUP_REPO}" - fi - say "resolved release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" - elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then - # Resolve to the latest nightly (prerelease) release via the GitHub API - say "fetching latest nightly releases from ${FOUNDRYUP_REPO}..." - FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' - /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } - /"draft"[[:space:]]*:[[:space:]]*true/ { tag="" } - /"prerelease"[[:space:]]*:[[:space:]]*true/ && !found { if (tag ~ /^nightly-/) { print tag; found=1 } } - ') || err "failed to fetch releases from GitHub API" - if [ -z "$FOUNDRYUP_TAG" ]; then - err "could not find a nightly release for ${FOUNDRYUP_REPO}" - fi - say "resolved nightly release tag: ${FOUNDRYUP_TAG}" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then - # Specific nightly tag (e.g. nightly-abc123...) - FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" - FOUNDRYUP_VERSION="nightly" - elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then - # Add v prefix - FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - else - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - fi + resolve_version_and_tag say "installing foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})" @@ -171,7 +142,7 @@ main() { tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp_dir/attestation.txt" ensure download "$ATTESTATION_URL" "$tmp" - + # Read the first line of the attestation file to get the artifact link. # The first line should contain the link to the attestation artifact. attestation_artifact_link="$(head -n1 "$tmp" | tr -d '\r')" @@ -284,7 +255,7 @@ main() { else say 'skipping manpage download: missing "tar"' fi - + if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = true ]; then say "skipped SHA verification for downloaded binaries due to --force flag" else @@ -497,6 +468,18 @@ list() { use() { [ -z "$FOUNDRYUP_VERSION" ] && err "no version provided" + + # If the requested version is a channel (`latest`, `stable`, `nightly`) or a bare semver + # version (e.g. `1.7.0`, `1.6.0-rc1`), resolve it to the immutable tag directory created by + # `--install` (channels hit the GitHub API; semver versions get a `v` prefix). + # Falls back to the literal value for locally-built versions (branches, PRs, commits, custom names). + case "$FOUNDRYUP_VERSION" in + latest|stable|nightly|[0-9]*.[0-9]*.[0-9]*) + resolve_version_and_tag + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + ;; + esac + FOUNDRY_VERSION_DIR="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" if [ -d "$FOUNDRY_VERSION_DIR" ]; then @@ -675,12 +658,70 @@ ensure() { if ! "$@"; then err "command failed: $*"; fi } -# Silently fetches $1 to stdout +# Normalizes `FOUNDRYUP_VERSION` and resolves it to a concrete release tag, +# populating `FOUNDRYUP_TAG`. Handles the `latest`/`stable`/`nightly` channels +# (looked up via the GitHub API). +resolve_version_and_tag() { + FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} + if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then + # Resolve to the latest release (non-prerelease) via the GitHub API. + say "fetching latest release tag from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk ' + /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 } + ') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a latest release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="$FOUNDRYUP_TAG" + elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then + # Resolve to the latest nightly (prerelease) release via the GitHub API. + # The GitHub API does not guarantee that releases are returned in + # chronological order, so we collect all matching nightlies along with + # their `published_at` timestamps and sort them ourselves. + say "fetching latest nightly release tags from ${FOUNDRYUP_REPO}..." + FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases" | awk ' + /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 } + /"published_at"[[:space:]]*:[[:space:]]*"/ { + pub=$0 + gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub) + gsub(/".*/, "", pub) + if (tag ~ /^nightly-/) print pub "\t" tag + tag="" + } + ' | sort -r | awk -F '\t' 'NR==1 { print $2 }') || err "failed to fetch release tags from GitHub API" + if [ -z "$FOUNDRYUP_TAG" ]; then + err "could not find a nightly release tag for ${FOUNDRYUP_REPO}" + fi + say "resolved nightly release tag: ${FOUNDRYUP_TAG}" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then + # Specific nightly tag (e.g. nightly-abc123...) + FOUNDRYUP_TAG="$FOUNDRYUP_VERSION" + FOUNDRYUP_VERSION="nightly" + elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then + # Add v prefix + FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + else + FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" + fi +} + +# Silently fetches $1 to stdout. fetch() { if check_cmd curl; then - curl -fsSL "$1" + curl -fsSL \ + --retry "$FOUNDRYUP_MAX_RETRIES" \ + --retry-delay "$FOUNDRYUP_RETRY_DELAY" \ + --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \ + --retry-all-errors \ + "$1" else - wget -qO- "$1" + wget --tries="$FOUNDRYUP_MAX_RETRIES" \ + --waitretry="$FOUNDRYUP_RETRY_DELAY" \ + --retry-on-http-error=403,408,429,500,502,503,504 \ + -qO- "$1" fi } diff --git a/sleep.json b/sleep.json new file mode 100644 index 0000000000000..5b430e1e663f6 --- /dev/null +++ b/sleep.json @@ -0,0 +1,955 @@ +{ + "results": [ + { + "command": "sleep 0.020", + "mean": 0.023726515413333333, + "stddev": 0.004602014051751124, + "median": 0.02267755758, + "user": 0.0013185473333333334, + "system": 0.0020899164444444446, + "min": 0.02109890308, + "max": 0.05602819808, + "times": [ + 0.02856005608, + 0.02346135008, + 0.02202502208, + 0.02139558708, + 0.02265920408, + 0.02121691608, + 0.02272505608, + 0.02114247908, + 0.02157142808, + 0.021514666079999998, + 0.02161920108, + 0.02335035008, + 0.02224331408, + 0.02228639708, + 0.02152537208, + 0.021732302079999998, + 0.02273370308, + 0.02115513608, + 0.02268494308, + 0.02244547308, + 0.023943647079999998, + 0.02324528508, + 0.02152617908, + 0.023991903079999998, + 0.02250884108, + 0.02342551708, + 0.02113216608, + 0.02168223108, + 0.02222267508, + 0.02273532108, + 0.02273995308, + 0.05602819808, + 0.02501500608, + 0.03121396008, + 0.02424400108, + 0.02459129108, + 0.02633760708, + 0.02377406808, + 0.02365474708, + 0.02406064008, + 0.02300910408, + 0.02437339208, + 0.02317403908, + 0.02257532008, + 0.02267017208, + 0.02356714508, + 0.02367204808, + 0.02258227108, + 0.02330384008, + 0.02225645108, + 0.02478414908, + 0.02484724308, + 0.02270765708, + 0.02339114708, + 0.02450795908, + 0.02348840008, + 0.044674490080000004, + 0.028041754080000002, + 0.022940745079999998, + 0.02259975308, + 0.022112378079999998, + 0.02271348408, + 0.02320266708, + 0.02284982108, + 0.02244050908, + 0.02238655808, + 0.022084648079999998, + 0.02241669808, + 0.02523103408, + 0.02256237908, + 0.03532525108, + 0.02232798408, + 0.02173793008, + 0.021903001079999998, + 0.02288046308, + 0.02368652508, + 0.02211418708, + 0.02265551308, + 0.02187778308, + 0.02191395108, + 0.02182523808, + 0.02185612208, + 0.02109890308, + 0.02294132008, + 0.02191512608, + 0.02264461208, + 0.02227651108, + 0.02307147508, + 0.02227169708, + 0.02177434208 + ], + "memory_usage_byte": [ + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3014656, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3141632, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3268608, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "command": "sleep 0.021", + "mean": 0.022889189941111117, + "stddev": 0.0007161191938371117, + "median": 0.02280623708, + "user": 0.0009166992592592593, + "system": 0.0016941181481481477, + "min": 0.02132554808, + "max": 0.02453766808, + "times": [ + 0.02311599608, + 0.02274468508, + 0.02193879008, + 0.02158843608, + 0.02329398008, + 0.02379494508, + 0.02260801308, + 0.02439507908, + 0.02448522508, + 0.02403379508, + 0.02298143008, + 0.02263027308, + 0.02229235308, + 0.02335063508, + 0.02377098008, + 0.02269184108, + 0.023631199079999998, + 0.02338021508, + 0.02198521708, + 0.02251586208, + 0.022295963079999998, + 0.02226397608, + 0.02453766808, + 0.02184453408, + 0.02289659908, + 0.02382663208, + 0.02347397108, + 0.02225926308, + 0.02207640608, + 0.02243237108, + 0.02278192608, + 0.02270514808, + 0.02245069008, + 0.023018867079999998, + 0.02399866208, + 0.02236840708, + 0.02366382208, + 0.02294188908, + 0.02155127708, + 0.02294999808, + 0.02132554808, + 0.02242025908, + 0.02202766108, + 0.02182175108, + 0.02272186608, + 0.02211805308, + 0.02319764908, + 0.022308045079999998, + 0.02345400908, + 0.022437877079999998, + 0.02273417808, + 0.02217370908, + 0.02254318408, + 0.023269922079999998, + 0.02384951108, + 0.02419476108, + 0.02439866908, + 0.02354840508, + 0.02304219108, + 0.02354960608, + 0.02382648708, + 0.02345751208, + 0.02367913708, + 0.02253067208, + 0.02215132608, + 0.022603942079999998, + 0.02284062808, + 0.02252907808, + 0.02220393508, + 0.023291509079999998, + 0.02399456908, + 0.02407123208, + 0.02279175108, + 0.02300624708, + 0.02309500408, + 0.023036532079999998, + 0.02303833108, + 0.02316846908, + 0.02228349608, + 0.02247140608, + 0.022482600079999998, + 0.02370720808, + 0.02220123708, + 0.02230588608, + 0.02333678708, + 0.02153336008, + 0.02203071908, + 0.02279195108, + 0.02353659108, + 0.02267460708, + 0.022536274079999998, + 0.022769262079999998, + 0.02314857808, + 0.02194885908, + 0.02355038408, + 0.02320035308, + 0.02307451408, + 0.02379926408, + 0.02330480208, + 0.02257055708, + 0.02330320308, + 0.02303003208, + 0.02327859908, + 0.02171311608, + 0.02282052308, + 0.02170123708, + 0.02254831308, + 0.02235855408 + ], + "memory_usage_byte": [ + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "command": "sleep 0.022", + "mean": 0.02415569324504855, + "stddev": 0.0009830972994273135, + "median": 0.02409406108, + "user": 0.001165289514563107, + "system": 0.001767603883495146, + "min": 0.02243173808, + "max": 0.02755932908, + "times": [ + 0.02456728108, + 0.02650439708, + 0.02480475408, + 0.02452974808, + 0.02300978308, + 0.02521451608, + 0.02543841408, + 0.02538411108, + 0.02475773908, + 0.02403843308, + 0.02426362708, + 0.02326921708, + 0.02447185308, + 0.02361749008, + 0.02410661008, + 0.02371481508, + 0.02327300908, + 0.02430165908, + 0.02328269108, + 0.02315262608, + 0.02380195808, + 0.02283639508, + 0.02491355808, + 0.02401717008, + 0.02556049408, + 0.02350359508, + 0.02400529208, + 0.02533555808, + 0.02467923308, + 0.02478442308, + 0.02422068708, + 0.02352175108, + 0.02481882108, + 0.02456148108, + 0.02314905108, + 0.024188183079999998, + 0.02483985908, + 0.02289141308, + 0.02364977308, + 0.02354907008, + 0.02379135508, + 0.026812933079999997, + 0.023360627079999998, + 0.02331436308, + 0.02504176308, + 0.02358805508, + 0.02409406108, + 0.02350689508, + 0.02303628508, + 0.02430972408, + 0.02516170908, + 0.02352843108, + 0.02274564308, + 0.02345165808, + 0.02429327308, + 0.02252948108, + 0.02445868508, + 0.02755932908, + 0.02522621808, + 0.02491753008, + 0.022858510079999998, + 0.02401968108, + 0.02409596908, + 0.02390450108, + 0.02373108808, + 0.027211489079999998, + 0.02537487108, + 0.02319182608, + 0.02390569508, + 0.02490164708, + 0.02384732708, + 0.02243173808, + 0.02367003008, + 0.02494288308, + 0.02436298308, + 0.02390639308, + 0.02423030808, + 0.02430082908, + 0.02320845908, + 0.02421546708, + 0.02530823508, + 0.02368935308, + 0.02306283708, + 0.023536658079999998, + 0.02359881208, + 0.02438320308, + 0.02477724008, + 0.02362231908, + 0.02419465008, + 0.02596891608, + 0.02307578608, + 0.02459456508, + 0.02384055408, + 0.02421387408, + 0.02510733208, + 0.02473580508, + 0.02243970708, + 0.02253156008, + 0.02550018108, + 0.02440877608, + 0.02281331608, + 0.02354148408, + 0.02352098308 + ], + "memory_usage_byte": [ + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680, + 3399680 + ], + "exit_codes": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +} diff --git a/testdata/default/cheats/ExpectRevert.t.sol b/testdata/default/cheats/ExpectRevert.t.sol index 839d97962aa94..ae0c8ed844f5d 100644 --- a/testdata/default/cheats/ExpectRevert.t.sol +++ b/testdata/default/cheats/ExpectRevert.t.sol @@ -305,6 +305,91 @@ contract ExpectRevertWithReverterTest is Test { vm.expectRevert(address(cContract)); aContract.createDContractThroughCContract(); } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // reverts directly, the reverter address argument must be enforced (it used to + // be silently ignored). The matched reverter is the would-be-deployed address. + function testExpectRevertsWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new DContract(); + + expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(abi.encodePacked("Reverted by DContract"), expected); + new DContract(); + } + + // + // Regression: when the next operation is a top-level CREATE whose constructor + // synchronously creates another contract that reverts (i.e. innermost frame is + // a CREATE), the matched reverter is the outer would-be-deployed address (the + // contract whose deployment failed). + function testExpectRevertsWithReverterNestedCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(expected); + new NestedDContractCreator(); + } + + // + // Regression: `expectPartialRevert(bytes4, address)` overload must enforce + // the reverter address argument when matching a top-level CREATE revert. + function testExpectPartialRevertWithReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + // `Reverted by DContract` triggers Solidity's `Error(string)` selector. + vm.expectPartialRevert(bytes4(keccak256("Error(string)")), expected); + new DContract(); + } + + // + // Regression: `expectRevert(bytes4, address)` (exact 4-byte selector + reverter) + // overload must enforce the reverter address argument for a top-level CREATE. + function testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate() public { + address expected = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectRevert(DCustomErrorContract.CustomError.selector, expected); + new DCustomErrorContract(); + } + + // + // Regression: `expectRevert(address, uint64)` count-bearing overload must + // exercise the `count > 1` branch in `create_end`. Use CREATE2 with the same + // salt so both deploys would resolve to the same would-be address (each + // constructor reverts so no contract is ever actually placed there). + function testExpectRevertsWithReverterCountTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0x42)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected, 2); + new DContract{salt: salt}(); + new DContract{salt: salt}(); + } + + // + // Regression: CREATE2 deploys must also enforce the reverter address argument. + function testExpectRevertsWithReverterTopLevelCreate2() public { + bytes32 salt = bytes32(uint256(0xC0FFEE)); + address expected = vm.computeCreate2Address(salt, keccak256(type(DContract).creationCode), address(this)); + vm.expectRevert(expected); + new DContract{salt: salt}(); + } +} + +// Used by `testExpectRevertsWithReverterNestedCreate`: a contract whose constructor +// directly creates another contract that reverts. +contract NestedDContractCreator { + constructor() { + new DContract(); + } +} + +// Used by `testExpectRevertWithBytes4SelectorAndReverterTopLevelCreate`: constructor +// reverts with a parameter-less custom error so the full revert data is exactly the +// 4-byte selector. +contract DCustomErrorContract { + error CustomError(); + + constructor() { + revert CustomError(); + } } contract ExpectRevertCount is Test { diff --git a/testdata/default/cheats/Fork2.t.sol b/testdata/default/cheats/Fork2.t.sol index 0941e508483fd..d83c0480b7e72 100644 --- a/testdata/default/cheats/Fork2.t.sol +++ b/testdata/default/cheats/Fork2.t.sol @@ -325,6 +325,7 @@ contract ForkTest is Test { struct LegacyTransactionResult { bytes32 blockHash; bytes blockNumber; + bytes blockTimestamp; bytes chainId; address from; bytes gas; diff --git a/testdata/default/cheats/GetFoundryVersion.t.sol b/testdata/default/cheats/GetFoundryVersion.t.sol index 6139b8b6b6a5e..f01b7cdd7d213 100644 --- a/testdata/default/cheats/GetFoundryVersion.t.sol +++ b/testdata/default/cheats/GetFoundryVersion.t.sol @@ -84,4 +84,55 @@ contract GetFoundryVersionTest is Test { // Should return true for past versions assertTrue(vm.foundryVersionAtLeast("0.2.0")); } + + /// Returns the `MAJOR.MINOR.PATCH` prefix of `vm.getFoundryVersion()`, + /// stripping any pre-release suffix (`-nightly`, `-dev`, …) and the + /// `+..` build metadata. + function _semverPrefix() internal view returns (string memory) { + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + require(plusSplit.length == 2, "Invalid version format: Missing '+' separator"); + string[] memory dashSplit = vm.split(plusSplit[0], "-"); + return dashSplit[0]; + } + + function testGetFoundryVersionMajorMinorPatchIsParseable() public view { + // The MAJOR.MINOR.PATCH prefix must always be three numeric components, + // regardless of build kind (tagged release / nightly / dev). + string[] memory parts = vm.split(_semverPrefix(), "."); + require(parts.length == 3, "Invalid semver prefix: expected MAJOR.MINOR.PATCH"); + // Each component must parse as a uint (this reverts on garbage). + vm.parseUint(parts[0]); + vm.parseUint(parts[1]); + vm.parseUint(parts[2]); + } + + function testGetFoundryVersionBuildProfile() public view { + // The build profile must be present and non-empty (e.g. "debug", "release", "dist", …). + string[] memory plusSplit = vm.split(vm.getFoundryVersion(), "+"); + string[] memory metadataComponents = vm.split(plusSplit[1], "."); + require(bytes(metadataComponents[2]).length > 0, "Build profile is empty"); + } + + function testFoundryVersionCmpAndAtLeastAreConsistent() public { + // `foundryVersionAtLeast(v)` must equal `foundryVersionCmp(v) >= 0` for any input. + string[3] memory probes = ["0.0.1", _semverPrefix(), "99.0.0"]; + for (uint256 i = 0; i < probes.length; i++) { + assertEq(vm.foundryVersionAtLeast(probes[i]), vm.foundryVersionCmp(probes[i]) >= 0); + } + } + + function testFoundryVersionCmpRejectsPreRelease() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0-nightly"); + } + + function testFoundryVersionCmpRejectsBuildMetadata() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("1.0.0+abc1234567.1700000000.release"); + } + + function testFoundryVersionCmpRejectsInvalidVersion() public { + vm._expectCheatcodeRevert(); + vm.foundryVersionCmp("not-a-version"); + } } diff --git a/testdata/default/cheats/MockCall.t.sol b/testdata/default/cheats/MockCall.t.sol index e2ac74d6f70fa..d8019ab4f6ee8 100644 --- a/testdata/default/cheats/MockCall.t.sol +++ b/testdata/default/cheats/MockCall.t.sol @@ -158,6 +158,35 @@ contract MockCallTest is Test { assertEq(mock.pay{value: 50}(1), 100); } + function testMockCallWithValueTransfersBalance() public { + Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + assertEq(address(mock).balance, 0); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + } + + function testMockCallWithValueTransfersPrankedSenderBalance() public { + Mock mock = new Mock(); + address sender = address(0xBEEF); + uint256 value = 10; + vm.deal(address(this), 0); + vm.deal(sender, value); + + vm.mockCall(address(mock), value, abi.encodeWithSelector(mock.pay.selector), abi.encode(10)); + + vm.prank(sender); + assertEq(mock.pay{value: value}(1), 10); + assertEq(address(mock).balance, value); + assertEq(address(this).balance, 0); + assertEq(sender.balance, 0); + } + function testMockCallWithValueCalldataPrecedence() public { Mock mock = new Mock(); @@ -279,17 +308,25 @@ contract MockCallRevertTest is Test { function testMockCallRevertWithValue() public { Mock mock = new Mock(); + uint256 value = 10; + vm.deal(address(this), value); - vm.mockCallRevert(address(mock), 10, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); + vm.mockCallRevert(address(mock), value, abi.encodeWithSelector(mock.pay.selector), ERROR_MESSAGE); assertEq(mock.pay(1), 1); assertEq(mock.pay(2), 2); - try mock.pay{value: 10}(1) { + uint256 initSenderBalance = address(this).balance; + uint256 initTargetBalance = address(mock).balance; + + try mock.pay{value: value}(1) { revert(); } catch (bytes memory err) { require(keccak256(err) == keccak256(ERROR_MESSAGE)); } + + assertEq(address(this).balance, initSenderBalance); + assertEq(address(mock).balance, initTargetBalance); } function testMockCallResetsMockCallRevert() public { diff --git a/testdata/default/cheats/MockCalls.t.sol b/testdata/default/cheats/MockCalls.t.sol index e0f5eef151db6..777543f28e361 100644 --- a/testdata/default/cheats/MockCalls.t.sol +++ b/testdata/default/cheats/MockCalls.t.sol @@ -28,13 +28,17 @@ contract MockCallsTest is Test { mocks[0] = abi.encode(2 ether); mocks[1] = abi.encode(1 ether); mocks[2] = abi.encode(6.423 ether); + vm.deal(address(this), 3 ether); vm.mockCalls(mockErc20, 1 ether, data, mocks); (, bytes memory ret1) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret1, (uint256)), 2 ether); + assertEq(mockErc20.balance, 1 ether); (, bytes memory ret2) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret2, (uint256)), 1 ether); + assertEq(mockErc20.balance, 2 ether); (, bytes memory ret3) = mockErc20.call{value: 1 ether}(data); assertEq(abi.decode(ret3, (uint256)), 6.423 ether); + assertEq(mockErc20.balance, 3 ether); } function testMockCalls() public { diff --git a/testdata/forge-std-rev b/testdata/forge-std-rev index b1716a0a12950..977c31eec5512 100644 --- a/testdata/forge-std-rev +++ b/testdata/forge-std-rev @@ -1 +1 @@ -8987040ede9553cea20c95ad40d0455930f9c8e0 \ No newline at end of file +620536fa5277db4e3fd46772d5cbc1ea0696fb43 \ No newline at end of file diff --git a/testdata/utils/Vm.sol b/testdata/utils/Vm.sol index d9f9b52821f52..e488a1820453e 100644 --- a/testdata/utils/Vm.sol +++ b/testdata/utils/Vm.sol @@ -36,121 +36,121 @@ interface Vm { function addr(uint256 privateKey) external pure returns (address keyAddr); function allowCheatcodes(address account) external; function assertApproxEqAbsDecimal(uint256 left, uint256 right, uint256 maxDelta, uint256 decimals) external pure; - function assertApproxEqAbsDecimal(uint256 left, uint256 right, uint256 maxDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqAbsDecimal(uint256 left, uint256 right, uint256 maxDelta, uint256 decimals, string calldata err) external pure; function assertApproxEqAbsDecimal(int256 left, int256 right, uint256 maxDelta, uint256 decimals) external pure; - function assertApproxEqAbsDecimal(int256 left, int256 right, uint256 maxDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqAbsDecimal(int256 left, int256 right, uint256 maxDelta, uint256 decimals, string calldata err) external pure; function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta) external pure; - function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata error) external pure; + function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata err) external pure; function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta) external pure; - function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata error) external pure; + function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata err) external pure; function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals) external pure; - function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata err) external pure; function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals) external pure; - function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata err) external pure; function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta) external pure; - function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure; + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata err) external pure; function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta) external pure; - function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata err) external pure; function assertEqDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertEqDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertEq(bool left, bool right) external pure; - function assertEq(bool left, bool right, string calldata error) external pure; + function assertEq(bool left, bool right, string calldata err) external pure; function assertEq(string calldata left, string calldata right) external pure; - function assertEq(string calldata left, string calldata right, string calldata error) external pure; + function assertEq(string calldata left, string calldata right, string calldata err) external pure; function assertEq(bytes calldata left, bytes calldata right) external pure; - function assertEq(bytes calldata left, bytes calldata right, string calldata error) external pure; + function assertEq(bytes calldata left, bytes calldata right, string calldata err) external pure; function assertEq(bool[] calldata left, bool[] calldata right) external pure; - function assertEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure; + function assertEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure; function assertEq(uint256[] calldata left, uint256[] calldata right) external pure; - function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure; + function assertEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure; function assertEq(int256[] calldata left, int256[] calldata right) external pure; - function assertEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure; + function assertEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure; function assertEq(uint256 left, uint256 right) external pure; function assertEq(address[] calldata left, address[] calldata right) external pure; - function assertEq(address[] calldata left, address[] calldata right, string calldata error) external pure; + function assertEq(address[] calldata left, address[] calldata right, string calldata err) external pure; function assertEq(bytes32[] calldata left, bytes32[] calldata right) external pure; - function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure; + function assertEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure; function assertEq(string[] calldata left, string[] calldata right) external pure; - function assertEq(string[] calldata left, string[] calldata right, string calldata error) external pure; + function assertEq(string[] calldata left, string[] calldata right, string calldata err) external pure; function assertEq(bytes[] calldata left, bytes[] calldata right) external pure; - function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure; - function assertEq(uint256 left, uint256 right, string calldata error) external pure; + function assertEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure; + function assertEq(uint256 left, uint256 right, string calldata err) external pure; function assertEq(int256 left, int256 right) external pure; - function assertEq(int256 left, int256 right, string calldata error) external pure; + function assertEq(int256 left, int256 right, string calldata err) external pure; function assertEq(address left, address right) external pure; - function assertEq(address left, address right, string calldata error) external pure; + function assertEq(address left, address right, string calldata err) external pure; function assertEq(bytes32 left, bytes32 right) external pure; - function assertEq(bytes32 left, bytes32 right, string calldata error) external pure; + function assertEq(bytes32 left, bytes32 right, string calldata err) external pure; function assertFalse(bool condition) external pure; - function assertFalse(bool condition, string calldata error) external pure; + function assertFalse(bool condition, string calldata err) external pure; function assertGeDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertGeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertGeDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertGeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertGe(uint256 left, uint256 right) external pure; - function assertGe(uint256 left, uint256 right, string calldata error) external pure; + function assertGe(uint256 left, uint256 right, string calldata err) external pure; function assertGe(int256 left, int256 right) external pure; - function assertGe(int256 left, int256 right, string calldata error) external pure; + function assertGe(int256 left, int256 right, string calldata err) external pure; function assertGtDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertGtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertGtDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertGtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertGt(uint256 left, uint256 right) external pure; - function assertGt(uint256 left, uint256 right, string calldata error) external pure; + function assertGt(uint256 left, uint256 right, string calldata err) external pure; function assertGt(int256 left, int256 right) external pure; - function assertGt(int256 left, int256 right, string calldata error) external pure; + function assertGt(int256 left, int256 right, string calldata err) external pure; function assertLeDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertLeDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertLeDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertLeDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertLe(uint256 left, uint256 right) external pure; - function assertLe(uint256 left, uint256 right, string calldata error) external pure; + function assertLe(uint256 left, uint256 right, string calldata err) external pure; function assertLe(int256 left, int256 right) external pure; - function assertLe(int256 left, int256 right, string calldata error) external pure; + function assertLe(int256 left, int256 right, string calldata err) external pure; function assertLtDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertLtDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertLtDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertLtDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertLt(uint256 left, uint256 right) external pure; - function assertLt(uint256 left, uint256 right, string calldata error) external pure; + function assertLt(uint256 left, uint256 right, string calldata err) external pure; function assertLt(int256 left, int256 right) external pure; - function assertLt(int256 left, int256 right, string calldata error) external pure; + function assertLt(int256 left, int256 right, string calldata err) external pure; function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals) external pure; - function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; + function assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata err) external pure; function assertNotEqDecimal(int256 left, int256 right, uint256 decimals) external pure; - function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata error) external pure; + function assertNotEqDecimal(int256 left, int256 right, uint256 decimals, string calldata err) external pure; function assertNotEq(bool left, bool right) external pure; - function assertNotEq(bool left, bool right, string calldata error) external pure; + function assertNotEq(bool left, bool right, string calldata err) external pure; function assertNotEq(string calldata left, string calldata right) external pure; - function assertNotEq(string calldata left, string calldata right, string calldata error) external pure; + function assertNotEq(string calldata left, string calldata right, string calldata err) external pure; function assertNotEq(bytes calldata left, bytes calldata right) external pure; - function assertNotEq(bytes calldata left, bytes calldata right, string calldata error) external pure; + function assertNotEq(bytes calldata left, bytes calldata right, string calldata err) external pure; function assertNotEq(bool[] calldata left, bool[] calldata right) external pure; - function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata error) external pure; + function assertNotEq(bool[] calldata left, bool[] calldata right, string calldata err) external pure; function assertNotEq(uint256[] calldata left, uint256[] calldata right) external pure; - function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata error) external pure; + function assertNotEq(uint256[] calldata left, uint256[] calldata right, string calldata err) external pure; function assertNotEq(int256[] calldata left, int256[] calldata right) external pure; - function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata error) external pure; + function assertNotEq(int256[] calldata left, int256[] calldata right, string calldata err) external pure; function assertNotEq(uint256 left, uint256 right) external pure; function assertNotEq(address[] calldata left, address[] calldata right) external pure; - function assertNotEq(address[] calldata left, address[] calldata right, string calldata error) external pure; + function assertNotEq(address[] calldata left, address[] calldata right, string calldata err) external pure; function assertNotEq(bytes32[] calldata left, bytes32[] calldata right) external pure; - function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata error) external pure; + function assertNotEq(bytes32[] calldata left, bytes32[] calldata right, string calldata err) external pure; function assertNotEq(string[] calldata left, string[] calldata right) external pure; - function assertNotEq(string[] calldata left, string[] calldata right, string calldata error) external pure; + function assertNotEq(string[] calldata left, string[] calldata right, string calldata err) external pure; function assertNotEq(bytes[] calldata left, bytes[] calldata right) external pure; - function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata error) external pure; - function assertNotEq(uint256 left, uint256 right, string calldata error) external pure; + function assertNotEq(bytes[] calldata left, bytes[] calldata right, string calldata err) external pure; + function assertNotEq(uint256 left, uint256 right, string calldata err) external pure; function assertNotEq(int256 left, int256 right) external pure; - function assertNotEq(int256 left, int256 right, string calldata error) external pure; + function assertNotEq(int256 left, int256 right, string calldata err) external pure; function assertNotEq(address left, address right) external pure; - function assertNotEq(address left, address right, string calldata error) external pure; + function assertNotEq(address left, address right, string calldata err) external pure; function assertNotEq(bytes32 left, bytes32 right) external pure; - function assertNotEq(bytes32 left, bytes32 right, string calldata error) external pure; + function assertNotEq(bytes32 left, bytes32 right, string calldata err) external pure; function assertTrue(bool condition) external pure; - function assertTrue(bool condition, string calldata error) external pure; + function assertTrue(bool condition, string calldata err) external pure; function assume(bool condition) external pure; function assumeNoRevert() external pure; function assumeNoRevert(PotentialRevert calldata potentialRevert) external pure; From de9cf905299ea383a5fe668a8d69e8440683c3a4 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 09:06:17 +0700 Subject: [PATCH 13/14] Update .github/scripts/compare-nightly.sh Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/scripts/compare-nightly.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/compare-nightly.sh b/.github/scripts/compare-nightly.sh index 674cc0fe01754..5b87aa0618e4c 100644 --- a/.github/scripts/compare-nightly.sh +++ b/.github/scripts/compare-nightly.sh @@ -39,7 +39,7 @@ for key in all_keys: if p is None: print(f"| `{key}` | N/A | {t:.5f}s | — | 🆕 New |") continue - delta = (t - p) / p * 100 + delta = (t - p) / p * 100 if p > 0 else 0 if delta >= fail: status = "🔴 Regression" has_regression = True From 2b8840d349f42a733fa89d544eea962d72731b35 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 7 May 2026 09:06:33 +0700 Subject: [PATCH 14/14] Update crates/forge/src/cmd/test/mod.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/forge/src/cmd/test/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index dd8f3afd56197..938e9674c0cbe 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -1166,7 +1166,7 @@ fn merge_outcomes(base: &mut TestOutcome, other: TestOutcome) { let base_suite = e.get_mut(); base_suite.test_results.extend(other_suite.test_results); base_suite.warnings.extend(other_suite.warnings); - base_suite.duration = base_suite.duration.max(other_suite.duration); + base_suite.duration += other_suite.duration; } } }