diff --git a/.github/FLAKY_TEST_ISOLATE_FAILURE_TEMPLATE.md b/.github/FLAKY_TEST_ISOLATE_FAILURE_TEMPLATE.md new file mode 100644 index 0000000000000..0d7a9bb84959d --- /dev/null +++ b/.github/FLAKY_TEST_ISOLATE_FAILURE_TEMPLATE.md @@ -0,0 +1,10 @@ +--- +title: "bug: flaky tests workflow failed (isolate)" +labels: P-normal, T-bug +--- + +The nightly flaky tests workflow (with isolation mode enabled) has failed. This indicates external API rate limiting, RPC reliability issues, or other intermittent failures that may affect users. + +Check the [flaky tests workflow page]({{ env.WORKFLOW_URL }}) for details. + +This issue was raised by the workflow at `.github/workflows/test-isolate.yml`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdb7848f55f29..bf3ee5a9f105b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,9 @@ name: CI permissions: {} on: + push: + branches: [master] pull_request: - merge_group: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -58,7 +59,7 @@ jobs: - uses: actions/checkout@v6 with: persist-credentials: false - - uses: crate-ci/typos@93cbdb2d23269548cf0db0f74d0bc6a09a3f0d5c # v1 + - uses: crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1 shellcheck: runs-on: depot-ubuntu-latest @@ -140,7 +141,7 @@ jobs: with: toolchain: stable - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2 + - uses: taiki-e/install-action@f8d25fb8a2df08dcd3cead89780d572767b8655f # v2 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 51e625d0d6d28..16de916a8993b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -92,10 +92,10 @@ jobs: printf "LABELS -> %s\n" "${{ steps.meta.outputs.labels }}" - name: Set up Depot CLI - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1 + uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1 - name: Build and push Foundry image - uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1 + uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1 with: build-args: | RUST_PROFILE=${{ env.RUST_PROFILE }} diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 8d39e71112494..38d60d534f611 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v6 with: persist-credentials: false - - uses: DeterminateSystems/update-flake-lock@5adeaaaf36f64df54f62adb34aa5fbfdb0109d34 # main + - uses: DeterminateSystems/update-flake-lock@a135ea602656a8348c5c34887131dd9f7a28bd8c # main with: pr-title: "Update flake.lock" pr-labels: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a267433f27ec2..0fea5547523a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,7 +163,7 @@ jobs: - name: cross setup if: contains(matrix.target, 'musl') - uses: taiki-e/cache-cargo-install-action@34ce5120836e5f9f1508d8713d7fdea0e8facd6f # v2 + uses: taiki-e/cache-cargo-install-action@2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f # v2 with: git: https://github.com/cross-rs/cross rev: baf457efc2555225af47963475bd70e8d2f5993f diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index 858d1c8c43f2c..64a55e13c08dd 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@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2 + - uses: taiki-e/install-action@f8d25fb8a2df08dcd3cead89780d572767b8655f # v2 with: tool: nextest - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index d24fbfca3d1a0..2eebe557e97fa 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@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2 + - uses: taiki-e/install-action@f8d25fb8a2df08dcd3cead89780d572767b8655f # v2 with: tool: nextest - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -91,4 +91,4 @@ jobs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} with: update_existing: true - filename: .github/FLAKY_TEST_FAILURE_TEMPLATE.md + filename: .github/FLAKY_TEST_ISOLATE_FAILURE_TEMPLATE.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index abb8b8363eece..81fd2aeff4884 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: toolchain: stable target: ${{ matrix.target }} - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@650c5ca14212efbbf3e580844b04bdccf68dac31 # v2 + - uses: taiki-e/install-action@f8d25fb8a2df08dcd3cead89780d572767b8655f # v2 with: tool: nextest diff --git a/Cargo.lock b/Cargo.lock index 7b638f17ed1a6..c8dfd6dc3f2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1958f0294ecc05ebe7b3c9a8662a3e221c2523b7f2bcd94c7a651efbd510bf" +checksum = "4e4ff99651d46cef43767b5e8262ea228cd05287409ccb0c947cc25e70a952f9" dependencies = [ "alloy-eips", "alloy-primitives", @@ -106,9 +106,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f752e99497ddc39e22d547d7dfe516af10c979405a034ed90e69b914b7dddeae" +checksum = "1a0701b0eda8051a2398591113e7862f807ccdd3315d0b441f06c2a0865a379b" dependencies = [ "alloy-consensus", "alloy-eips", @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2140796bc79150b1b7375daeab99750f0ff5e27b1f8b0aa81ccde229c7f02a2" +checksum = "f3c83c7a3c4e1151e8cac383d0a67ddf358f37e5ea51c95a1283d897c9de0a5a" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ff5ee5f27aa305bda825c735f686ad71bb65508158f059f513895abe69b8c3" +checksum = "e6ab1b2f1b48a7e6b3597cb2afae04f93879fb69d71e39736b5663d7366b23f2" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "alloy-eip5792" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6988f193c07204e04c6eb5e77e50b7cc964ec406c0d6d8f496bf374f37bd2918" +checksum = "8a8615856ba057cf04351edd50f04e71bd2556fa38e767509ae079a9427c352e" dependencies = [ "alloy-primitives", "alloy-serde", @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813a67f87e56b38554d18b182616ee5006e8e2bf9df96a0df8bf29dff1d52e3f" +checksum = "def1626eea28d48c6cc0a6f16f34d4af0001906e4f889df6c660b39c86fd044d" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -245,15 +245,15 @@ dependencies = [ "ethereum_ssz_derive", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", ] [[package]] name = "alloy-ens" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e43e421cb6e174846debd5fef802a59d68d409362c220b3366c850eb52e57" +checksum = "4f6784826e694087e974bdc35b0661062f0ebe7db18d414c510feb8d97eff050" dependencies = [ "alloy-contract", "alloy-primitives", @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96827207397445a919a8adc49289b53cc74e48e460411740bba31cec2fc307d" +checksum = "7b99ba7b74a87176f31ee1cd26768f7155b0eeff61ed925f59b13085ffe5f891" dependencies = [ "alloy-consensus", "alloy-eips", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05864eef929c4d28895ae4b4d8ac9c6753c4df66e873b9c8fafc8089b59c1502" +checksum = "55d9d1aba3f914f0e8db9e4616ae37f3d811426d95bdccf44e47d0605ab202f6" dependencies = [ "alloy-eips", "alloy-primitives", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8708475665cc00e081c085886e68eada2f64cfa08fc668213a9231655093d4de" +checksum = "1e414aa37b335ad2acb78a95814c59d137d53139b412f87aed1e10e2d862cd49" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dd146b3de349a6ffaa4e4e319ab3a90371fb159fb0bddeb1c7bbe8b1792eff" +checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c12278ffbb8872dfba3b2f17d8ea5e8503c2df5155d9bc5ee342794bde505c3" +checksum = "3b36c2a0ed74e48851f78415ca5b465211bd678891ba11e88fee09eac534bab1" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833037c04917bc2031541a60e8249e4ab5500e24c637c1c62e95e963a655d66f" +checksum = "636c8051da58802e757b76c3b65af610b95799f72423dc955737dec73de234fd" dependencies = [ "alloy-consensus", "alloy-eips", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "alloy-op-evm" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54dc5c46a92fc7267055a174d30efb34e2599a0047102a4d38a025ae521435ba" +checksum = "646a01ebc9778ee08bcc33cfa6714efc548f94e53de1aff6b34d9da55a2e5d41" dependencies = [ "alloy-consensus", "alloy-eips", @@ -411,9 +411,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b88cf92ed20685979ed1d8472422f0c6c2d010cec77caf63aaa7669cc1a7bc2" +checksum = "66b1483f8c2562bf35f0270b697d5b5fe8170464e935bd855a4c5eaf6f89b354" dependencies = [ "alloy-rlp", "arbitrary", @@ -421,8 +421,8 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "foldhash", - "getrandom 0.3.4", + "foldhash 0.2.0", + "getrandom 0.4.1", "hashbrown 0.16.1", "indexmap 2.13.0", "itoa", @@ -441,9 +441,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafa840b0afe01c889a3012bb2fde770a544f74eab2e2870303eb0a5fb869c48" +checksum = "b3dd56e2eafe8b1803e325867ac2c8a4c73c9fb5f341ffd8347f9344458c5922" dependencies = [ "alloy-chains", "alloy-consensus", @@ -486,9 +486,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b3a3b3e4efc9f4d30e3326b6bd6811231d16ef94837e18a802b44ca55119e6" +checksum = "6eebf54983d4fccea08053c218ee5c288adf2e660095a243d0532a8070b43955" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -508,9 +508,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -519,20 +519,20 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "alloy-rpc-client" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12768ae6303ec764905a8a7cd472aea9072f9f9c980d18151e26913da8ae0123" +checksum = "91577235d341a1bdbee30a463655d08504408a4d51e9f72edbfc5a622829f402" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -556,9 +556,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0622d8bcac2f16727590aa33f4c3f05ea98130e7e4b4924bce8be85da5ad0dae" +checksum = "79cff039bf01a17d76c0aace3a3a773d5f895eb4c68baaae729ec9da9e86c99c" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -572,9 +572,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8eb0e5d6c48941b61ab76fabab4af66f7d88309a98aa14ad3dec7911c1eba3" +checksum = "d22250cf438b6a3926de67683c08163bfa1fd1efa47ee9512cbcd631b6b0243c" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -584,9 +584,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1cf5a093e437dfd62df48e480f24e1a3807632358aad6816d7a52875f1c04aa" +checksum = "73234a141ecce14e2989748c04fcac23deee67a445e2c4c167cfb42d4dacd1b6" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -595,9 +595,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e07949e912479ef3b848e1cf8db54b534bdd7bc58e6c23f28ea9488960990c8c" +checksum = "625af0c3ebd3c31322edb1fb6b8e3e518acc39e164ed07e422eaff05310ff2fa" dependencies = [ "alloy-eips", "alloy-primitives", @@ -611,9 +611,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925ff0f48c2169c050f0ae7a82769bdf3f45723d6742ebb6a5efb4ed2f491b26" +checksum = "779f70ab16a77e305571881b07a9bc6b0068ae6f962497baf5762825c5b839fb" dependencies = [ "alloy-primitives", "derive_more", @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336ef381c7409f23c69f6e79bddc1917b6e832cff23e7a5cf84b9381d53582e6" +checksum = "10620d600cc46538f613c561ac9a923843c6c74c61f054828dcdb8dd18c72ec4" dependencies = [ "alloy-consensus", "alloy-eips", @@ -643,9 +643,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e97603095020543a019ab133e0e3dc38cd0819f19f19bdd70c642404a54751" +checksum = "010e101dbebe0c678248907a2545b574a87d078d82c2f6f5d0e8e7c9a6149a10" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -664,9 +664,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1aec4e1c66505d067933ea1a949a4fb60a19c4cfc2f109aa65873ea99e62ea8" +checksum = "be096f74d85e1f927580b398bf7bc5b4aa62326f149680ec0867e3c040c9aced" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b73c1d6e4f1737a20d246dad5a0abd6c1b76ec4c3d153684ef8c6f1b6bb4f4" +checksum = "14ab75189fbc29c5dd6f0bc1529bccef7b00773b458763f4d9d81a77ae4a1a2d" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -690,9 +690,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946a0d413dbb5cd9adba0de5f8a1a34d5b77deda9b69c1d7feed8fc875a1aa26" +checksum = "9e6d631f8b975229361d8af7b2c749af31c73b3cf1352f90e144ddb06227105e" dependencies = [ "alloy-primitives", "serde", @@ -701,9 +701,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7481dc8316768f042495eaf305d450c32defbc9bce09d8bf28afcd956895bb" +checksum = "97f40010b5e8f79b70bf163b38cd15f529b18ca88c4427c0e43441ee54e4ed82" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139b98be3eb3176f42f3bffafc4ef997b65b5b4c157ebc3013617054c0ef6964" +checksum = "e8857c6346edb898df606130a7d29b3dfc765746ccc7915b36466a1e16347b93" dependencies = [ "alloy-consensus", "alloy-network", @@ -737,9 +737,9 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48b61c2c6376ea877e79e44e6167643be8cc8d8609bf5024674c997222b438e9" +checksum = "787a9d7e155ee613f4f4ad0514b8c721c069497a494cfe52dc8b1e8ba29bfe2f" dependencies = [ "alloy-consensus", "alloy-network", @@ -755,9 +755,9 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66912ca4a4dc21191c97decb20069360493d8bcb57e7a65d04aad295331970b" +checksum = "3c85812b6ae80e3a5ba634cbf2c44835744a120e29768ced60ed81c54c8ef562" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -775,9 +775,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1259dac1f534a4c66c1d65237c89915d0010a2a91d6c3b0bada24dc5ee0fb917" +checksum = "9c4ec1cc27473819399a3f0da83bc1cef0ceaac8c1c93997696e46dc74377a58" dependencies = [ "alloy-consensus", "alloy-network", @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1aad3540b4b647cd1f1c29a454d0b665e681d8a9f5d7f0c3fdd7c470f50c0bf" +checksum = "b862c0e9f9acf839601b012313a9c4b619e35da0ea7c1b634af8df20ea2c00ab" dependencies = [ "alloy-consensus", "alloy-network", @@ -812,9 +812,9 @@ dependencies = [ [[package]] name = "alloy-signer-turnkey" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1089363e303973ab1602058bdcf05f537fb21f298e240cc0d5622a79114d93" +checksum = "66bd4f8c8ece153684392a054da16dc77d6c4754a3eb5004950f34c3ba5ea251" dependencies = [ "alloy-consensus", "alloy-network", @@ -828,23 +828,23 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fa1ca7e617c634d2bd9fa71f9ec8e47c07106e248b9fcbd3eaddc13cabd625" +checksum = "2c4b64c8146291f750c3f391dff2dd40cf896f7e2b253417a31e342aa7265baa" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c00c0c3a75150a9dc7c8c679ca21853a137888b4e1c5569f92d7e2b15b5102" +checksum = "d9df903674682f9bae8d43fdea535ab48df2d6a8cb5104ca29c58ada22ef67b3" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -855,15 +855,15 @@ dependencies = [ "proc-macro2", "quote", "sha3", - "syn 2.0.114", + "syn 2.0.115", "syn-solidity", ] [[package]] name = "alloy-sol-macro-input" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297db260eb4d67c105f68d6ba11b8874eec681caec5505eab8fbebee97f790bc" +checksum = "737b8a959f527a86e07c44656db237024a32ae9b97d449f788262a547e8aa136" dependencies = [ "alloy-json-abi", "const-hex", @@ -873,15 +873,15 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.114", + "syn 2.0.115", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b91b13181d3bcd23680fd29d7bc861d1f33fbe90fdd0af67162434aeba902d" +checksum = "b28e6e86c6d2db52654b65a5a76b4f57eae5a32a7f0aa2222d1dbdb74e2cb8e0" dependencies = [ "serde", "winnow", @@ -889,9 +889,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc442cc2a75207b708d481314098a0f8b6f7b58e3148dd8d8cc7407b0d6f9385" +checksum = "fdf7effe4ab0a4f52c865959f790036e61a7983f68b13b75d7fbcedf20b753ce" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -901,9 +901,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f169b85eb9334871db986e7eaf59c58a03d86a30cc68b846573d47ed0656bb" +checksum = "a03bb3f02b9a7ab23dacd1822fa7f69aa5c8eefcdcf57fad085e0b8d76fb4334" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -924,12 +924,13 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019821102e70603e2c141954418255bec539ef64ac4117f8e84fb493769acf73" +checksum = "5ce599598ef8ebe067f3627509358d9faaa1ef94f77f834a7783cd44209ef55c" dependencies = [ "alloy-json-rpc", "alloy-transport", + "itertools 0.14.0", "reqwest", "serde_json", "tower", @@ -939,9 +940,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e574ca2f490fb5961d2cdd78188897392c46615cd88b35c202d34bbc31571a81" +checksum = "49963a2561ebd439549915ea61efb70a7b13b97500ec16ca507721c9d9957d07" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -959,9 +960,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92dea6996269769f74ae56475570e3586910661e037b7b52d50c9641f76c68f" +checksum = "5ed38ea573c6658e0c2745af9d1f1773b1ed83aa59fbd9c286358ad469c3233a" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -976,9 +977,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428aa0f0e0658ff091f8f667c406e034b431cb10abd39de4f507520968acc499" +checksum = "4d7fd448ab0a017de542de1dcca7a58e7019fe0e7a34ed3f9543ebddf6aceffa" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -987,19 +988,20 @@ dependencies = [ "nybbles", "serde", "smallvec", + "thiserror 2.0.18", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.5.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ceac797eb8a56bdf5ab1fab353072c17d472eab87645ca847afe720db3246d" +checksum = "397406cf04b11ca2a48e6f81804c70af3f40a36abf648e11dc7416043eb0834d" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1228,9 +1230,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -1371,7 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1409,7 +1411,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1498,7 +1500,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1557,9 +1559,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" +checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" dependencies = [ "compression-codecs", "compression-core", @@ -1595,7 +1597,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1606,7 +1608,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1659,7 +1661,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1670,9 +1672,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.12" +version = "1.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1700,9 +1702,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1722,9 +1724,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -1734,9 +1736,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.18" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959dab27ce613e6c9658eb3621064d0e2027e5f2acb65bc526a43577facea557" +checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1747,20 +1749,21 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] name = "aws-sdk-kms" -version = "1.98.0" +version = "1.100.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c74fef3d08159467cad98300f33a2e3bd1a985d527ad66ab0ea83c95e3a615" +checksum = "723700afe7459a33d1ac30852e9208b801946c032625cc8c808f57b9563bb5c7" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1775,15 +1778,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "1.92.0" +version = "1.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d63bd2bdeeb49aa3f9b00c15e18583503b778b2e792fc06284d54e7d5b6566" +checksum = "699da1961a289b23842d88fe2984c6ff68735fdf9bdcbc69ceaeb2491c9bf434" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1798,15 +1802,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.94.0" +version = "1.96.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532d93574bf731f311bafb761366f9ece345a0416dbcc273d81d6d1a1205239b" +checksum = "e3e3a4cb3b124833eafea9afd1a6cc5f8ddf3efefffc6651ef76a03cbc6b4981" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1821,15 +1826,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.96.0" +version = "1.98.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357e9a029c7524db6a0099cd77fbd5da165540339e7296cca603531bc783b56c" +checksum = "89c4f19655ab0856375e169865c91264de965bd74c407c7f1e403184b1049409" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1845,15 +1851,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -1866,16 +1873,16 @@ dependencies = [ "http 0.2.12", "http 1.4.0", "percent-encoding", - "sha2", + "sha2 0.10.9", "time", "tracing", ] [[package]] name = "aws-smithy-async" -version = "1.2.10" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39f47abe8641e434de98e047e85ced629862e7ab719b6914a846796ceb289e2" +checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" dependencies = [ "futures-util", "pin-project-lite", @@ -1884,9 +1891,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -1894,9 +1901,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -1905,9 +1912,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a395c914b1ff95db3cb7003ea4fd19432343af698a9b5028a7d35f8e712240a1" +checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1929,27 +1936,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7764bc1dfdb71157bc481528a649e617ed8c9c8aa93c0e8b01087133677cfc8e" +checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.12" +version = "0.60.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786bbf4434ed3e3413c5a1741abf53a0372dd48124ddb2091e6bff1b1b2582b5" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" dependencies = [ "aws-smithy-types", "urlencoding", @@ -1957,9 +1964,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.8" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1" +checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -1973,6 +1980,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -1981,9 +1989,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.2" +version = "1.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b743e9aab0b8d50a9a40eebedf974fcfe3621032e07c6388d1c7821b155b7b0" +checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1998,9 +2006,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.2" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0828575b70da70406b4cdb5d4afe0afe725f72245f04d34f02e0fb5ebd6fc872" +checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" dependencies = [ "base64-simd", "bytes", @@ -2021,18 +2029,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.13" +version = "0.60.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -2197,9 +2205,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -2217,6 +2225,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -2253,7 +2270,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc119a5ad34c3f459062a96907f53358989b173d104258891bb74f95d93747e8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "boa_interner", "boa_macros", "boa_string", @@ -2270,7 +2287,7 @@ checksum = "e637ec52ea66d76b0ca86180c259d6c7bb6e6a6e14b2f36b85099306d8b00cc3" dependencies = [ "aligned-vec", "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "boa_ast", "boa_gc", "boa_interner", @@ -2352,7 +2369,7 @@ dependencies = [ "cow-utils", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -2362,7 +2379,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02f99bf5b684f0de946378fcfe5f38c3a0fbd51cbf83a0f39ff773a0e218541f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "boa_ast", "boa_interner", "boa_macros", @@ -2390,9 +2407,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.8.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234655ec178edd82b891e262ea7cf71f6584bcd09eff94db786be23f1821825c" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" dependencies = [ "bon-macros", "rustversion", @@ -2400,9 +2417,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.8.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" dependencies = [ "darling 0.23.0", "ident_case", @@ -2410,7 +2427,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2433,7 +2450,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2448,7 +2465,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2", + "sha2 0.10.9", "tinyvec", ] @@ -2492,7 +2509,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2640,9 +2657,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -2756,9 +2773,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -2776,9 +2793,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -2791,9 +2808,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.5.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" dependencies = [ "clap", ] @@ -2817,14 +2834,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clearscreen" @@ -2883,7 +2900,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -2899,7 +2916,7 @@ dependencies = [ "once_cell", "pbkdf2 0.12.2", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -2917,7 +2934,7 @@ dependencies = [ "generic-array", "ripemd", "serde", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", ] @@ -3248,7 +3265,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", @@ -3308,15 +3325,28 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.30.1", + "nix 0.31.1", "windows-sys 0.61.2", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.20.11" @@ -3358,7 +3388,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3373,7 +3403,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3386,7 +3416,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3397,7 +3427,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3408,7 +3438,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3419,7 +3449,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3455,9 +3485,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", "serde_core", @@ -3482,7 +3512,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3493,7 +3523,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3514,7 +3544,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3524,7 +3554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3546,7 +3576,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.114", + "syn 2.0.115", "unicode-xid", ] @@ -3582,7 +3612,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -3615,7 +3645,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -3629,7 +3659,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3688,7 +3718,7 @@ checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3706,6 +3736,21 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "educe" version = "0.6.0" @@ -3715,7 +3760,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3781,9 +3826,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" dependencies = [ "log", ] @@ -3826,14 +3871,14 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -3847,9 +3892,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -3875,7 +3920,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -3927,7 +3972,7 @@ dependencies = [ "scrypt", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", "uuid 0.8.2", @@ -3985,7 +4030,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -4136,9 +4181,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -4161,6 +4206,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -4245,7 +4296,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit 0.24.1+spec-1.1.0", "tower-http", "tracing", "url", @@ -4380,7 +4431,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -4484,6 +4535,7 @@ dependencies = [ "base64 0.22.1", "dialoguer", "ecdsa", + "ed25519-consensus", "eyre", "forge-script-sequence", "foundry-cheatcodes-spec", @@ -4493,6 +4545,7 @@ dependencies = [ "foundry-evm-core", "foundry-evm-fuzz", "foundry-evm-traces", + "foundry-primitives", "foundry-wallets", "itertools 0.14.0", "jsonpath_lib", @@ -4520,7 +4573,7 @@ version = "1.6.0" dependencies = [ "alloy-sol-types", "foundry-macros", - "schemars 1.2.0", + "schemars 1.2.1", "serde", "serde_json", ] @@ -4681,7 +4734,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "solar-compiler", "svm-rs", "svm-rs-builds", @@ -4795,7 +4848,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "toml", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit 0.24.1+spec-1.1.0", "tracing", "unit-prefix", "walkdir", @@ -4851,7 +4904,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -4996,9 +5049,9 @@ dependencies = [ [[package]] name = "foundry-fork-db" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a94e30b64a91d72bbda706e3e13774b7f660015eb1246d73fc03e405fd75e7" +checksum = "ad537243394b8cda523e63749f2c9833be4c612500d35518efad34a3d1086710" dependencies = [ "alloy-chains", "alloy-consensus", @@ -5036,7 +5089,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -5060,6 +5113,7 @@ dependencies = [ "revm", "serde", "serde_json", + "tempo-alloy", "tempo-primitives", ] @@ -5131,18 +5185,16 @@ dependencies = [ "eyre", "foundry-common", "foundry-config", - "foundry-primitives", "reqwest", "rpassword", "serde", "serde_json", - "tempo-primitives", "thiserror 2.0.18", "tokio", "tower", "tower-http", "tracing", - "uuid 1.20.0", + "uuid 1.21.0", "webbrowser", ] @@ -5265,7 +5317,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -5385,6 +5437,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "getset" version = "0.1.6" @@ -5394,7 +5459,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -5498,6 +5563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", + "foldhash 0.1.5", ] [[package]] @@ -5508,7 +5574,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", "serde_core", ] @@ -5700,7 +5766,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -5718,14 +5784,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -5855,6 +5920,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -5925,7 +5996,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -5966,9 +6037,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console 0.16.2", "portable-atomic", @@ -6008,7 +6079,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -6041,14 +6112,14 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "interprocess" -version = "2.2.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +checksum = "53bf2b0e0785c5394a7392f66d7c4fb9c653633c29b27a932280da3cb344c66a" dependencies = [ "doctest-file", "futures-core", @@ -6158,9 +6229,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" dependencies = [ "jiff-static", "log", @@ -6171,13 +6242,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -6259,7 +6330,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2", + "sha2 0.10.9", "signature", ] @@ -6276,9 +6347,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -6350,6 +6421,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "levenshtein" version = "1.0.5" @@ -6384,7 +6461,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", ] @@ -6406,7 +6483,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -6487,7 +6564,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -6575,7 +6652,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tracing", ] @@ -6630,9 +6707,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -6682,7 +6759,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -6761,7 +6838,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -6803,7 +6880,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" dependencies = [ - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -6834,7 +6911,19 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -6877,7 +6966,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -6895,7 +6984,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -7042,7 +7131,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7056,9 +7145,9 @@ dependencies = [ [[package]] name = "nybbles" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5676b5c379cf5b03da1df2b3061c4a4e2aa691086a56ac923e08c143f53f59" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" dependencies = [ "alloy-rlp", "cfg-if", @@ -7089,7 +7178,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -7234,7 +7323,7 @@ dependencies = [ "ethereum_ssz_derive", "op-alloy-consensus", "serde", - "sha2", + "sha2 0.10.9", "snap", "thiserror 2.0.18", ] @@ -7250,6 +7339,12 @@ dependencies = [ "serde", ] +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "opener" version = "0.8.4" @@ -7295,7 +7390,7 @@ dependencies = [ "elliptic-curve", "primeorder", "serdect", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -7323,7 +7418,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7413,9 +7508,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -7423,9 +7518,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -7433,25 +7528,25 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -7545,7 +7640,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7558,7 +7653,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7616,7 +7711,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7694,9 +7789,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "predicates-core", @@ -7704,15 +7799,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -7744,7 +7839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7796,7 +7891,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7810,13 +7905,13 @@ dependencies = [ [[package]] name = "process-wrap" -version = "9.0.1" +version = "9.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1395947e69c07400ef4d43db0051d6f773c21f647ad8b97382fc01f0204c60" +checksum = "ccd9713fe2c91c3c85ac388b31b89de339365d2c995146e630b5e0da9d06526a" dependencies = [ "futures", "indexmap 2.13.0", - "nix 0.30.1", + "nix 0.31.1", "tokio", "tracing", "windows", @@ -7824,13 +7919,13 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -7843,13 +7938,13 @@ dependencies = [ [[package]] name = "proptest-derive" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" +checksum = "fb6dc647500e84a25a85b100e76c85b8ace114c209432dc174f20aac11d4ed6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7892,7 +7987,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7905,7 +8000,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7918,7 +8013,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -7974,7 +8069,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "memchr", "pulldown-cmark-escape", "unicase", @@ -8004,7 +8099,7 @@ dependencies = [ "quick-xml 0.38.4", "strip-ansi-escapes", "thiserror 2.0.18", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -8184,9 +8279,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.2.1" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7" +checksum = "84816e4c99c467e92cf984ee6328caa976dfecd33a673544489d79ca2caaefe5" dependencies = [ "rand 0.9.2", "rustversion", @@ -8210,7 +8305,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "compact_str", "hashbrown 0.16.1", "indoc", @@ -8242,7 +8337,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.16.1", "indoc", "instability", @@ -8287,7 +8382,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -8318,14 +8413,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -8335,9 +8430,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -8346,15 +8441,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "regress" @@ -8410,7 +8505,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -8438,7 +8533,7 @@ source = "git+https://github.com/paradigmxyz/reth?rev=b25f32a977b489f9b84254c781 dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -8686,7 +8781,7 @@ dependencies = [ "revm-primitives 22.0.0", "ripemd", "secp256k1 0.31.1", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -8719,7 +8814,7 @@ version = "8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8be953b7e374dbdea0773cf360debed8df394ea8d82a8b240a6b5da37592fc" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "revm-bytecode 7.1.1", "revm-primitives 21.0.2", "serde", @@ -8732,7 +8827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "311720d4f0f239b041375e7ddafdbd20032a33b7bae718562ea188e188ed9fd3" dependencies = [ "alloy-eip7928", - "bitflags 2.10.0", + "bitflags 2.11.0", "revm-bytecode 8.0.0", "revm-primitives 22.0.0", "serde", @@ -8934,7 +9029,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -9015,7 +9110,7 @@ version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "clipboard-win", "fd-lock", @@ -9033,9 +9128,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "ryu-js" @@ -9093,9 +9188,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -9106,14 +9201,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9137,7 +9232,7 @@ dependencies = [ "hmac", "pbkdf2 0.11.0", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9211,11 +9306,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -9224,9 +9319,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", @@ -9308,7 +9403,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9319,7 +9414,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9389,7 +9484,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -9405,7 +9500,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9429,6 +9524,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -9550,9 +9658,9 @@ dependencies = [ [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", @@ -9737,7 +9845,7 @@ source = "git+https://github.com/paradigmxyz/solar?rev=530f129#530f129b1b2d7138d dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9746,7 +9854,7 @@ version = "0.1.8" source = "git+https://github.com/paradigmxyz/solar?rev=530f129#530f129b1b2d7138df973dd71d2fc1e592b593d7" dependencies = [ "alloy-primitives", - "bitflags 2.10.0", + "bitflags 2.11.0", "bumpalo", "itertools 0.14.0", "memchr", @@ -9768,7 +9876,7 @@ source = "git+https://github.com/paradigmxyz/solar?rev=530f129#530f129b1b2d7138d dependencies = [ "alloy-json-abi", "alloy-primitives", - "bitflags 2.10.0", + "bitflags 2.11.0", "bumpalo", "derive_more", "either", @@ -9828,11 +9936,11 @@ dependencies = [ "semver 1.0.27", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "tokio", "toml_edit 0.23.10+spec-1.0.0", - "uuid 1.20.0", + "uuid 1.21.0", "zip", "zip-extract", ] @@ -9945,7 +10053,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -9954,17 +10062,23 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "sval" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" +checksum = "c1aaf178a50bbdd86043fce9bf0a5867007d9b382db89d1c96ccae4601ff1ff9" [[package]] name = "sval_buffer" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" +checksum = "f89273e48f03807ebf51c4d81c52f28d35ffa18a593edf97e041b52de143df89" dependencies = [ "sval", "sval_ref", @@ -9972,18 +10086,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" +checksum = "0430f4e18e7eba21a49d10d25a8dec3ce0e044af40b162347e99a8e3c3ced864" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" +checksum = "835f51b9d7331b9d7fc48fc716c02306fa88c4a076b1573531910c91a525882d" dependencies = [ "itoa", "ryu", @@ -9992,9 +10106,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" +checksum = "13cbfe3ef406ee2366e7e8ab3678426362085fa9eaedf28cb878a967159dced3" dependencies = [ "itoa", "ryu", @@ -10003,9 +10117,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" +checksum = "8b20358af4af787c34321a86618c3cae12eabdd0e9df22cd9dd2c6834214c518" dependencies = [ "sval", "sval_buffer", @@ -10014,18 +10128,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" +checksum = "fb5e500f8eb2efa84f75e7090f7fc43f621b9f8b6cde571c635b3855f97b332a" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" +checksum = "ca2032ae39b11dcc6c18d5fbc50a661ea191cac96484c59ccf49b002261ca2c1" dependencies = [ "serde_core", "sval", @@ -10044,7 +10158,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "url", @@ -10076,9 +10190,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -10087,14 +10201,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2379beea9476b89d0237078be761cf8e012d92d5ae4ae0c9a329f974838870fc" +checksum = "f8658017776544996edc21c8c7cc8bb4f13db60955382f4bac25dc6303b38438" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -10114,16 +10228,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -10152,17 +10266,49 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "tempo-alloy" +version = "1.0.0" +source = "git+https://github.com/tempoxyz/tempo?tag=v1.0.0#9ad04e2058993dfba4984cc7d413474b0278d427" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "derive_more", + "serde", + "tempo-contracts", + "tempo-primitives", +] + +[[package]] +name = "tempo-contracts" +version = "1.0.0" +source = "git+https://github.com/tempoxyz/tempo?tag=v1.0.0#9ad04e2058993dfba4984cc7d413474b0278d427" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-sol-types", +] + [[package]] name = "tempo-primitives" version = "1.0.0" @@ -10181,7 +10327,7 @@ dependencies = [ "reth-primitives-traits", "serde", "serde_json", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -10275,7 +10421,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -10286,7 +10432,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -10329,9 +10475,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -10353,9 +10499,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -10412,7 +10558,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -10480,9 +10626,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", @@ -10519,9 +10665,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.24.0+spec-1.1.0" +version = "0.24.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" +checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" dependencies = [ "indexmap 2.13.0", "toml_datetime", @@ -10532,9 +10678,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" dependencies = [ "winnow", ] @@ -10608,7 +10754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -10674,7 +10820,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -10943,9 +11089,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-joining-type" @@ -11076,11 +11222,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -11237,6 +11383,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +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", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -11283,7 +11438,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "wasm-bindgen-shared", ] @@ -11296,6 +11451,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -11309,6 +11486,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -11412,9 +11601,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.1", "jni", @@ -11432,14 +11621,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -11545,7 +11734,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -11556,7 +11745,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -11864,6 +12053,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.115", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.115", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "write16" @@ -11951,28 +12222,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -11992,7 +12263,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -12013,7 +12284,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -12047,7 +12318,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -12077,15 +12348,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" [[package]] name = "zmij" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 06a320a24b358..506e88164bc46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,6 @@ ruint.opt-level = 3 sha2.opt-level = 3 sha3.opt-level = 3 -tiny-keccak.opt-level = 3 keccak.opt-level = 3 # Fuzzing. @@ -280,7 +279,7 @@ foundry-compilers = { version = "0.19.14", default-features = false, features = "rustls", "svm-solc", ] } -foundry-fork-db = "0.22" +foundry-fork-db = "0.23" 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 = [ @@ -442,6 +441,7 @@ ethereum_ssz = "0.10" # Tempo tempo-primitives = { git = "https://github.com/tempoxyz/tempo", tag = "v1.0.0", default-features = false, features = ["serde"] } +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", tag = "v1.0.0", default-features = false } ## Pinned dependencies. Enabled for the workspace in crates/test-utils. diff --git a/Dockerfile b/Dockerfile index 6240487f6838e..5c402cce672f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,8 @@ COPY . . RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \ --mount=type=cache,target=/usr/local/cargo/git,sharing=shared \ --mount=type=cache,target=$SCCACHE_DIR,sharing=shared \ - cargo build --profile ${RUST_PROFILE} --no-default-features --features "${RUST_FEATURES}" + cargo build --profile ${RUST_PROFILE} --no-default-features --features "${RUST_FEATURES}" \ + && sccache --show-stats || true # `dev` profile outputs to the `target/debug` directory. RUN ln -s /app/target/debug /app/target/dev \ @@ -51,8 +52,6 @@ RUN ln -s /app/target/debug /app/target/dev \ /app/target/${RUST_PROFILE}/chisel \ /app/output/ -RUN sccache --show-stats || true - FROM ubuntu:22.04 AS runtime # Install runtime dependencies. diff --git a/README.md b/README.md index 4cb63bb467ebc..c9f0a45c57b0a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@   [![Github Actions][gha-badge]][gha-url] [![Telegram Chat][tg-badge]][tg-url] [![Telegram Support][tg-support-badge]][tg-support-url] -![Foundry](https://img.shields.io/badge/Foundry-grey?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAElElEQVR4nH1VUUhUaRg9984YdzBpkqR0Z210rIESIXSabEbcHgydrpNRRj00kWaztj0U1MOW0MOIbD300IvLMqBpMTGYxdoqyoRNDUESBDWwUuPugCSSsTM7u0Oj1/+efdiMcmnP2/fDd77D4f/OB6xCa2urQZbllVICYGtqanK1tLS4AdgAyAAgyzJaW1sNq/ulT4twOGw4fPiwAGDp7Ow8VV1d7bVarRWxWCw/k8mgsbExm0wmZ+Lx+M/Xr1//CcAsSVmSJH01McLhsAEAnE5nx+Tk5B/xeJxOp5N9fX2sqqqixWLhnTt36HA4GIvFGI1GU3V1df5Pe/9D1t7eHkgkEuzo6GBPT49WWloq7Ha7fujQITocDu7atUs3m83i6tWr2okTJ/jixQuePn265zPScDhskGUZe/fubXv8+DFv3rypbdiwQaxbt46RSIT79u3j0NAQb926RVVVOT4+TqvVyvz8fD0YDC5NTk6ysbHxlCRJ/5KSlAAURyKRTFNTkwAg7t69S5/Px76+Pq7GyMgI9+/fz9HRUQIQO3bsEKOjo38DsJCUJADw+/0BVVW7otHo8ps3b4yvXr3CxMQETCYTTCYTNE0DAOTl5SGXy0FRFOzZswdmsxkVFRXLNTU1xmg0+kNvb+/3AGAcGBiI7969Wwcg6urq+OTJE967d49btmzh9PT0R3WJRIKBQIDBYJBTU1NsaGggAGGz2fTe3t5fAeQZAWwuLi4uP3nypOT1emEwGFBeXo7a2losLCygoaEB/f39MJlMCIVCkCQJBw8ehNVqhcfjQXNzs1RSUiKtX7++DEAZqqqq3KFQiABYUFDAM2fOkCQXFxdJkvfv32dhYSG9Xi+vXbvG2dnZj4oDgQCLioqoKAqHhobodDq/Mc7NzUklJSUIBoOw2WzYtm0blpeXsWbNGkxMTODp06doa2vD4OAgNm7cCIvFApLQdR3nzp3Dzp078fLlSxQVFeHdu3cAgIpHjx69/zBUX5k+MDBAt9vNY8eOsbu7m6lUigcOHKDL5WImkyHJz9TGYrEcALsMIPn69esZTdMIgM+ePUNXVxdu376NsrIyuN1uXLp0CWazGcPDw3C5XFBVFWfPnkVNTQ18Pp+ezWY5MzPzO4DfAABHjhzpJslUKqVdvHiR4+PjbG9vZy6XI0kuLS0xmUxSCEGS9Pv9LC0tpdFoZGVlpSaEoM/nuwIAKx/7q5GRkb9CoZBQVVWcP3+ez58/J0mm02kODg7ywoULjMViTKfTtNvtXLt2LTdt2qTncrnlsbGxLICvSUqfrl5HJBLh1NTUkhBCJ8mFhQX29/dTVVUWFBTwwYMH1HWdly9fpqIoeiKRWJqfn2d1dXWnLMuf7zMAHD16tGd+fn7FZy2bzYrKykodAAFQVVV9cXFRkNTevn3Lubk5trS0XPnfxHE4HN8ODw+nV/yanp6mx+Ohx+P5aIMQgmNjY3/W1tZ+t5rsSwG7+fjx4/76+vrm7du32woLC00AkE6n38fj8ZmHDx/+cuPGjR8BJL8YsCtYdQIMALYqilKvKEo9APuHty+egH8A3GfFDJXmxmMAAAAASUVORK5CYII%3D&link=https%3A%2F%2Fbook.getfoundry.sh%2F) -[gha-badge]: https://img.shields.io/github/actions/workflow/status/foundry-rs/foundry/test.yml?branch=master +[gha-badge]: https://img.shields.io/github/actions/workflow/status/foundry-rs/foundry/test.yml?branch=master&style=flat-square [gha-url]: https://github.com/foundry-rs/foundry/actions [tg-badge]: https://img.shields.io/endpoint?color=neon&logo=telegram&label=chat&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Ffoundry_rs [tg-url]: https://t.me/foundry_rs @@ -15,6 +14,7 @@ **[Install](https://getfoundry.sh/getting-started/installation)** | [Docs][foundry-docs] +| [Benchmarks](https://www.getfoundry.sh/benchmarks) | [Developer Guidelines](./docs/dev/README.md) | [Contributing](./CONTRIBUTING.md) | [Crate Docs](https://foundry-rs.github.io/foundry) @@ -23,305 +23,58 @@ --- -### Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. +Blazing fast, portable and modular toolkit for Ethereum application development, written in Rust. -Foundry consists of: - -- [**Forge**](#forge): Build, test, fuzz, debug and deploy [Solidity][solidity] contracts, like Hardhat, Brownie, Ape. -- [**Cast**](#cast): A Swiss Army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- [**Anvil**](#anvil): Fast local Ethereum development node, akin to Hardhat Network, Tenderly. -- [**Chisel**](#chisel): Fast, utilitarian, and verbose Solidity REPL. - -**Need help getting started with Foundry? Read the [📖 Foundry Docs][foundry-docs]!** +- [**Forge**](https://getfoundry.sh/forge) — Build, test, fuzz, debug and deploy Solidity contracts. +- [**Cast**](https://getfoundry.sh/cast) — Swiss Army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- [**Anvil**](https://getfoundry.sh/anvil) — Fast local Ethereum development node. +- [**Chisel**](https://getfoundry.sh/chisel) — Fast, utilitarian and verbose Solidity REPL. ![Demo](.github/assets/demo.gif) -## Features - -- **High-Performance Compilation** - - - **Fast and Flexible**: Automatically detects and installs the required Solidity compiler version. - - **Solidity and Vyper Support**: Fully supports both Solidity and Vyper out-of-the-box. - - **Incremental Compilation**: Re-compiles only changed files, saving time. - - **Parallelized Pipeline**: Leverages multi-core systems for ultra-fast builds. - - **Broad Compatibility**: Supports non-standard directory structures, including [Hardhat repos](https://twitter.com/gakonst/status/1461289225337421829). - -- **Advanced Testing** - - - **No Context Switching**: Write tests directly in Solidity. - - **Fuzz Testing**: Quickly identify edge cases with input shrinking and counter-example generation. - - **Invariant Testing**: Ensure complex system properties hold across a wide range of inputs. - - **Debugging Made Easy**: Use [forge-std](https://github.com/foundry-rs/forge-std)'s `console.sol` for flexible debug logging. - - **Interactive Debugger**: Step through your Solidity code with Foundry's interactive debugger, making it easy to pinpoint issues. - -- **Powerful Runtime Features** - - - **RPC Forking**: Fast and efficient remote RPC forking backed by [Alloy][alloy]. - - **Lightweight & Portable**: No dependency on Nix or other package managers for installation. - -- **Streamlined CI/CD** - - - **Optimized CI**: Accelerate builds, run tests and execute scripts using [Foundry's GitHub action][foundry-gha]. - ## Installation -Getting started is very easy: - -Install `foundryup`: - -``` +```sh curl -L https://foundry.paradigm.xyz | bash -``` - -Next, run `foundryup`. - -It will automatically install the latest version of the precompiled binaries: [`forge`](#forge), [`cast`](#cast), [`anvil`](#anvil), and [`chisel`](#chisel). - -``` foundryup ``` -**Done!** - -For additional details see the [installation guide](https://getfoundry.sh/getting-started/installation) in the [Foundry Docs][foundry-docs]. - -If you're experiencing any issues while installing, check out [Getting Help](#getting-help) and the [FAQ](https://getfoundry.sh/faq). - -## How Fast? - -Forge is quite fast at both compiling (leveraging `solc` with [foundry-compilers]) and testing. - -See the benchmarks below. Older benchmarks against [DappTools][dapptools] can be found in the [v0.2.0 announcement post][benchmark-post] and in the [Convex Shutdown Simulation][convex] repository. - -### Testing Benchmarks - -| Project | Type | [Forge 1.0][foundry-1.0] | [Forge 0.2][foundry-0.2] | DappTools | Speedup | -| --------------------------------------------- | -------------------- | ------------------------ | ------------------------ | --------- | -------------- | -| [vectorized/solady][solady] | Unit / Fuzz | 0.9s | 2.3s | - | 2.6x | -| [morpho-org/morpho-blue][morpho-blue] | Invariant | 0.7s | 1m43s | - | 147.1x | -| [morpho-org/morpho-blue-oracles][morpho-blue] | Integration (Cold) | 6.1s | 6.3s | - | 1.04x | -| [morpho-org/morpho-blue-oracles][morpho-blue] | Integration (Cached) | 0.6s | 0.9s | - | 1.50x | -| [transmissions11/solmate][solmate] | Unit / Fuzz | 2.7s | 2.8s | 6m34s | 1.03x / 140.0x | -| [reflexer-labs/geb][geb] | Unit / Fuzz | 0.2s | 0.4s | 23s | 2.0x / 57.5x | - -_In the above benchmarks, compilation was always skipped_ - -**Takeaway: Forge dramatically outperforms the competition, delivering blazing-fast execution speeds while continuously expanding its robust feature set.** - -### Compilation Benchmarks +See the [installation guide](https://getfoundry.sh/getting-started/installation) for more details. -
- - - - - - - - - - -  - -
- -**Takeaway: Forge compilation is consistently faster than Hardhat by a factor of `2.1x` to `5.2x`, depending on the amount of caching involved.** - -## Forge +## Getting Started -Forge helps you build, test, fuzz, debug and deploy Solidity contracts. - -The best way to understand Forge is to simply try it (in less than 30 seconds!). - -First, let's initialize a new `counter` example repository: - -```sh -forge init counter -``` - -Next `cd` into `counter` and build: +Initialize a new project, build and test: ```sh +forge init counter && cd counter forge build -``` - -```console -[⠊] Compiling... -[⠔] Compiling 27 files with Solc 0.8.28 -[⠒] Solc 0.8.28 finished in 452.13ms -Compiler run successful! -``` - -Let's [test](https://getfoundry.sh/forge/tests#tests) our contracts: - -```sh forge test ``` -```console -[⠊] Compiling... -No files changed, compilation skipped - -Ran 2 tests for test/Counter.t.sol:CounterTest -[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 31121, ~: 31277) -[PASS] test_Increment() (gas: 31293) -Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.35ms (4.86ms CPU time) - -Ran 1 test suite in 5.91ms (5.35ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests) -``` - -Finally, let's run our deployment script: - -```sh -forge script script/Counter.s.sol -``` - -```console -[⠊] Compiling... -No files changed, compilation skipped -Script ran successfully. -Gas used: 109037 - -If you wish to simulate on-chain transactions pass a RPC URL. -``` - -Run `forge --help` to explore the full list of available subcommands and their usage. - -More documentation can be found in the [forge](https://getfoundry.sh/forge/overview) section of the Foundry Docs. - -## Cast - -Cast is a Swiss Army knife for interacting with Ethereum applications from the command line. - -Here are a few examples of what you can do: - -**Check the latest block on Ethereum Mainnet**: +Interact with a live network: ```sh cast block-number --rpc-url https://eth.merkle.io -``` - -**Check the Ether balance of `vitalik.eth`** - -```sh cast balance vitalik.eth --ether --rpc-url https://eth.merkle.io ``` -**Replay and trace a transaction** - -```sh -cast run 0x9c32042f5e997e27e67f82583839548eb19dc78c4769ad6218657c17f2a5ed31 --rpc-url https://eth.merkle.io -``` - -Optionally, pass `--etherscan-api-key ` to decode transaction traces using verified source maps, providing more detailed and human-readable information. - ---- - -Run `cast --help` to explore the full list of available subcommands and their usage. - -More documentation can be found in the [cast](https://getfoundry.sh/cast/overview) section of the Foundry Docs. - -## Anvil - -Anvil is a fast local Ethereum development node. - -Let's fork Ethereum mainnet at the latest block: +Fork mainnet locally: ```sh anvil --fork-url https://eth.merkle.io ``` -You can use those same `cast` subcommands against your `anvil` instance: - -```sh -cast block-number -``` - ---- - -Run `anvil --help` to explore the full list of available features and their usage. - -More documentation can be found in the [anvil](https://getfoundry.sh/anvil/overview) section of the Foundry Docs. - -## Chisel - -Chisel is a fast, utilitarian, and verbose Solidity REPL. - -To use Chisel, simply type `chisel`. - -```sh -chisel -``` - -From here, start writing Solidity code! Chisel will offer verbose feedback on each input. - -Create a variable `a` and query it: - -```console -➜ uint256 a = 123; -➜ a -Type: uint256 -├ Hex: 0x7b -├ Hex (full word): 0x000000000000000000000000000000000000000000000000000000000000007b -└ Decimal: 123 -``` - -Finally, run `!source` to see `a` was applied: - -```solidity -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; - -import {Vm} from "forge-std/Vm.sol"; - -contract REPL { - Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - /// @notice REPL contract entry point - function run() public { - uint256 a = 123; - } -} -``` - ---- - -Run `chisel --help` to explore the full list of available features and their usage. - -More documentation can be found in the [chisel](https://getfoundry.sh/chisel/overview) section of the Foundry Docs. - -## Configuration - -Foundry is highly configurable, allowing you to tailor it to your needs. Configuration is managed via a file called [`foundry.toml`](./crates/config) located in the root of your project or any parent directory. For a full list of configuration options, refer to the [config package documentation](./crates/config/README.md#all-options). - -You can find additional [setup and configurations guides](https://getfoundry.sh/config/overview) in the [Foundry Docs][foundry-docs] and in the [config crate](./crates/config/README.md): - -- [Configuring with `foundry.toml`](https://getfoundry.sh/config/overview) -- [Setting up VSCode][vscode-setup] -- [Shell autocompletions][shell-setup] - -**Profiles and Namespaces** - -- Configuration can be organized into **profiles**, which are arbitrarily namespaced for flexibility. -- The default profile is named `default`. Learn more in the [Default Profile section](./crates/config/README.md#default-profile). -- To select a different profile, set the `FOUNDRY_PROFILE` environment variable. -- Override specific settings using environment variables prefixed with `FOUNDRY_` (e.g., `FOUNDRY_SRC`). - ---- +Read the [Foundry Docs][foundry-docs] to learn more. ## Contributing Contributions are welcome and highly appreciated. To get started, check out the [contributing guidelines](./CONTRIBUTING.md). -If you want to contribute, or follow along with contributor discussion, you can use our [main telegram](https://t.me/foundry_rs) to chat with us about the development of Foundry! +Join our [Telegram][tg-url] to chat about the development of Foundry. ## Support -Having trouble? See if the answer to your question can be found in the [Foundry Docs][foundry-docs]. - -If the answer is not there: -- Join the [support Telegram][tg-support-url] to get help, or -- Open an issue with [the bug](https://github.com/foundry-rs/foundry/issues/new) +Having trouble? Check the [Foundry Docs][foundry-docs], join the [support Telegram][tg-support-url], or [open an issue](https://github.com/foundry-rs/foundry/issues/new). #### License @@ -338,30 +91,4 @@ for inclusion in these crates by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -## Acknowledgements - -- Foundry is a clean-room rewrite of the testing framework [DappTools][dapptools]. None of this would have been possible without the DappHub team's work over the years. -- [Matthias Seitz](https://twitter.com/mattsse_): Created [ethers-solc] (now [foundry-compilers]) which is the backbone of our compilation pipeline, as well as countless contributions to ethers, in particular the `abigen` macros. -- [Rohit Narurkar](https://twitter.com/rohitnarurkar): Created the Rust Solidity version manager [svm-rs](https://github.com/roynalnaruto/svm-rs) which we use to auto-detect and manage multiple Solidity versions. -- [Brock Elmore](https://twitter.com/brockjelmore): For extending the VM's cheatcodes and implementing [structured call tracing](https://github.com/foundry-rs/foundry/pull/192), a critical feature for debugging smart contract calls. -- Thank you to [Depot](https://depot.dev) for sponsoring us with their fast GitHub runners and sccache, which we use in CI to reduce build and test times significantly. -- All the other [contributors](https://github.com/foundry-rs/foundry/graphs/contributors) to the [ethers-rs](https://github.com/gakonst/ethers-rs), [alloy][alloy] & [foundry](https://github.com/foundry-rs/foundry) repositories and chatrooms. - -[solidity]: https://soliditylang.org/ [foundry-docs]: https://getfoundry.sh -[foundry-gha]: https://github.com/foundry-rs/foundry-toolchain -[foundry-compilers]: https://github.com/foundry-rs/compilers -[ethers-solc]: https://github.com/gakonst/ethers-rs/tree/master/ethers-solc/ -[solady]: https://github.com/Vectorized/solady -[openzeppelin]: https://github.com/OpenZeppelin/openzeppelin-contracts/tree/release-v5.1 -[morpho-blue]: https://github.com/morpho-org/morpho-blue -[solmate]: https://github.com/transmissions11/solmate/ -[geb]: https://github.com/reflexer-labs/geb -[benchmark-post]: https://www.paradigm.xyz/2022/03/foundry-02#blazing-fast-compilation--testing -[convex]: https://github.com/mds1/convex-shutdown-simulation -[vscode-setup]: https://getfoundry.sh/config/vscode.html -[shell-setup]: https://getfoundry.sh/config/shell-autocompletion.html -[foundry-0.2]: https://github.com/foundry-rs/foundry/releases/tag/nightly-5b7e4cb3c882b28f3c32ba580de27ce7381f415a -[foundry-1.0]: https://github.com/foundry-rs/foundry/releases/tag/nightly-59f354c179f4e7f6d7292acb3d068815c79286d1 -[dapptools]: https://github.com/dapphub/dapptools -[alloy]: https://github.com/alloy-rs/alloy diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index fb4b774f775f9..ee28f59e67c9b 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -1,5 +1,5 @@ use crate::{eth::subscription::SubscriptionId, types::ReorgOptions}; -use alloy_primitives::{Address, B64, B256, Bytes, TxHash, U256}; +use alloy_primitives::{Address, B64, B256, Bytes, TxHash, U256, map::HashSet}; use alloy_rpc_types::{ BlockId, BlockNumberOrTag as BlockNumber, BlockOverrides, Filter, Index, anvil::{Forking, MineOptions}, @@ -10,6 +10,7 @@ use alloy_rpc_types::{ trace::{ filter::TraceFilter, geth::{GethDebugTracingCallOptions, GethDebugTracingOptions}, + parity::TraceType, }, }; use alloy_serde::WithOtherFields; @@ -325,6 +326,13 @@ pub enum EthRequest { #[serde(rename = "trace_filter", with = "sequence")] TraceFilter(TraceFilter), + /// Trace transaction endpoint for parity's `trace_replayBlockTransactions` + #[serde(rename = "trace_replayBlockTransactions")] + TraceReplayBlockTransactions( + #[serde(deserialize_with = "lenient_block_number::lenient_block_number")] BlockNumber, + HashSet, + ), + // Custom endpoints, they're not extracted to a separate type out of serde convenience /// send transactions impersonating specific account and contract addresses. #[serde( diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 57e08dd54fc87..70b33b1d76734 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -1212,7 +1212,7 @@ impl NodeConfig { eth_rpc_url: String, env: &mut Env, fees: &FeeManager, - ) -> Result<(ForkedDatabase, ClientForkConfig)> { + ) -> Result<(ForkedDatabase, ClientForkConfig)> { debug!(target: "node", ?eth_rpc_url, "setting up fork db"); let provider = Arc::new( ProviderBuilder::new(ð_rpc_url) diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 7e623ea2bd986..f5203ee7dc520 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -56,7 +56,7 @@ use alloy_rpc_types::{ trace::{ filter::TraceFilter, geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace}, - parity::LocalizedTransactionTrace, + parity::{LocalizedTransactionTrace, TraceResultsWithTransactionHash, TraceType}, }, txpool::{TxpoolContent, TxpoolInspect, TxpoolInspectSummary, TxpoolStatus}, }; @@ -344,6 +344,9 @@ impl EthApi { EthRequest::TraceTransaction(tx) => self.trace_transaction(tx).await.to_rpc_result(), EthRequest::TraceBlock(block) => self.trace_block(block).await.to_rpc_result(), EthRequest::TraceFilter(filter) => self.trace_filter(filter).await.to_rpc_result(), + EthRequest::TraceReplayBlockTransactions(block, trace_types) => { + self.trace_replay_block_transactions(block, trace_types).await.to_rpc_result() + } EthRequest::ImpersonateAccount(addr) => { self.anvil_impersonate_account(addr).await.to_rpc_result() } @@ -1859,7 +1862,7 @@ impl EthApi { if let Some(filter) = self.filters.get_log_filter(id).await { self.backend.logs(filter).await } else { - Ok(Vec::new()) + Err(BlockchainError::FilterNotFound) } } @@ -1990,6 +1993,18 @@ impl EthApi { node_info!("trace_filter"); self.backend.trace_filter(filter).await } + + /// Replays all transactions in a block returning the requested traces for each transaction + /// + /// Handler for RPC call: `trace_replayBlockTransactions` + pub async fn trace_replay_block_transactions( + &self, + block: BlockNumber, + trace_types: HashSet, + ) -> Result> { + node_info!("trace_replayBlockTransactions"); + self.backend.trace_replay_block_transactions(block, trace_types).await + } } // == impl EthApi anvil endpoints == @@ -2169,7 +2184,7 @@ impl EthApi { pub async fn anvil_add_balance(&self, address: Address, balance: U256) -> Result<()> { node_info!("anvil_addBalance"); let current_balance = self.backend.get_balance(address, None).await?; - self.backend.set_balance(address, current_balance + balance).await?; + self.backend.set_balance(address, current_balance.saturating_add(balance)).await?; Ok(()) } @@ -3295,10 +3310,14 @@ impl EthApi { request.kind().is_none().then(|| request.set_kind(TxKind::default())); if request.gas_limit().is_none() { request.set_gas_limit( - self.do_estimate_gas(request.as_ref().clone(), None, EvmOverrides::default()) - .await - .map(|v| v as u64) - .unwrap_or(self.backend.gas_limit()), + self.do_estimate_gas( + request.as_ref().clone().into(), + None, + EvmOverrides::default(), + ) + .await + .map(|v| v as u64) + .unwrap_or(self.backend.gas_limit()), ); } diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index d21a4ae345bb0..5e38f41cd08ab 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -71,7 +71,7 @@ impl ExecutedTransaction { *cumulative_gas_used = cumulative_gas_used.saturating_add(self.gas_used); // successful return see [Return] - let status_code = u8::from(self.exit_reason as u8 <= InstructionResult::SelfDestruct as u8); + let status_code = u8::from(self.exit_reason.is_ok()); let receipt_with_bloom: ReceiptWithBloom = Receipt { status: (status_code == 1).into(), cumulative_gas_used: *cumulative_gas_used, diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index 995206fbfcc89..b7732446c8358 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -6,7 +6,7 @@ use alloy_eips::eip2930::AccessListResult; use alloy_network::{AnyRpcBlock, AnyRpcTransaction, BlockResponse, TransactionResponse}; use alloy_primitives::{ Address, B256, Bytes, StorageValue, U256, - map::{FbHashMap, HashMap}, + map::{FbHashMap, HashMap, HashSet}, }; use alloy_provider::{ Provider, @@ -19,7 +19,7 @@ use alloy_rpc_types::{ simulate::{SimulatePayload, SimulatedBlock}, trace::{ geth::{GethDebugTracingOptions, GethTrace}, - parity::LocalizedTransactionTrace as Trace, + parity::{LocalizedTransactionTrace as Trace, TraceResultsWithTransactionHash, TraceType}, }, }; use alloy_serde::WithOtherFields; @@ -419,6 +419,16 @@ impl ClientFork { Ok(traces) } + pub async fn trace_replay_block_transactions( + &self, + number: u64, + trace_types: HashSet, + ) -> Result, TransportError> { + // Forward to upstream provider for historical blocks + let params = (number, trace_types.iter().map(|t| format!("{t:?}")).collect::>()); + self.provider().raw_request("trace_replayBlockTransactions".into(), params).await + } + pub async fn transaction_receipt( &self, hash: B256, diff --git a/crates/anvil/src/eth/backend/mem/cache.rs b/crates/anvil/src/eth/backend/mem/cache.rs index b7c7d634a3d6c..524ef9cbd74bb 100644 --- a/crates/anvil/src/eth/backend/mem/cache.rs +++ b/crates/anvil/src/eth/backend/mem/cache.rs @@ -55,24 +55,21 @@ impl DiskStateCache { } } - /// Stores the snapshot for the given hash + /// Stores the snapshot for the given hash synchronously. /// - /// Note: this writes the state on a new spawned task - /// - /// Caution: this requires a running tokio Runtime. - pub fn write(&mut self, hash: B256, state: StateSnapshot) { - self.with_cache_file(hash, |file| { - tokio::task::spawn_blocking(move || { - match foundry_common::fs::write_json_file(&file, &state) { - Ok(_) => { - trace!(target: "backend", ?hash, "wrote state json file"); - } - Err(err) => { - error!(target: "backend", %err, ?hash, "Failed to write state snapshot"); - } - }; - }); - }); + /// Returns `true` if the write was successful, `false` otherwise. + pub fn write(&mut self, hash: B256, state: &StateSnapshot) -> bool { + self.with_cache_file(hash, |file| match foundry_common::fs::write_json_file(&file, state) { + Ok(_) => { + trace!(target: "backend", ?hash, "wrote state json file"); + true + } + Err(err) => { + error!(target: "backend", %err, ?hash, "Failed to write state snapshot"); + false + } + }) + .unwrap_or(false) } /// Loads the snapshot file for the given hash diff --git a/crates/anvil/src/eth/backend/mem/fork_db.rs b/crates/anvil/src/eth/backend/mem/fork_db.rs index 1ff97edb82e25..308cdd2e4334c 100644 --- a/crates/anvil/src/eth/backend/mem/fork_db.rs +++ b/crates/anvil/src/eth/backend/mem/fork_db.rs @@ -2,6 +2,7 @@ use crate::eth::backend::db::{ Db, MaybeForkedDatabase, MaybeFullDatabase, SerializableAccountRecord, SerializableBlock, SerializableHistoricalStates, SerializableState, SerializableTransaction, StateDb, }; +use alloy_network::Network; use alloy_primitives::{Address, B256, U256, map::AddressMap}; use alloy_rpc_types::BlockId; use foundry_evm::{ @@ -16,7 +17,7 @@ use revm::{ pub use foundry_evm::fork::database::ForkedDatabase; -impl Db for ForkedDatabase { +impl Db for ForkedDatabase { fn insert_account(&mut self, address: Address, account: AccountInfo) { self.database_mut().insert_account(address, account) } @@ -86,7 +87,7 @@ impl Db for ForkedDatabase { } } -impl MaybeFullDatabase for ForkedDatabase { +impl MaybeFullDatabase for ForkedDatabase { fn maybe_as_full_db(&self) -> Option<&AddressMap> { Some(&self.database().cache.accounts) } @@ -121,7 +122,7 @@ impl MaybeFullDatabase for ForkedDatabase { } } -impl MaybeFullDatabase for ForkDbStateSnapshot { +impl MaybeFullDatabase for ForkDbStateSnapshot { fn maybe_as_full_db(&self) -> Option<&AddressMap> { Some(&self.local.cache.accounts) } @@ -154,7 +155,7 @@ impl MaybeFullDatabase for ForkDbStateSnapshot { } } -impl MaybeForkedDatabase for ForkedDatabase { +impl MaybeForkedDatabase for ForkedDatabase { fn maybe_reset(&mut self, url: Option, block_number: BlockId) -> Result<(), String> { self.reset(url, block_number) } diff --git a/crates/anvil/src/eth/backend/mem/inspector.rs b/crates/anvil/src/eth/backend/mem/inspector.rs index 3efaa1db98ec1..e50f2115f80c8 100644 --- a/crates/anvil/src/eth/backend/mem/inspector.rs +++ b/crates/anvil/src/eth/backend/mem/inspector.rs @@ -38,8 +38,8 @@ impl AnvilInspector { /// /// This will log all `console.sol` logs pub fn print_logs(&self) { - if let Some(collector) = &self.log_collector { - print_logs(&collector.logs); + if let Some(LogCollector::Capture { logs }) = &self.log_collector { + print_logs(logs); } } @@ -78,7 +78,7 @@ impl AnvilInspector { /// Configures the `Tracer` [`revm::Inspector`] with a log collector pub fn with_log_collector(mut self) -> Self { - self.log_collector = Some(Default::default()); + self.log_collector = Some(LogCollector::Capture { logs: Vec::new() }); self } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 557ea2d0d07f1..34755d7f4bb00 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -54,7 +54,7 @@ use alloy_network::{ }; use alloy_primitives::{ Address, B256, Bytes, TxHash, TxKind, U64, U256, hex, keccak256, logs_bloom, - map::{AddressMap, HashMap}, + map::{AddressMap, HashMap, HashSet}, }; use alloy_rpc_types::{ AccessList, Block as AlloyBlock, BlockId, BlockNumberOrTag as BlockNumber, BlockTransactions, @@ -71,7 +71,7 @@ use alloy_rpc_types::{ FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, NoopFrame, }, - parity::LocalizedTransactionTrace, + parity::{LocalizedTransactionTrace, TraceResultsWithTransactionHash, TraceType}, }, }; use alloy_serde::{OtherFields, WithOtherFields}; @@ -1738,7 +1738,7 @@ impl Backend { }) .collect(), }; - logs.extend(sim_res.logs.clone().iter().map(|log| log.inner.clone())); + logs.extend(sim_res.logs.iter().map(|log| log.inner.clone())); log_index += sim_res.logs.len(); call_res.push(sim_res); } @@ -1981,7 +1981,7 @@ impl Backend { drop(evm); let tracing_inspector = inspector.tracer.expect("tracer disappeared"); - let return_value = out.as_ref().map(|o| o.data().clone()).unwrap_or_default(); + let return_value = out.as_ref().map(|o| o.data()).cloned().unwrap_or_default(); trace!(target: "backend", ?exit_reason, ?out, %gas_used, %block_number, "trace call"); @@ -2424,8 +2424,14 @@ impl Backend { None => None, }; let block_number = self.convert_block_number(block_number); + let current_number = self.best_number(); - if block_number < self.env.read().evm_env.block_env.number.saturating_to() { + // Reject requests for future blocks that don't exist yet + if block_number > current_number { + return Err(BlockchainError::BlockOutOfRange(current_number, block_number)); + } + + if block_number < current_number { if let Some((block_hash, block)) = self .block_by_number(BlockNumber::Number(block_number)) .await? @@ -2443,10 +2449,7 @@ impl Backend { } warn!(target: "backend", "Not historic state found for block={}", block_number); - return Err(BlockchainError::BlockOutOfRange( - self.env.read().evm_env.block_env.number.saturating_to(), - block_number, - )); + return Err(BlockchainError::BlockOutOfRange(current_number, block_number)); } let db = self.db.read().await; @@ -2943,6 +2946,114 @@ impl Backend { Ok(vec![]) } + /// Replays all transactions in a block and returns the requested traces for each transaction + pub async fn trace_replay_block_transactions( + &self, + block: BlockNumber, + trace_types: HashSet, + ) -> Result, BlockchainError> { + let block_number = self.convert_block_number(Some(block)); + + // Try mined blocks first + if let Some(results) = + self.mined_parity_trace_replay_block_transactions(block_number, &trace_types) + { + return Ok(results); + } + + // Fallback to fork if block predates fork + if let Some(fork) = self.get_fork() + && fork.predates_fork(block_number) + { + return Ok(fork.trace_replay_block_transactions(block_number, trace_types).await?); + } + + Ok(vec![]) + } + + /// Returns the trace results for all transactions in a mined block by replaying them + fn mined_parity_trace_replay_block_transactions( + &self, + block_number: u64, + trace_types: &HashSet, + ) -> Option> { + let block = self.get_block(block_number)?; + + // Execute this in the context of the parent state + let parent_hash = block.header.parent_hash; + let trace_config = TracingInspectorConfig::from_parity_config(trace_types); + + let read_guard = self.states.upgradable_read(); + if let Some(state) = read_guard.get_state(&parent_hash) { + self.replay_block_transactions_with_inspector(&block, state, trace_config, trace_types) + } else { + let mut write_guard = RwLockUpgradableReadGuard::upgrade(read_guard); + let state = write_guard.get_on_disk_state(&parent_hash)?; + self.replay_block_transactions_with_inspector(&block, state, trace_config, trace_types) + } + } + + /// Replays all transactions in a block with the tracing inspector to generate TraceResults + fn replay_block_transactions_with_inspector( + &self, + block: &Block, + parent_state: &StateDb, + trace_config: TracingInspectorConfig, + trace_types: &HashSet, + ) -> Option> { + let mut cache_db = CacheDB::new(Box::new(parent_state)); + let mut results = Vec::new(); + + // Configure the block environment + let mut env = self.env.read().clone(); + env.evm_env.block_env = BlockEnv { + number: U256::from(block.header.number), + beneficiary: block.header.beneficiary, + timestamp: U256::from(block.header.timestamp), + difficulty: block.header.difficulty, + prevrandao: Some(block.header.mix_hash), + basefee: block.header.base_fee_per_gas.unwrap_or_default(), + gas_limit: block.header.gas_limit, + ..Default::default() + }; + + // Execute each transaction in the block with tracing + for tx_envelope in &block.body.transactions { + let tx_hash = tx_envelope.hash(); + + // Create a fresh inspector for this transaction + let mut inspector = TracingInspector::new(trace_config); + + // Prepare transaction environment + let pending_tx = + PendingTransaction::from_maybe_impersonated(tx_envelope.clone()).ok()?; + let mut tx_env: OpTransaction = FromRecoveredTx::from_recovered_tx( + pending_tx.transaction.as_ref(), + *pending_tx.sender(), + ); + if env.networks.is_optimism() { + tx_env.enveloped_tx = Some(pending_tx.transaction.encoded_2718().into()); + } + + // Execute the transaction with the inspector + let mut evm = self.new_evm_with_inspector_ref(&cache_db, &env, &mut inspector); + let result = evm.transact(tx_env.clone()).ok()?; + + // Build TraceResults from the inspector and execution result + let full_trace = inspector + .into_parity_builder() + .into_trace_results_with_state(&result, trace_types, &cache_db) + .ok()?; + + results.push(TraceResultsWithTransactionHash { transaction_hash: tx_hash, full_trace }); + + // Commit the state changes for the next transaction + cache_db.commit(result.state); + } + + Some(results) + } + pub async fn transaction_receipt( &self, hash: B256, @@ -3310,14 +3421,14 @@ impl Backend { .into_iter() .map(|(_, v)| v) .collect(); - let storage_proofs = prove_storage(&account.storage, &keys); + let (storage_hash, storage_proofs) = prove_storage(&account.storage, &keys); let account_proof = AccountProof { address, balance: account.info.balance, nonce: account.info.nonce, code_hash: account.info.code_hash, - storage_hash: storage_root(&account.storage), + storage_hash, account_proof: proof, storage_proof: keys .into_iter() @@ -3792,7 +3903,7 @@ pub fn transaction_build( /// `storage_key` is the hash of the desired storage key, meaning /// this will only work correctly under a secure trie. /// `storage_key` == keccak(key) -pub fn prove_storage(storage: &HashMap, keys: &[B256]) -> Vec> { +pub fn prove_storage(storage: &HashMap, keys: &[B256]) -> (B256, Vec>) { let keys: Vec<_> = keys.iter().map(|key| Nibbles::unpack(keccak256(key))).collect(); let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(keys.clone())); @@ -3801,7 +3912,7 @@ pub fn prove_storage(storage: &HashMap, keys: &[B256]) -> Vec, keys: &[B256]) -> Vec bool { diff --git a/crates/anvil/src/eth/backend/mem/storage.rs b/crates/anvil/src/eth/backend/mem/storage.rs index 2bb42df4ba592..4d73f19ebdaca 100644 --- a/crates/anvil/src/eth/backend/mem/storage.rs +++ b/crates/anvil/src/eth/backend/mem/storage.rs @@ -149,9 +149,19 @@ impl InMemoryBlockStates { // only write to disk if supported if !self.is_memory_only() { let state_snapshot = state.0.clear_into_state_snapshot(); - self.disk_cache.write(hash, state_snapshot); - self.on_disk_states.insert(hash, state); - self.oldest_on_disk.push_back(hash); + if self.disk_cache.write(hash, &state_snapshot) { + // Write succeeded, move state to on-disk tracking + self.on_disk_states.insert(hash, state); + self.oldest_on_disk.push_back(hash); + } else { + // Write failed, restore state to memory to avoid data loss + state.init_from_state_snapshot(state_snapshot); + self.states.insert(hash, state); + self.present.push_front(hash); + // Increase limit temporarily to prevent infinite retry loop + self.in_memory_limit = self.in_memory_limit.saturating_add(1); + break; + } } } } diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error.rs index 2db3afbae8060..d3844f67312f2 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error.rs @@ -123,6 +123,8 @@ pub enum BlockchainError { }, #[error("Invalid transaction request: {0}")] InvalidTransactionRequest(String), + #[error("filter not found")] + FilterNotFound, } impl From for BlockchainError { @@ -577,6 +579,11 @@ impl ToRpcResponseResult for Result { err @ BlockchainError::RecoveryError(_) => { RpcError::invalid_params(err.to_string()) } + BlockchainError::FilterNotFound => RpcError { + code: ErrorCode::ServerError(-32000), + message: "filter not found".into(), + data: None, + }, } .into(), } diff --git a/crates/anvil/src/eth/pool/mod.rs b/crates/anvil/src/eth/pool/mod.rs index 25b5e673db812..8231315ea7e31 100644 --- a/crates/anvil/src/eth/pool/mod.rs +++ b/crates/anvil/src/eth/pool/mod.rs @@ -106,19 +106,17 @@ impl Pool { markers: impl IntoIterator, ) -> PruneResult { debug!(target: "txpool", ?block_number, "pruning transactions"); - self.inner.write().prune_markers(markers) + let res = self.inner.write().prune_markers(markers); + for tx in &res.promoted { + self.notify_ready(tx); + } + res } /// Adds a new transaction to the pool pub fn add_transaction(&self, tx: PoolTransaction) -> Result { let added = self.inner.write().add_transaction(tx)?; - if let AddedTransaction::Ready(ref ready) = added { - self.notify_listener(ready.hash); - // also notify promoted transactions - for promoted in ready.promoted.iter().copied() { - self.notify_listener(promoted); - } - } + self.notify_ready(&added); Ok(added) } @@ -172,6 +170,16 @@ impl Pool { pool.clear(); } + /// Notifies listeners if the transaction was added to the ready queue. + fn notify_ready(&self, tx: &AddedTransaction) { + if let AddedTransaction::Ready(ready) = tx { + self.notify_listener(ready.hash); + for promoted in ready.promoted.iter().copied() { + self.notify_listener(promoted); + } + } + } + /// notifies all listeners about the transaction fn notify_listener(&self, hash: TxHash) { let mut listener = self.transaction_listener.lock(); diff --git a/crates/anvil/src/eth/pool/transactions.rs b/crates/anvil/src/eth/pool/transactions.rs index e549009d7a5c0..e044bf041132c 100644 --- a/crates/anvil/src/eth/pool/transactions.rs +++ b/crates/anvil/src/eth/pool/transactions.rs @@ -653,11 +653,11 @@ impl ReadyTransactions { // remove from unlocks for mark in &tx.transaction.transaction.requires { - if let Some(hash) = self.provided_markers.get(mark) - && let Some(tx) = ready.get_mut(hash) - && let Some(idx) = tx.unlocks.iter().position(|i| i == hash) + if let Some(provider_hash) = self.provided_markers.get(mark) + && let Some(provider_tx) = ready.get_mut(provider_hash) + && let Some(idx) = provider_tx.unlocks.iter().position(|i| i == &hash) { - tx.unlocks.swap_remove(idx); + provider_tx.unlocks.swap_remove(idx); } } diff --git a/crates/anvil/src/filter.rs b/crates/anvil/src/filter.rs index 9d1e0958e99fe..a23be64cf89be 100644 --- a/crates/anvil/src/filter.rs +++ b/crates/anvil/src/filter.rs @@ -7,7 +7,10 @@ use crate::{ use alloy_primitives::{TxHash, map::HashMap}; use alloy_rpc_types::{Filter, FilteredParams, Log}; use anvil_core::eth::subscription::SubscriptionId; -use anvil_rpc::response::ResponseResult; +use anvil_rpc::{ + error::{ErrorCode, RpcError}, + response::ResponseResult, +}; use futures::{Stream, StreamExt, channel::mpsc::Receiver}; use std::{ pin::Pin, @@ -55,7 +58,11 @@ impl Filters { } } warn!(target: "node::filter", "No filter found for {}", id); - ResponseResult::success(Vec::<()>::new()) + ResponseResult::error(RpcError { + code: ErrorCode::ServerError(-32000), + message: "filter not found".into(), + data: None, + }) } /// Returns the original `Filter` of an `eth_newFilter` diff --git a/crates/anvil/tests/it/traces.rs b/crates/anvil/tests/it/traces.rs index ba6c7ba887246..916dcbb728f63 100644 --- a/crates/anvil/tests/it/traces.rs +++ b/crates/anvil/tests/it/traces.rs @@ -25,7 +25,7 @@ use alloy_rpc_types::{ GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, }, - parity::{Action, LocalizedTransactionTrace}, + parity::{Action, ChangedType, LocalizedTransactionTrace, TraceType}, }, }; use alloy_serde::WithOtherFields; @@ -1243,7 +1243,7 @@ async fn test_debug_trace_transaction_pre_state_tracer() { "nonce": 1 }, "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { - "balance": "0x56bc75e2d630fffff" + "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512": { "balance": "0x0", @@ -1273,3 +1273,81 @@ async fn test_debug_trace_transaction_pre_state_tracer() { _ => unreachable!(), } } + +#[tokio::test(flavor = "multi_thread")] +async fn test_trace_replay_block_transactions_local() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + api.anvil_set_auto_mine(false).await.unwrap(); + + let accounts = handle.dev_wallets().collect::>(); + let from = accounts[0].address(); + let to = accounts[1].address(); + let amount = U256::from(1000000u64); + + // Send first transaction + let tx1 = TransactionRequest::default().to(to).value(amount).from(from); + let tx1 = WithOtherFields::new(tx1); + let pending_tx1 = provider.send_transaction(tx1).await.unwrap(); + + // Send second transaction with different value + let tx2 = TransactionRequest::default().to(to).value(amount).from(from); + let tx2 = WithOtherFields::new(tx2); + let pending_tx2 = provider.send_transaction(tx2).await.unwrap(); + + api.mine_one().await; + let receipt1 = pending_tx1.get_receipt().await.unwrap(); + let receipt2 = pending_tx2.get_receipt().await.unwrap(); + + let block_number = receipt2.block_number.unwrap(); + + // Replay the block transactions with call trace type + // Pass block number as hex string as per Ethereum RPC spec + let results = api + .trace_replay_block_transactions( + block_number.into(), + vec![TraceType::Trace, TraceType::VmTrace, TraceType::StateDiff].into_iter().collect(), + ) + .await + .unwrap(); + + // Verify we have traces for both transactions + assert_eq!(results.len(), 2, "Should have traces for 2 transactions"); + + // Verify first transaction hash matches + assert_eq!(results[0].transaction_hash, receipt1.transaction_hash); + + // Verify second transaction hash matches + assert_eq!(results[1].transaction_hash, receipt2.transaction_hash); + + // Verify trace types are present and accurate + for result in results { + let full_trace = &result.full_trace; + + // Verify Trace (call trace) is present and accurate + assert!(!full_trace.trace.is_empty(), "Trace should not be empty"); + let first_trace = &full_trace.trace[0]; + match &first_trace.action { + Action::Call(call) => { + assert_eq!(call.from, from, "Call from address should match"); + assert_eq!(call.to, to, "Call to address should match"); + } + _ => panic!("Expected Call action, got {:?}", first_trace.action), + } + + // Verify VmTrace is present + assert!(full_trace.vm_trace.is_some(), "VmTrace should be present when requested"); + + // Verify StateDiff is present + assert!(full_trace.state_diff.is_some(), "StateDiff should be present when requested"); + // Verify balance change is correct in state diff + let ChangedType:: { from, to } = + full_trace.state_diff.as_ref().unwrap().get(&to).unwrap().balance.as_changed().unwrap(); + assert_eq!( + to.checked_sub(*from).unwrap(), + amount, + "Incorrect balance change in state diff" + ); + } +} diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 2f30b89e07216..3188ade007e3c 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -356,7 +356,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { } CastSubcommand::BlockNumber { rpc, block } => { let config = rpc.load_config()?; - let provider = utils::get_provider_with_curl(&config, rpc.curl)?; + let provider = utils::get_provider(&config)?; let number = match block { Some(id) => { provider @@ -377,7 +377,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { } CastSubcommand::ChainId { rpc } => { let config = rpc.load_config()?; - let provider = utils::get_provider_with_curl(&config, rpc.curl)?; + let provider = utils::get_provider(&config)?; sh_println!("{}", Cast::new(provider).chain_id().await?)? } CastSubcommand::Client { rpc } => { @@ -449,7 +449,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::FindBlock(cmd) => cmd.run().await?, CastSubcommand::GasPrice { rpc } => { let config = rpc.load_config()?; - let provider = utils::get_provider_with_curl(&config, rpc.curl)?; + let provider = utils::get_provider(&config)?; sh_println!("{}", Cast::new(provider).gas_price().await?)?; } CastSubcommand::Index { key_type, key, slot_number } => { diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 6359682ae717e..f647a5cd05619 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -16,7 +16,7 @@ use clap::Parser; use eyre::Result; use foundry_cli::{ opts::{ChainValueParser, RpcOpts, TransactionOpts}, - utils::{LoadConfig, TraceResult, get_provider_with_curl, parse_ether_value}, + utils::{LoadConfig, TraceResult, get_provider, parse_ether_value}, }; use foundry_common::{ abi::{encode_function_args, get_func}, @@ -247,7 +247,7 @@ impl CallArgs { sig = Some(data); } - let provider = get_provider_with_curl(&config, false)?; + let provider = get_provider(&config)?; let sender = SenderKind::from_wallet_opts(wallet).await?; let from = sender.address(); diff --git a/crates/cast/src/cmd/create2.rs b/crates/cast/src/cmd/create2.rs index a07e3fd8e4378..a531c95856666 100644 --- a/crates/cast/src/cmd/create2.rs +++ b/crates/cast/src/cmd/create2.rs @@ -218,7 +218,10 @@ impl Create2Args { // Important: add the thread index to the salt to avoid duplicate results. *salt_word = salt_word.wrapping_add(i); - let mut checksum = [0; 42]; + // Use checksum format only when case_sensitive is enabled. + // This avoids an extra keccak256 call per iteration when not needed. + let mut checksum_buf = [0u8; 42]; + let mut hex_buf = [0u8; 40]; loop { // Stop if a result was found in another thread. if found.load(Ordering::Relaxed) { @@ -229,11 +232,19 @@ impl Create2Args { #[expect(clippy::needless_borrows_for_generic_args)] let addr = deployer.create2(&salt.0, &init_code_hash); - // Check if the regex matches the calculated address' checksum. - let _ = addr.to_checksum_raw(&mut checksum, None); - // SAFETY: stripping 2 ASCII bytes ("0x") off of an already valid UTF-8 string - // is safe. - let s = unsafe { std::str::from_utf8_unchecked(checksum.get_unchecked(2..)) }; + // Check if the regex matches the calculated address. + // When case_sensitive is true, use EIP-55 checksum format (requires keccak256). + // Otherwise, use lowercase hex to avoid the extra hash computation. + let s = if case_sensitive { + let _ = addr.to_checksum_raw(&mut checksum_buf, None); + // SAFETY: stripping 2 ASCII bytes ("0x") off of an already valid UTF-8 + // string is safe. + unsafe { std::str::from_utf8_unchecked(checksum_buf.get_unchecked(2..)) } + } else { + // SAFETY: hex::encode_to_slice always produces valid UTF-8 (hex digits). + let _ = hex::encode_to_slice(addr.as_slice(), &mut hex_buf); + unsafe { std::str::from_utf8_unchecked(&hex_buf) } + }; if regex.matches(s).into_iter().count() == regex_len { // Notify other threads that we found a result. found.store(true, Ordering::Relaxed); diff --git a/crates/cast/src/cmd/erc20.rs b/crates/cast/src/cmd/erc20.rs index 4806fc9489eb2..55ec3951ee6b0 100644 --- a/crates/cast/src/cmd/erc20.rs +++ b/crates/cast/src/cmd/erc20.rs @@ -3,11 +3,11 @@ use std::str::FromStr; use crate::{ cmd::send::cast_send, format_uint_exp, - tx::{CastTxSender, SendTxOpts, signing_provider_with_curl}, + tx::{CastTxSender, SendTxOpts, get_provider_with_wallet}, }; use alloy_eips::{BlockId, Encodable2718}; use alloy_ens::NameOrAddress; -use alloy_network::{AnyNetwork, NetworkWallet, TransactionBuilder}; +use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; use alloy_primitives::{U64, U256}; use alloy_provider::Provider; use alloy_rpc_types::TransactionRequest; @@ -16,8 +16,9 @@ use alloy_sol_types::sol; use clap::{Args, Parser}; use foundry_cli::{ opts::{RpcOpts, TempoOpts}, - utils::{LoadConfig, get_chain, get_provider_with_curl}, + utils::{LoadConfig, get_chain, get_provider}, }; +use foundry_common::shell; #[doc(hidden)] pub use foundry_config::{Chain, utils::*}; use foundry_primitives::FoundryTransactionRequest; @@ -118,8 +119,7 @@ async fn send_erc20_tx>( ftx.set_chain_id(provider.get_chain_id().await?); } - // Sign using NetworkWallet - let signed_tx = signer.sign_request(ftx).await?; + let signed_tx = ftx.build(&EthereumWallet::new(signer)).await?; // Encode and send raw let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len()); @@ -352,8 +352,8 @@ impl Erc20Subcommand { match self { // Read-only - Self::Allowance { token, owner, spender, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::Allowance { token, owner, spender, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let owner = owner.resolve(&provider).await?; let spender = spender.resolve(&provider).await?; @@ -364,10 +364,14 @@ impl Erc20Subcommand { .call() .await?; - sh_println!("{}", format_uint_exp(allowance))? + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&allowance.to_string())?)? + } else { + sh_println!("{}", format_uint_exp(allowance))? + } } - Self::Balance { token, owner, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::Balance { token, owner, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let owner = owner.resolve(&provider).await?; @@ -376,10 +380,15 @@ impl Erc20Subcommand { .block(block.unwrap_or_default()) .call() .await?; - sh_println!("{}", format_uint_exp(balance))? + + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&balance.to_string())?)? + } else { + sh_println!("{}", format_uint_exp(balance))? + } } - Self::Name { token, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::Name { token, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let name = IERC20::new(token, &provider) @@ -387,10 +396,15 @@ impl Erc20Subcommand { .block(block.unwrap_or_default()) .call() .await?; - sh_println!("{}", name)? + + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&name)?)? + } else { + sh_println!("{}", name)? + } } - Self::Symbol { token, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::Symbol { token, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let symbol = IERC20::new(token, &provider) @@ -398,10 +412,15 @@ impl Erc20Subcommand { .block(block.unwrap_or_default()) .call() .await?; - sh_println!("{}", symbol)? + + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&symbol)?)? + } else { + sh_println!("{}", symbol)? + } } - Self::Decimals { token, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::Decimals { token, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let decimals = IERC20::new(token, &provider) @@ -409,10 +428,14 @@ impl Erc20Subcommand { .block(block.unwrap_or_default()) .call() .await?; - sh_println!("{}", decimals)? + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&decimals)?)? + } else { + sh_println!("{}", decimals)? + } } - Self::TotalSupply { token, block, rpc, .. } => { - let provider = get_provider_with_curl(&config, rpc.curl)?; + Self::TotalSupply { token, block, .. } => { + let provider = get_provider(&config)?; let token = token.resolve(&provider).await?; let total_supply = IERC20::new(token, &provider) @@ -420,11 +443,16 @@ impl Erc20Subcommand { .block(block.unwrap_or_default()) .call() .await?; - sh_println!("{}", format_uint_exp(total_supply))? + + if shell::is_json() { + sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)? + } else { + sh_println!("{}", format_uint_exp(total_supply))? + } } // State-changing Self::Transfer { token, to, amount, send_tx, tx: tx_opts, .. } => { - let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?; + let provider = get_provider_with_wallet(&send_tx).await?; let mut tx = IERC20::new(token.resolve(&provider).await?, &provider) .transfer(to.resolve(&provider).await?, U256::from_str(&amount)?) .into_transaction_request(); @@ -445,7 +473,7 @@ impl Erc20Subcommand { .await? } Self::Approve { token, spender, amount, send_tx, tx: tx_opts, .. } => { - let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?; + let provider = get_provider_with_wallet(&send_tx).await?; let mut tx = IERC20::new(token.resolve(&provider).await?, &provider) .approve(spender.resolve(&provider).await?, U256::from_str(&amount)?) .into_transaction_request(); @@ -466,7 +494,7 @@ impl Erc20Subcommand { .await? } Self::Mint { token, to, amount, send_tx, tx: tx_opts, .. } => { - let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?; + let provider = get_provider_with_wallet(&send_tx).await?; let mut tx = IERC20::new(token.resolve(&provider).await?, &provider) .mint(to.resolve(&provider).await?, U256::from_str(&amount)?) .into_transaction_request(); @@ -487,7 +515,7 @@ impl Erc20Subcommand { .await? } Self::Burn { token, amount, send_tx, tx: tx_opts, .. } => { - let provider = signing_provider_with_curl(&send_tx, send_tx.eth.rpc.curl).await?; + let provider = get_provider_with_wallet(&send_tx).await?; let mut tx = IERC20::new(token.resolve(&provider).await?, &provider) .burn(U256::from_str(&amount)?) .into_transaction_request(); diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index 541789bbc94c6..da11ea3e47353 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -1,7 +1,7 @@ use crate::tx::{self, CastTxBuilder}; use alloy_eips::Encodable2718; use alloy_ens::NameOrAddress; -use alloy_network::{EthereumWallet, NetworkWallet, TransactionBuilder}; +use alloy_network::{EthereumWallet, TransactionBuilder}; use alloy_primitives::{Address, hex}; use alloy_provider::Provider; use alloy_signer::Signer; @@ -132,7 +132,7 @@ impl MakeTxArgs { // 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 signed_tx = provider.sign_transaction(tx.into_inner()).await?; + let signed_tx = provider.sign_transaction(tx.into_inner().into()).await?; sh_println!("{signed_tx}")?; return Ok(()); @@ -152,8 +152,7 @@ impl MakeTxArgs { if is_tempo { let (ftx, _) = tx_builder.build(&signer).await?; - // Sign using NetworkWallet - let signed_tx = signer.sign_request(ftx).await?; + let signed_tx = ftx.build(&EthereumWallet::new(signer)).await?; // Encode as 2718 let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len()); diff --git a/crates/cast/src/cmd/rpc.rs b/crates/cast/src/cmd/rpc.rs index d31202eb136b5..8883c3fbb5be2 100644 --- a/crates/cast/src/cmd/rpc.rs +++ b/crates/cast/src/cmd/rpc.rs @@ -53,7 +53,7 @@ impl RpcArgs { serde_json::Value::Array(params.into_iter().map(value_or_string).collect()) }; - let provider = utils::get_provider_with_curl(&config, rpc.curl)?; + let provider = utils::get_provider(&config)?; let result = Cast::new(provider).rpc(&method, params).await?; if shell::is_json() { let result: serde_json::Value = serde_json::from_str(&result)?; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 492ce360e7c2e..54894fc10b7ea 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -135,7 +135,7 @@ impl RunArgs { let compute_units_per_second = if self.no_rate_limit { Some(u64::MAX) } else { self.compute_units_per_second }; - let provider = foundry_cli::utils::get_provider_builder(&config, false)? + let provider = foundry_cli::utils::get_provider_builder(&config)? .compute_units_per_second_opt(compute_units_per_second) .build()?; diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 3e50ed27ed799..79a035032e365 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr, time::Duration}; use alloy_eips::Encodable2718; use alloy_ens::NameOrAddress; -use alloy_network::{AnyNetwork, EthereumWallet, NetworkWallet}; +use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types::TransactionRequest; use alloy_serde::WithOtherFields; @@ -11,7 +11,7 @@ use clap::Parser; use eyre::{Result, eyre}; use foundry_cli::{ opts::TransactionOpts, - utils::{LoadConfig, get_provider_with_curl}, + utils::{LoadConfig, get_provider}, }; use foundry_wallets::WalletSigner; @@ -120,7 +120,7 @@ impl SendTxArgs { }; let config = send_tx.eth.load_config()?; - let provider = get_provider_with_curl(&config, send_tx.eth.rpc.curl)?; + let provider = get_provider(&config)?; if let Some(interval) = send_tx.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) @@ -172,7 +172,7 @@ impl SendTxArgs { cast_send( provider, - tx.into_inner(), + tx.into_inner().into(), send_tx.cast_async, send_tx.sync, send_tx.confirmations, @@ -195,9 +195,8 @@ impl SendTxArgs { && let WalletSigner::Browser(ref browser_signer) = signer { let (tx_request, _) = builder.build(from).await?; - let tx_hash = browser_signer - .send_transaction_via_browser(tx_request.into_inner().inner) - .await?; + let tx_hash = + browser_signer.send_transaction_via_browser(tx_request.into_inner()).await?; if send_tx.cast_async { sh_println!("{tx_hash:#x}")?; @@ -225,8 +224,7 @@ impl SendTxArgs { if is_tempo { let (ftx, _) = builder.build(&signer).await?; - // Sign using NetworkWallet - let signed_tx = signer.sign_request(ftx).await?; + let signed_tx = ftx.build(&EthereumWallet::new(signer)).await?; // Encode and send raw let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len()); @@ -238,18 +236,6 @@ impl SendTxArgs { if send_tx.cast_async { sh_println!("{tx_hash:#x}")?; - } else if send_tx.sync { - // For sync mode, we already have the hash, just wait for receipt - let receipt = cast - .receipt( - format!("{tx_hash:#x}"), - None, - send_tx.confirmations, - Some(timeout), - false, - ) - .await?; - sh_println!("{receipt}")?; } else { let receipt = cast .receipt( @@ -275,7 +261,7 @@ impl SendTxArgs { cast_send( provider, - tx_request.into_inner(), + tx_request.into_inner().into(), send_tx.cast_async, send_tx.sync, send_tx.confirmations, diff --git a/crates/cast/src/cmd/storage.rs b/crates/cast/src/cmd/storage.rs index d5a74c0954624..b2a2c32251f6f 100644 --- a/crates/cast/src/cmd/storage.rs +++ b/crates/cast/src/cmd/storage.rs @@ -156,6 +156,7 @@ impl StorageArgs { } // Create or reuse a persistent cache for Etherscan sources; fall back to a temp dir + let mut temp_dir = None; let root_path = if let Some(cache_root) = foundry_config::Config::foundry_etherscan_chain_cache_dir(chain) { @@ -163,12 +164,18 @@ impl StorageArgs { let contract_root = sources_root.join(format!("{address}")); if let Err(err) = std::fs::create_dir_all(&contract_root) { sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?; - tempfile::tempdir()?.path().to_path_buf() + let tmp = tempfile::tempdir()?; + let path = tmp.path().to_path_buf(); + temp_dir = Some(tmp); + path } else { contract_root } } else { - tempfile::tempdir()?.keep() + let tmp = tempfile::tempdir()?; + let path = tmp.path().to_path_buf(); + temp_dir = Some(tmp); + path }; let mut project = etherscan_project(metadata, &root_path)?; add_storage_layout_output(&mut project); @@ -221,6 +228,7 @@ impl StorageArgs { artifact }; + drop(temp_dir); fetch_and_print_storage(provider, address, block, artifact).await } } diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index d38d27fec63dc..c43ffaaa7807d 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -705,17 +705,13 @@ async fn decode_execution_revert(data: &RawValue) -> Result> { } /// Creates a provider with wallet for signing transactions locally. -/// -/// If `curl_mode` is true, the provider will print equivalent curl commands to stdout -/// instead of executing RPC requests. -pub(crate) async fn signing_provider_with_curl( +pub(crate) async fn get_provider_with_wallet( tx_opts: &SendTxOpts, - curl_mode: bool, ) -> eyre::Result { let config = tx_opts.eth.load_config()?; let signer = tx_opts.eth.wallet.signer().await?; let wallet = alloy_network::EthereumWallet::from(signer); - let provider = get_provider_builder(&config, curl_mode)?.build_with_wallet(wallet)?; + let provider = get_provider_builder(&config)?.build_with_wallet(wallet)?; if let Some(interval) = tx_opts.poll_interval { provider.client().set_poll_interval(Duration::from_secs(interval)) } diff --git a/crates/cast/tests/cli/erc20.rs b/crates/cast/tests/cli/erc20.rs index 012844fb07730..3256e6f8bc167 100644 --- a/crates/cast/tests/cli/erc20.rs +++ b/crates/cast/tests/cli/erc20.rs @@ -501,3 +501,108 @@ casttest!(erc20_curl_total_supply, |_prj, cmd| { assert!(output.contains("eth_call")); assert!(output.contains(rpc)); }); + +// tests that `balance` command works correctly with --json flag +forgetest_async!(erc20_balance_json, |prj, cmd| { + let (rpc, token) = setup_token_test(&prj, &mut cmd).await; + + let output = cmd + .cast_fuse() + .args(["--json", "erc20", "balance", &token, anvil_const::ADDR1, "--rpc-url", &rpc]) + .assert_success() + .get_output() + .stdout_lossy(); + + let balance_str: String = serde_json::from_str(&output).expect("valid json string"); + let balance: U256 = balance_str.parse().unwrap(); + assert_eq!(balance, U256::from(1_000_000_000_000_000_000_000u128)); +}); + +// tests that `allowance` command works correctly with --json flag +forgetest_async!(erc20_allowance_json, |prj, cmd| { + let (rpc, token) = setup_token_test(&prj, &mut cmd).await; + + // First approve some tokens + let approve_amount = U256::from(50_000_000_000_000_000_000u128); + cmd.cast_fuse() + .args([ + "erc20", + "approve", + &token, + anvil_const::ADDR2, + &approve_amount.to_string(), + "--rpc-url", + &rpc, + "--private-key", + anvil_const::PK1, + ]) + .assert_success(); + + // Check allowance with JSON flag + let output = cmd + .cast_fuse() + .args([ + "--json", + "erc20", + "allowance", + &token, + anvil_const::ADDR1, + anvil_const::ADDR2, + "--rpc-url", + &rpc, + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let allowance_str: String = serde_json::from_str(&output).expect("valid json string"); + let allowance: U256 = allowance_str.parse().unwrap(); + assert_eq!(allowance, approve_amount); +}); + +// tests that `name`, `symbol`, `decimals`, and `totalSupply` commands work correctly with --json +// flag +forgetest_async!(erc20_metadata_json, |prj, cmd| { + let (rpc, token) = setup_token_test(&prj, &mut cmd).await; + + // Test name with --json + let output = cmd + .cast_fuse() + .args(["--json", "erc20", "name", &token, "--rpc-url", &rpc]) + .assert_success() + .get_output() + .stdout_lossy(); + let name: String = serde_json::from_str(&output).expect("valid json string"); + assert_eq!(name, "Test Token"); + + // Test symbol with --json + let output = cmd + .cast_fuse() + .args(["--json", "erc20", "symbol", &token, "--rpc-url", &rpc]) + .assert_success() + .get_output() + .stdout_lossy(); + let symbol: String = serde_json::from_str(&output).expect("valid json string"); + assert_eq!(symbol, "TEST"); + + // Test decimals with --json + let output = cmd + .cast_fuse() + .args(["--json", "erc20", "decimals", &token, "--rpc-url", &rpc]) + .assert_success() + .get_output() + .stdout_lossy(); + let decimals: u8 = output.trim().parse().expect("valid number"); + assert_eq!(decimals, 18); + + // Test totalSupply with --json + let output = cmd + .cast_fuse() + .args(["--json", "erc20", "total-supply", &token, "--rpc-url", &rpc]) + .assert_success() + .get_output() + .stdout_lossy(); + let total_supply_str: String = serde_json::from_str(&output).expect("valid json string"); + let total_supply: U256 = total_supply_str.parse().unwrap(); + assert_eq!(total_supply, U256::from(1_000_000_000_000_000_000_000u128)); +}); diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index a9147e2e284ff..2b60528814221 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4589,17 +4589,20 @@ casttest!(abi_encode_event_dynamic_indexed, |_prj, cmd| { }); // Test cast run Celo transfer with precompiles. -casttest!(flaky_run_celo_with_precompiles, |_prj, cmd| { - let rpc = next_rpc_endpoint(NamedChain::Celo); - cmd.args([ - "run", - "0xa652b9f41bb1a617ea6b2835b3316e79f0f21b8264e7bcd20e57c4092a70a0f6", - "--quick", - "--rpc-url", - rpc.as_str(), - ]) - .assert_success() - .stdout_eq(str![[r#" +casttest!( + #[ignore = "requires debug_traceTransaction, which most free Celo RPC endpoints no longer support"] + flaky_run_celo_with_precompiles, + |_prj, cmd| { + let rpc = next_rpc_endpoint(NamedChain::Celo); + cmd.args([ + "run", + "0xa652b9f41bb1a617ea6b2835b3316e79f0f21b8264e7bcd20e57c4092a70a0f6", + "--quick", + "--rpc-url", + rpc.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" Traces: [17776] 0x471EcE3750Da237f93B8E339c536989b8978a438::transfer(0xD2eB2d37d238Caeff39CFA36A013299C6DbAC56A, 138000000000000000 [1.38e17]) ├─ [12370] 0xFeA1B35f1D5f2A58532a70e7A32e6F2D3Bc4F7B1::transfer(0xD2eB2d37d238Caeff39CFA36A013299C6DbAC56A, 138000000000000000 [1.38e17]) [delegatecall] @@ -4614,7 +4617,8 @@ Transaction successfully executed. [GAS] "#]]); -}); + } +); casttest!(keccak_stdin_bytes, |_prj, cmd| { cmd.args(["keccak"]).stdin("0x12").assert_success().stdout_eq(str![[r#" 0x5fa2358263196dbbf23d1ca7a509451f7a2f64c15837bfbb81298b1e3e24e4fa diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 799945a663161..5b8724e559257 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -20,6 +20,7 @@ foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm-core.workspace = true +foundry-primitives.workspace = true foundry-evm-fuzz.workspace = true foundry-evm-traces.workspace = true foundry-wallets.workspace = true @@ -55,6 +56,7 @@ jsonpath_lib.workspace = true k256.workspace = true memchr.workspace = true p256 = "0.13" +ed25519-consensus = "2.1" ecdsa = "0.16" rand.workspace = true revm.workspace = true diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 67820b2030c23..7a865cc348386 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3780,6 +3780,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "createEd25519Key", + "description": "Generates an Ed25519 key pair from a deterministic salt.\nReturns (publicKey, privateKey) as 32-byte values.", + "declaration": "function createEd25519Key(bytes32 salt) external pure returns (bytes32 publicKey, bytes32 privateKey);", + "visibility": "external", + "mutability": "pure", + "signature": "createEd25519Key(bytes32)", + "selector": "0x1ef3f27a", + "selectorBytes": [ + 30, + 243, + 242, + 122 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "createFork_0", @@ -5084,6 +5104,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "executeTransaction", + "description": "Executes an RLP-encoded signed transaction with full EVM semantics (like `--isolate` mode).\nThe transaction is decoded from EIP-2718 format (type byte prefix + RLP payload) or legacy RLP.\nReturns the execution output bytes.\nThis cheatcode is not allowed in `forge script` contexts.", + "declaration": "function executeTransaction(bytes calldata rawTx) external returns (bytes memory);", + "visibility": "external", + "mutability": "", + "signature": "executeTransaction(bytes)", + "selector": "0x943d7209", + "selectorBytes": [ + 148, + 61, + 114, + 9 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "exists", @@ -8426,6 +8466,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "publicKeyEd25519", + "description": "Derives the Ed25519 public key from a private key.", + "declaration": "function publicKeyEd25519(bytes32 privateKey) external pure returns (bytes32 publicKey);", + "visibility": "external", + "mutability": "pure", + "signature": "publicKeyEd25519(bytes32)", + "selector": "0x27f44236", + "selectorBytes": [ + 39, + 244, + 66, + 54 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "publicKeyP256", @@ -10150,6 +10210,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "signEd25519", + "description": "Signs a message with namespace using Ed25519.\nThe signature covers namespace || message for domain separation.\nReturns a 64-byte Ed25519 signature.", + "declaration": "function signEd25519(bytes calldata namespace, bytes calldata message, bytes32 privateKey) external pure returns (bytes memory signature);", + "visibility": "external", + "mutability": "pure", + "signature": "signEd25519(bytes,bytes,bytes32)", + "selector": "0xef609c65", + "selectorBytes": [ + 239, + 96, + 156, + 101 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "signP256", @@ -11332,6 +11412,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "verifyEd25519", + "description": "Verifies an Ed25519 signature over namespace || message.\nReturns true if signature is valid, false otherwise.", + "declaration": "function verifyEd25519(bytes calldata signature, bytes calldata namespace, bytes calldata message, bytes32 publicKey) external pure returns (bool valid);", + "visibility": "external", + "mutability": "pure", + "signature": "verifyEd25519(bytes,bytes,bytes,bytes32)", + "selector": "0xd08c2888", + "selectorBytes": [ + 208, + 140, + 40, + 136 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "warmSlot", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 640c97e3108fb..c703c2fc5fd9a 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -563,6 +563,14 @@ interface Vm { #[cheatcode(group = Evm, safety = Unsafe)] function setBlockhash(uint256 blockNumber, bytes32 blockHash) external; + /// Executes an RLP-encoded signed transaction with full EVM semantics (like `--isolate` mode). + /// The transaction is decoded from EIP-2718 format (type byte prefix + RLP payload) or legacy RLP. + /// Returns the execution output bytes. + /// + /// This cheatcode is not allowed in `forge script` contexts. + #[cheatcode(group = Evm, safety = Unsafe)] + function executeTransaction(bytes calldata rawTx) external returns (bytes memory); + // -------- Account State -------- /// Sets an address' balance. @@ -2783,6 +2791,34 @@ interface Vm { #[cheatcode(group = Crypto)] function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); + /// Generates an Ed25519 key pair from a deterministic salt. + /// Returns (publicKey, privateKey) as 32-byte values. + #[cheatcode(group = Crypto, safety = Safe)] + function createEd25519Key(bytes32 salt) external pure returns (bytes32 publicKey, bytes32 privateKey); + + /// Derives the Ed25519 public key from a private key. + #[cheatcode(group = Crypto, safety = Safe)] + function publicKeyEd25519(bytes32 privateKey) external pure returns (bytes32 publicKey); + + /// Signs a message with namespace using Ed25519. + /// The signature covers namespace || message for domain separation. + /// Returns a 64-byte Ed25519 signature. + #[cheatcode(group = Crypto, safety = Safe)] + function signEd25519(bytes calldata namespace, bytes calldata message, bytes32 privateKey) + external + pure + returns (bytes memory signature); + + /// Verifies an Ed25519 signature over namespace || message. + /// Returns true if signature is valid, false otherwise. + #[cheatcode(group = Crypto, safety = Safe)] + function verifyEd25519( + bytes calldata signature, + bytes calldata namespace, + bytes calldata message, + bytes32 publicKey + ) external pure returns (bool valid); + /// Derive a private key from a provided mnemonic string (or mnemonic file path) /// at the derivation path `m/44'/60'/0'/0/{index}`. #[cheatcode(group = Crypto)] diff --git a/crates/cheatcodes/src/crypto.rs b/crates/cheatcodes/src/crypto.rs index 924a44ef7ce32..fad6f945aadf5 100644 --- a/crates/cheatcodes/src/crypto.rs +++ b/crates/cheatcodes/src/crypto.rs @@ -21,6 +21,11 @@ use p256::ecdsa::{ Signature as P256Signature, SigningKey as P256SigningKey, signature::hazmat::PrehashSigner, }; +use ed25519_consensus::{ + Signature as Ed25519Signature, SigningKey as Ed25519SigningKey, + VerificationKey as Ed25519VerificationKey, +}; + /// The BIP32 default derivation path prefix. const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/"; @@ -209,6 +214,34 @@ impl Cheatcode for publicKeyP256Call { } } +impl Cheatcode for createEd25519KeyCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { salt } = self; + create_ed25519_key(salt) + } +} + +impl Cheatcode for publicKeyEd25519Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { privateKey } = self; + public_key_ed25519(privateKey) + } +} + +impl Cheatcode for signEd25519Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { namespace, message, privateKey } = self; + sign_ed25519(namespace, message, privateKey) + } +} + +impl Cheatcode for verifyEd25519Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { signature, namespace, message, publicKey } = self; + verify_ed25519(signature, namespace, message, publicKey) + } +} + /// Using a given private key, return its public ETH address, its public key affine x and y /// coordinates, and its private key (see the 'Wallet' struct) /// @@ -399,6 +432,47 @@ fn parse_private_key_p256(private_key: &U256) -> Result { Ok(P256SigningKey::from_bytes((&private_key.to_be_bytes()).into())?) } +fn parse_signing_key_ed25519(private_key: &B256) -> Result { + Ed25519SigningKey::try_from(private_key.as_slice()) + .map_err(|e| fmt_err!("invalid Ed25519 private key: {e}")) +} + +fn create_ed25519_key(salt: &B256) -> Result { + let signing_key = parse_signing_key_ed25519(salt)?; + let public_key = B256::from_slice(signing_key.verification_key().as_ref()); + Ok((public_key, *salt).abi_encode()) +} + +fn public_key_ed25519(private_key: &B256) -> Result { + let signing_key = parse_signing_key_ed25519(private_key)?; + Ok(B256::from_slice(signing_key.verification_key().as_ref()).abi_encode()) +} + +fn sign_ed25519(namespace: &[u8], message: &[u8], private_key: &B256) -> Result { + let signing_key = parse_signing_key_ed25519(private_key)?; + let combined = [namespace, message].concat(); + let signature: [u8; 64] = signing_key.sign(&combined).into(); + Ok(signature.to_vec().abi_encode()) +} + +fn verify_ed25519(signature: &[u8], namespace: &[u8], message: &[u8], public_key: &B256) -> Result { + if signature.len() != 64 { + return Ok(false.abi_encode()); + } + + let Ok(verification_key) = Ed25519VerificationKey::try_from(public_key.as_slice()) else { + return Ok(false.abi_encode()); + }; + + let Ok(sig_bytes): Result<[u8; 64], _> = signature.try_into() else { + return Ok(false.abi_encode()); + }; + + let combined = [namespace, message].concat(); + let valid = verification_key.verify(&Ed25519Signature::from(sig_bytes), &combined).is_ok(); + Ok(valid.abi_encode()) +} + pub(super) fn parse_wallet(private_key: &U256) -> Result { parse_private_key(private_key).map(PrivateKeySigner::from) } @@ -596,4 +670,105 @@ mod tests { let msg = err.to_string(); assert!(msg.contains("invalid nonce scalar"), "unexpected error: {msg}"); } + + #[test] + fn test_create_ed25519_key_determinism() { + let salt = B256::from([1u8; 32]); + let result1 = create_ed25519_key(&salt).unwrap(); + let result2 = create_ed25519_key(&salt).unwrap(); + assert_eq!(result1, result2, "same salt should produce same keys"); + } + + #[test] + fn test_create_ed25519_key_different_salts() { + let salt1 = B256::from([1u8; 32]); + let salt2 = B256::from([2u8; 32]); + let result1 = create_ed25519_key(&salt1).unwrap(); + let result2 = create_ed25519_key(&salt2).unwrap(); + assert_ne!(result1, result2, "different salts should produce different keys"); + } + + #[test] + fn test_public_key_ed25519_consistency() { + let salt = B256::from([42u8; 32]); + let create_result = create_ed25519_key(&salt).unwrap(); + let (expected_public, private): (B256, B256) = + <(B256, B256)>::abi_decode(&create_result).unwrap(); + + let derived_public_result = public_key_ed25519(&private).unwrap(); + let derived_public = B256::abi_decode(&derived_public_result).unwrap(); + + assert_eq!(expected_public, derived_public, "derived public key should match"); + } + + #[test] + fn test_sign_and_verify_ed25519_valid() { + let salt = B256::from([123u8; 32]); + let create_result = create_ed25519_key(&salt).unwrap(); + let (public_key, private_key): (B256, B256) = + <(B256, B256)>::abi_decode(&create_result).unwrap(); + + let namespace = b"test.namespace"; + let message = b"hello world"; + let sig_result = sign_ed25519(namespace, message, &private_key).unwrap(); + let sig_bytes: Vec = Vec::abi_decode(&sig_result).unwrap(); + + let verify_result = verify_ed25519(&sig_bytes, namespace, message, &public_key).unwrap(); + let valid = bool::abi_decode(&verify_result).unwrap(); + + assert!(valid, "signature should be valid"); + } + + #[test] + fn test_verify_ed25519_invalid_signature() { + let salt = B256::from([123u8; 32]); + let create_result = create_ed25519_key(&salt).unwrap(); + let (public_key, _): (B256, B256) = <(B256, B256)>::abi_decode(&create_result).unwrap(); + + let invalid_sig = [0u8; 64]; + let namespace = b"test.namespace"; + let message = b"hello world"; + + let verify_result = verify_ed25519(&invalid_sig, namespace, message, &public_key).unwrap(); + let valid = bool::abi_decode(&verify_result).unwrap(); + + assert!(!valid, "invalid signature should not verify"); + } + + #[test] + fn test_verify_ed25519_namespace_separation() { + let salt = B256::from([123u8; 32]); + let create_result = create_ed25519_key(&salt).unwrap(); + let (public_key, private_key): (B256, B256) = + <(B256, B256)>::abi_decode(&create_result).unwrap(); + + let namespace_a = b"namespace.a"; + let message = b"message"; + let sig_result = sign_ed25519(namespace_a, message, &private_key).unwrap(); + let sig_bytes: Vec = Vec::abi_decode(&sig_result).unwrap(); + + let namespace_b = b"namespace.b"; + let verify_result = verify_ed25519(&sig_bytes, namespace_b, message, &public_key).unwrap(); + let valid = bool::abi_decode(&verify_result).unwrap(); + assert!(!valid, "signature with namespace A should not verify with namespace B"); + + let verify_result = verify_ed25519(&sig_bytes, namespace_a, message, &public_key).unwrap(); + let valid = bool::abi_decode(&verify_result).unwrap(); + assert!(valid, "signature should verify with correct namespace"); + } + + #[test] + fn test_verify_ed25519_invalid_signature_length() { + let salt = B256::from([123u8; 32]); + let create_result = create_ed25519_key(&salt).unwrap(); + let (public_key, _): (B256, B256) = <(B256, B256)>::abi_decode(&create_result).unwrap(); + + let invalid_sig = [0u8; 32]; + let namespace = b"test"; + let message = b"message"; + + let verify_result = verify_ed25519(&invalid_sig, namespace, message, &public_key).unwrap(); + let valid = bool::abi_decode(&verify_result).unwrap(); + assert!(!valid, "signature with wrong length should not verify"); + } } diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index b5e76e1148694..a71fc966b43a9 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -6,6 +6,7 @@ use crate::{ inspector::{Ecx, RecordDebugStepInfo}, }; use alloy_consensus::TxEnvelope; +use alloy_evm::{Evm as _, FromRecoveredTx}; use alloy_genesis::{Genesis, GenesisAccount}; use alloy_network::eip2718::EIP4844_TX_TYPE_ID; use alloy_primitives::{ @@ -26,16 +27,18 @@ use foundry_evm_core::{ ContextExt, backend::{DatabaseExt, RevertStateSnapshotAction}, constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS}, + evm::new_evm_with_inspector, utils::get_blob_base_fee_update_fraction_by_spec_id, }; use foundry_evm_traces::TraceMode; +use foundry_primitives::FoundryTxEnvelope; use itertools::Itertools; use rand::Rng; use revm::{ bytecode::Bytecode, - context::{Block, JournalTr}, + context::{Block, JournalTr, TxEnv, result::ExecutionResult}, primitives::{KECCAK_EMPTY, hardfork::SpecId}, - state::Account, + state::{Account, AccountStatus}, }; use std::{ collections::{BTreeMap, HashSet, btree_map::Entry}, @@ -959,10 +962,11 @@ impl Cheatcode for getStorageSlotsCall { trace!(storage = ?storage_layout.storage, "fetched storage"); + let variable_name_lower = variableName.to_lowercase(); let storage = storage_layout .storage .iter() - .find(|s| s.label.to_lowercase() == *variableName.to_lowercase()) + .find(|s| s.label.to_lowercase() == variable_name_lower) .ok_or_else(|| fmt_err!("variable '{variableName}' not found in storage layout"))?; let storage_type = storage_layout @@ -1080,6 +1084,133 @@ impl Cheatcode for setBlockhashCall { } } +impl Cheatcode for executeTransactionCall { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { + use crate::env::FORGE_CONTEXT; + + // Block in script contexts. + if let Some(ctx) = FORGE_CONTEXT.get() + && *ctx == ForgeContext::ScriptGroup + { + return Err(fmt_err!("executeTransaction is not allowed in forge script")); + } + + // Decode the RLP-encoded signed transaction. + let tx = FoundryTxEnvelope::decode(&mut self.rawTx.as_ref()) + .map_err(|err| fmt_err!("failed to decode RLP-encoded transaction: {err}"))?; + + // Reject unsupported transaction types. + // TODO: add support for OP deposit transactions. + if matches!(tx, FoundryTxEnvelope::Deposit(_)) { + return Err(fmt_err!( + "OP deposit transactions are not yet supported by executeTransaction" + )); + } + // TODO: add support for Tempo AA transactions. + if matches!(tx, FoundryTxEnvelope::Tempo(_)) { + return Err(fmt_err!("Tempo transactions are not yet supported by executeTransaction")); + } + + // Recover signer from the transaction signature. + let sender = tx.recover().map_err(|err| fmt_err!("failed to recover signer: {err}"))?; + + // Build TxEnv from the recovered transaction. + let tx_env = >::from_recovered_tx(&tx, sender); + + let mut inspector = executor.get_inspector(ccx.state); + + let res = { + let (db, journal, env) = ccx.ecx.as_db_env_and_journal(); + let cached_env = + foundry_evm_core::Env::from(env.cfg.clone(), env.block.clone(), env.tx.clone()); + + // Override env for isolated execution. + env.block.basefee = 0; + *env.tx = tx_env; + env.tx.gas_price = 0; + env.tx.gas_priority_fee = None; + + // Enable nonce checks for realistic simulation. + env.cfg.disable_nonce_check = false; + + // EIP-3860: enforce initcode size limit. + env.cfg.limit_contract_initcode_size = + Some(revm::primitives::eip3860::MAX_INITCODE_SIZE); + + // Create a new EVM instance with the inspector. + let mut evm = new_evm_with_inspector(db, env.to_owned(), &mut *inspector); + + // Clone journaled state and mark all accounts/slots cold. + evm.journaled_state.state = { + let mut state = journal.state.clone(); + for (addr, acc_mut) in &mut state { + if journal.warm_addresses.is_cold(addr) { + acc_mut.mark_cold(); + } + for slot_mut in acc_mut.storage.values_mut() { + slot_mut.is_cold = true; + slot_mut.original_value = slot_mut.present_value; + } + } + state + }; + + // Set depth to 1 for proper trace collection. + evm.journaled_state.depth = 1; + + let res = evm.transact(env.tx.clone()); + + // Restore the original environment. + *env.tx = cached_env.tx; + env.block.basefee = cached_env.evm_env.block_env.basefee; + + res + }; + + let res = res.map_err(|e| fmt_err!("transaction execution failed: {e}"))?; + + // Merge state changes back into the parent journaled state. + for (addr, mut acc) in res.state { + let Some(acc_mut) = ccx.ecx.journaled_state.state.get_mut(&addr) else { + ccx.ecx.journaled_state.state.insert(addr, acc); + continue; + }; + + // Preserve warm account status from parent context. + if acc.status.contains(AccountStatus::Cold) + && !acc_mut.status.contains(AccountStatus::Cold) + { + acc.status -= AccountStatus::Cold; + } + acc_mut.info = acc.info; + acc_mut.status |= acc.status; + + // Merge storage changes. + for (key, val) in acc.storage { + let Some(slot_mut) = acc_mut.storage.get_mut(&key) else { + acc_mut.storage.insert(key, val); + continue; + }; + slot_mut.present_value = val.present_value; + slot_mut.is_cold &= val.is_cold; + } + } + + // Return output bytes. + let output = match res.result { + ExecutionResult::Success { output, .. } => output.into_data(), + ExecutionResult::Halt { reason, .. } => { + return Err(fmt_err!("transaction halted: {reason:?}")); + } + ExecutionResult::Revert { output, .. } => { + return Err(fmt_err!("transaction reverted: {}", hex::encode_prefixed(&output))); + } + }; + + Ok(output.abi_encode()) + } +} + impl Cheatcode for startDebugTraceRecordingCall { fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let Some(tracer) = executor.tracing_inspector() else { diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 711f2b185e881..a8db91023bc57 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -766,6 +766,13 @@ impl Cheatcodes { return None; } + // `expectRevert`: track max call depth. This is also done in `initialize_interp`, but + // precompile calls don't create an interpreter frame so we must also track it here. + // The callee executes at `curr_depth + 1`. + if let Some(expected) = &mut self.expected_revert { + expected.max_depth = max(curr_depth + 1, expected.max_depth); + } + // Handle expected calls // Grab the different calldatas expected. diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index bdbfc583b5d04..46b5471bce76f 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -932,10 +932,7 @@ pub(crate) fn handle_expect_emit( } // Maybe match source address. - if event_to_fill_or_check - .address - .is_some_and(|addr| addr.to_checksum(None) != log.address.to_checksum(None)) - { + if event_to_fill_or_check.address.is_some_and(|addr| addr != log.address) { event_to_fill_or_check.mismatch_error = Some(format!( "log emitter mismatch: expected={:#x}, got={:#x}", event_to_fill_or_check.address.unwrap(), diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index abb6be8693585..929e3b87a833a 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -213,15 +213,19 @@ impl SessionSource { let executor = ExecutorBuilder::new() .inspectors(|stack| { - stack.chisel_state(final_pc).trace_mode(TraceMode::Call).cheatcodes( - CheatsConfig::new( - &self.config.foundry_config, - self.config.evm_opts.clone(), - None, - None, + stack + .logs(self.config.foundry_config.live_logs) + .chisel_state(final_pc) + .trace_mode(TraceMode::Call) + .cheatcodes( + CheatsConfig::new( + &self.config.foundry_config, + self.config.evm_opts.clone(), + None, + None, + ) + .into(), ) - .into(), - ) }) .gas_limit(self.config.evm_opts.gas_limit()) .spec_id(self.config.foundry_config.evm_spec_id()) diff --git a/crates/chisel/tests/it/repl/mod.rs b/crates/chisel/tests/it/repl/mod.rs index 6fb38b6ca1805..aa9c00f471aa7 100644 --- a/crates/chisel/tests/it/repl/mod.rs +++ b/crates/chisel/tests/it/repl/mod.rs @@ -264,3 +264,13 @@ repl_test!(uninitialized_variables, |repl| { repl.sendln("y"); repl.expect("Data: 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"); }); + +repl_test!(chisel_can_run_with_live_logs_flag, "--live-logs", init = true, |repl| { + repl.sendln("import {console} from 'forge-std/Script.sol';"); + repl.sendln("console.log('Hello, World!');"); + repl.expect("Hello, World!"); + + repl.sendln("console.log('Goodbye, World!');"); + repl.expect("Hello, World!"); // old log is also printed + repl.expect("Goodbye, World!"); +}); diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index 6499d693ddf40..7af7c20fb021a 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -93,6 +93,11 @@ pub struct EvmArgs { #[serde(skip)] pub ffi: bool, + /// Whether to show `console.log` outputs in realtime during script/test execution + #[arg(long)] + #[serde(skip)] + pub live_logs: bool, + /// Use the create 2 factory in all cases including tests and non-broadcasting scripts. #[arg(long)] #[serde(skip)] @@ -157,6 +162,10 @@ impl Provider for EvmArgs { dict.insert("ffi".to_string(), self.ffi.into()); } + if self.live_logs { + dict.insert("live_logs".to_string(), self.live_logs.into()); + } + if self.isolate { dict.insert("isolate".to_string(), self.isolate.into()); } diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index f9cc8fee83703..15a5de678272a 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -129,6 +129,9 @@ impl RpcOpts { if self.no_proxy { dict.insert("eth_rpc_no_proxy".into(), true.into()); } + if self.curl { + dict.insert("eth_rpc_curl".into(), true.into()); + } dict } diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index dff85f9457fa7..a91bea68f121f 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -101,22 +101,14 @@ fn env_filter() -> tracing_subscriber::EnvFilter { /// Returns a [RetryProvider] instantiated using [Config]'s RPC settings. pub fn get_provider(config: &Config) -> Result { - get_provider_builder(config, false)?.build() -} - -/// Returns a [RetryProvider] with curl mode option. -/// -/// When `curl_mode` is true, the provider will print equivalent curl commands -/// to stdout instead of executing RPC requests. -pub fn get_provider_with_curl(config: &Config, curl_mode: bool) -> Result { - get_provider_builder(config, curl_mode)?.build() + get_provider_builder(config)?.build() } /// Returns a [ProviderBuilder] instantiated using [Config] values. /// /// Defaults to `http://localhost:8545` and `Mainnet`. -pub fn get_provider_builder(config: &Config, curl_mode: bool) -> Result { - ProviderBuilder::from_config(config).map(|builder| builder.curl_mode(curl_mode)) +pub fn get_provider_builder(config: &Config) -> Result { + ProviderBuilder::from_config(config) } pub async fn get_chain

(chain: Option, provider: P) -> Result @@ -289,7 +281,7 @@ impl CommandUtils for Command { }; if !msg.is_empty() { err.push(':'); - err.push(if msg.lines().count() == 0 { ' ' } else { '\n' }); + err.push(if msg.lines().count() == 1 { ' ' } else { '\n' }); err.push_str(&msg); } Err(eyre::eyre!(err)) diff --git a/crates/common/src/preprocessor/mod.rs b/crates/common/src/preprocessor/mod.rs index bc27cca580eee..7499776cf2c72 100644 --- a/crates/common/src/preprocessor/mod.rs +++ b/crates/common/src/preprocessor/mod.rs @@ -46,7 +46,7 @@ impl Preprocessor for DynamicTestLinkingPreprocessor { ) -> Result<()> { // Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing. if !input.input.sources.iter().any(|(path, _)| paths.is_test_or_script(path)) { - trace!("no tests or sources to preprocess"); + trace!("no tests or scripts to preprocess"); return Ok(()); } diff --git a/crates/common/src/provider/mod.rs b/crates/common/src/provider/mod.rs index a098c33a34827..764a44db55baa 100644 --- a/crates/common/src/provider/mod.rs +++ b/crates/common/src/provider/mod.rs @@ -163,6 +163,7 @@ impl ProviderBuilder { let mut builder = Self::new(url.as_ref()); builder = builder.accept_invalid_certs(config.eth_rpc_accept_invalid_certs); + builder = builder.curl_mode(config.eth_rpc_curl); if let Ok(chain) = config.chain.unwrap_or_default().try_into() { builder = builder.chain(chain); @@ -451,7 +452,11 @@ fn resolve_path(path: &Path) -> Result { { return Ok(path.to_path_buf()); } - Err(()) + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + std::env::current_dir().map(|d| d.join(path)).map_err(drop) + } } #[cfg(test)] diff --git a/crates/common/src/term.rs b/crates/common/src/term.rs index 77d860a80038d..6fe1672d55000 100644 --- a/crates/common/src/term.rs +++ b/crates/common/src/term.rs @@ -1,7 +1,7 @@ //! terminal utils use foundry_compilers::{ artifacts::remappings::Remapping, - report::{self, BasicStdoutReporter, Reporter}, + report::{self, Reporter}, }; use itertools::Itertools; use semver::Version; @@ -209,19 +209,6 @@ impl Reporter for SpinnerReporter { } } -/// If the output medium is terminal, this calls `f` within the [`SpinnerReporter`] that displays a -/// spinning cursor to display solc progress. -/// -/// If no terminal is available this falls back to common `println!` in [`BasicStdoutReporter`]. -pub fn with_spinner_reporter(project_root: Option, f: impl FnOnce() -> T) -> T { - let reporter = if TERM_SETTINGS.indicate_progress { - report::Report::new(SpinnerReporter::spawn(project_root)) - } else { - report::Report::new(BasicStdoutReporter::default()) - }; - report::with_scoped(&reporter, f) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index b002a94e5fd41..e34e0f148eccb 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -161,7 +161,7 @@ pub fn get_pretty_tx_receipt_attr( "gasUsed" | "gas_used" => Some(receipt.receipt.gas_used.to_string()), "logs" => Some(receipt.receipt.inner.inner.inner.receipt.logs.as_slice().pretty()), "logsBloom" | "logs_bloom" => Some(receipt.receipt.inner.inner.inner.logs_bloom.pretty()), - "root" | "stateRoot" | "state_root " => Some(receipt.receipt.state_root().pretty()), + "root" | "stateRoot" | "state_root" => Some(receipt.receipt.state_root().pretty()), "status" | "statusCode" | "status_code" => { Some(receipt.receipt.inner.inner.inner.receipt.status.pretty()) } diff --git a/crates/config/src/endpoints.rs b/crates/config/src/endpoints.rs index 0901842a00bff..af25127beb585 100644 --- a/crates/config/src/endpoints.rs +++ b/crates/config/src/endpoints.rs @@ -63,22 +63,6 @@ pub enum RpcEndpointType { } impl RpcEndpointType { - /// Returns the string variant - pub fn as_endpoint_string(&self) -> Option<&RpcEndpointUrl> { - match self { - Self::String(url) => Some(url), - Self::Config(_) => None, - } - } - - /// Returns the config variant - pub fn as_endpoint_config(&self) -> Option<&RpcEndpoint> { - match self { - Self::Config(config) => Some(config), - Self::String(_) => None, - } - } - /// Returns the url or config this type holds /// /// # Error @@ -137,14 +121,6 @@ impl RpcEndpointUrl { } } - /// Returns the env variant - pub fn as_env(&self) -> Option<&str> { - match self { - Self::Env(val) => Some(val), - Self::Url(_) => None, - } - } - /// Returns the url this type holds /// /// # Error diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index e24f92b7d931e..7fb68deb7accc 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -350,22 +350,6 @@ pub enum EtherscanApiKey { } impl EtherscanApiKey { - /// Returns the key variant - pub fn as_key(&self) -> Option<&str> { - match self { - Self::Key(url) => Some(url), - Self::Env(_) => None, - } - } - - /// Returns the env variant - pub fn as_env(&self) -> Option<&str> { - match self { - Self::Env(val) => Some(val), - Self::Key(_) => None, - } - } - /// Returns the key this type holds /// /// # Error diff --git a/crates/config/src/invariant.rs b/crates/config/src/invariant.rs index 591af88efdee4..4db46607f1885 100644 --- a/crates/config/src/invariant.rs +++ b/crates/config/src/invariant.rs @@ -49,6 +49,8 @@ pub struct InvariantConfig { /// /// Example: `check_interval = 10` means assert after calls 10, 20, 30, ... and the last call. pub check_interval: u32, + /// Continue invariant run until all invariants declared in current test suite breaks. + pub continuous_run: bool, } impl Default for InvariantConfig { @@ -70,6 +72,7 @@ impl Default for InvariantConfig { max_time_delay: None, max_block_delay: None, check_interval: 1, + continuous_run: false, } } } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d2b9570339a01..c133fa01291c4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -304,6 +304,8 @@ pub struct Config { /// You can also the ETH_RPC_HEADERS env variable like so: /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"` pub eth_rpc_headers: Option>, + /// Print the equivalent curl command instead of making the RPC request. + pub eth_rpc_curl: bool, /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table pub etherscan_api_key: Option, /// Multiple etherscan api configs and their aliases @@ -352,6 +354,8 @@ pub struct Config { pub invariant: InvariantConfig, /// Whether to allow ffi cheatcodes in test pub ffi: bool, + /// Whether to show `console.log` outputs in realtime during script/test execution + pub live_logs: bool, /// Whether to allow `expectRevert` for internal functions. pub allow_internal_expect_revert: bool, /// Use the create 2 factory in all cases including tests and non-broadcasting scripts. @@ -1863,11 +1867,6 @@ impl Config { src: paths.sources.file_name().unwrap().into(), out: artifacts.clone(), libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(), - remappings: paths - .remappings - .into_iter() - .map(|r| RelativeRemapping::new(r, root)) - .collect(), fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]), ..Self::default() } @@ -2328,8 +2327,8 @@ impl Config { figment = figment.merge(("evm_version", version)); } - // Normalize `deny` based on the provided `deny_warnings` version. - if self.deny_warnings + // Normalize `deny` based on the provided `deny_warnings` value. + if figment.extract_inner::("deny_warnings").unwrap_or(false) && let Ok(DenyLevel::Never) = figment.extract_inner("deny") { figment = figment.merge(("deny", DenyLevel::Warnings)); @@ -2570,6 +2569,7 @@ impl Default for Config { invariant: InvariantConfig::new("cache/invariant".into()), always_use_create_2_factory: false, ffi: false, + live_logs: false, allow_internal_expect_revert: false, prompt_timeout: 120, sender: Self::DEFAULT_SENDER, @@ -2596,6 +2596,7 @@ impl Default for Config { eth_rpc_jwt: None, eth_rpc_timeout: None, eth_rpc_headers: None, + eth_rpc_curl: false, etherscan_api_key: None, verbosity: 0, remappings: vec![], @@ -6870,6 +6871,156 @@ mod tests { }); } + // Test for issue #13316: vyper config keys should not trigger unknown key warnings + #[test] + fn no_false_warnings_for_vyper_config_keys() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + src = "src" + + [vyper] + optimize = "gas" + path = "/usr/bin/vyper" + experimental_codegen = true + "#, + )?; + + let cfg = Config::load().unwrap(); + // None of the valid vyper keys should trigger warnings + let vyper_warnings: Vec<_> = cfg + .warnings + .iter() + .filter(|w| { + matches!( + w, + crate::Warning::UnknownSectionKey { section, .. } if section == "vyper" + ) + }) + .collect(); + + assert!( + vyper_warnings.is_empty(), + "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}" + ); + + Ok(()) + }); + } + + // Test for issue #13316: vyper config in profile should not trigger false warnings + #[test] + fn no_false_warnings_for_nested_vyper_config_keys() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + src = "src" + + [profile.default.vyper] + optimize = "codesize" + path = "/opt/vyper/bin/vyper" + experimental_codegen = false + "#, + )?; + + let cfg = Config::load().unwrap(); + // None of the valid vyper keys should trigger warnings + let vyper_warnings: Vec<_> = cfg + .warnings + .iter() + .filter(|w| { + matches!( + w, + crate::Warning::UnknownSectionKey { section, .. } if section == "vyper" + ) + }) + .collect(); + + assert!( + vyper_warnings.is_empty(), + "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}" + ); + + Ok(()) + }); + } + + // Test for issue #13316: inline vyper config format should not trigger false warnings + // This matches the exact format used in https://github.com/pcaversaccio/snekmate + #[test] + fn no_false_warnings_for_inline_vyper_config() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + src = "src" + vyper = { optimize = "gas" } + + [profile.default-venom] + vyper = { experimental_codegen = true } + + [profile.ci-venom] + vyper = { experimental_codegen = true } + "#, + )?; + + let cfg = Config::load().unwrap(); + let vyper_warnings: Vec<_> = cfg + .warnings + .iter() + .filter(|w| { + matches!( + w, + crate::Warning::UnknownSectionKey { section, .. } if section == "vyper" + ) + }) + .collect(); + + assert!( + vyper_warnings.is_empty(), + "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}" + ); + + Ok(()) + }); + } + + // Test for issue #13316: unknown vyper keys should still warn + #[test] + fn warns_on_unknown_vyper_keys() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [profile.default] + src = "src" + + [vyper] + optimize = "gas" + unknown_vyper_option = true + "#, + )?; + + let cfg = Config::load().unwrap(); + assert!( + cfg.warnings.iter().any(|w| matches!( + w, + crate::Warning::UnknownSectionKey { key, section, .. } + if key == "unknown_vyper_option" && section == "vyper" + )), + "Unknown vyper key should trigger warning, got: {:?}", + cfg.warnings + ); + + Ok(()) + }); + } + // Test for issue #12844: known profile should work #[test] fn succeeds_on_known_profile() { diff --git a/crates/config/src/providers/warnings.rs b/crates/config/src/providers/warnings.rs index d9fe76a005a38..9229dba8718be 100644 --- a/crates/config/src/providers/warnings.rs +++ b/crates/config/src/providers/warnings.rs @@ -24,6 +24,11 @@ const COMPILATION_RESTRICTIONS_KEYS: &[&str] = &[ const SETTINGS_OVERRIDES_KEYS: &[&str] = &["name", "via_ir", "evm_version", "optimizer", "optimizer_runs", "bytecode_hash"]; +/// Allowed keys for VyperConfig. +/// Required because VyperConfig uses `skip_serializing_if = "Option::is_none"` on all fields, +/// causing the default serialization to produce an empty dict. +const VYPER_KEYS: &[&str] = &["optimize", "path", "experimental_codegen"]; + /// Reserved keys that should not trigger unknown key warnings. const RESERVED_KEYS: &[&str] = &["extends"]; @@ -183,15 +188,20 @@ impl WarningsProvider

{ }; // Get allowed keys for this section from the default config - let Some(default_section_value) = default_dict.get(*section_name) else { - continue; - }; - let Some(default_section_dict) = default_section_value.as_dict() else { - continue; + // Special case for vyper: VyperConfig uses skip_serializing_if on all Option fields, + // so the default serialization produces an empty dict. Use explicit keys instead. + let allowed_keys: BTreeSet = if *section_name == "vyper" { + VYPER_KEYS.iter().map(|s| s.to_string()).collect() + } else { + let Some(default_section_value) = default_dict.get(*section_name) else { + continue; + }; + let Some(default_section_dict) = default_section_value.as_dict() else { + continue; + }; + default_section_dict.keys().cloned().collect() }; - let allowed_keys: BTreeSet = default_section_dict.keys().cloned().collect(); - for key in section_dict.keys() { let is_not_allowed = !allowed_keys.contains(key) && !allowed_keys.contains(&key.to_snake_case()); @@ -248,15 +258,20 @@ impl WarningsProvider

{ }; // Get allowed keys from the default config for this nested section - let Some(default_value) = default_dict.get(key) else { - continue; - }; - let Some(default_nested_dict) = default_value.as_dict() else { - continue; + // Special case for vyper: VyperConfig uses skip_serializing_if on all Option fields, + // so the default serialization produces an empty dict. Use explicit keys instead. + let allowed_keys: BTreeSet = if key == "vyper" { + VYPER_KEYS.iter().map(|s| s.to_string()).collect() + } else { + let Some(default_value) = default_dict.get(key) else { + continue; + }; + let Some(default_nested_dict) = default_value.as_dict() else { + continue; + }; + default_nested_dict.keys().cloned().collect() }; - let allowed_keys: BTreeSet = default_nested_dict.keys().cloned().collect(); - for nested_key in nested_dict.keys() { let is_not_allowed = !allowed_keys.contains(nested_key) && !allowed_keys.contains(&nested_key.to_snake_case()); diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index e3d55b69cd58b..590108e155fda 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -11,7 +11,7 @@ use crate::{ use alloy_consensus::Typed2718; use alloy_evm::Evm; use alloy_genesis::GenesisAccount; -use alloy_network::{AnyRpcBlock, AnyTxEnvelope, TransactionResponse}; +use alloy_network::{AnyNetwork, AnyRpcBlock, AnyTxEnvelope, TransactionResponse}; use alloy_primitives::{Address, B256, TxKind, U256, keccak256, uint}; use alloy_rpc_types::{BlockNumberOrTag, Transaction, TransactionRequest}; use eyre::Context; @@ -438,7 +438,7 @@ struct _ObjectSafe(dyn DatabaseExt); #[must_use] pub struct Backend { /// The access point for managing forks - forks: MultiFork, + forks: MultiFork, // The default in memory db mem_db: FoundryEvmInMemoryDB, /// The journaled_state to use to initialize new forks with @@ -481,7 +481,7 @@ impl Backend { /// database. /// /// Prefer using [`spawn`](Self::spawn) instead. - pub fn new(forks: MultiFork, fork: Option) -> eyre::Result { + pub fn new(forks: MultiFork, fork: Option) -> eyre::Result { trace!(target: "backend", forking_mode=?fork.is_some(), "creating executor backend"); // Note: this will take of registering the `fork` let inner = BackendInner { diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 9fe6a7b9b5c3e..0e28d92d91cae 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -54,7 +54,7 @@ pub fn decode_console_logs(logs: &[Log]) -> Vec { /// /// This function returns [None] if it is not a DSTest log or the result of a Hardhat /// `console.log`. -fn decode_console_log(log: &Log) -> Option { +pub fn decode_console_log(log: &Log) -> Option { console::ds::ConsoleEvents::decode_log(log).ok().map(|decoded| decoded.to_string()) } diff --git a/crates/evm/core/src/fork/database.rs b/crates/evm/core/src/fork/database.rs index 6a53a19843b1b..65686ade1aada 100644 --- a/crates/evm/core/src/fork/database.rs +++ b/crates/evm/core/src/fork/database.rs @@ -4,6 +4,7 @@ use crate::{ backend::{RevertStateSnapshotAction, StateSnapshot}, state_snapshot::StateSnapshots, }; +use alloy_network::Network; use alloy_primitives::{Address, B256, U256, map::HashMap}; use alloy_rpc_types::BlockId; use foundry_fork_db::{BlockchainDb, DatabaseError, SharedBackend}; @@ -23,28 +24,28 @@ use std::sync::Arc; /// This database uses the `backend` for read and the `db` for write operations. But note the /// `backend` will also write (missing) data to the `db` in the background #[derive(Clone, Debug)] -pub struct ForkedDatabase { +pub struct ForkedDatabase { /// Responsible for fetching missing data. /// /// This is responsible for getting data. - backend: SharedBackend, + backend: SharedBackend, /// Cached Database layer, ensures that changes are not written to the database that /// exclusively stores the state of the remote client. /// /// This separates Read/Write operations /// - reads from the `SharedBackend as DatabaseRef` writes to the internal cache storage. - cache_db: CacheDB, + cache_db: CacheDB>, /// Contains all the data already fetched. /// /// This exclusively stores the _unchanged_ remote client state. db: BlockchainDb, /// Holds the state snapshots of a blockchain. - state_snapshots: Arc>>, + state_snapshots: Arc>>>, } -impl ForkedDatabase { +impl ForkedDatabase { /// Creates a new instance of this DB - pub fn new(backend: SharedBackend, db: BlockchainDb) -> Self { + pub fn new(backend: SharedBackend, db: BlockchainDb) -> Self { Self { cache_db: CacheDB::new(backend.clone()), backend, @@ -53,15 +54,15 @@ impl ForkedDatabase { } } - pub fn database(&self) -> &CacheDB { + pub fn database(&self) -> &CacheDB> { &self.cache_db } - pub fn database_mut(&mut self) -> &mut CacheDB { + pub fn database_mut(&mut self) -> &mut CacheDB> { &mut self.cache_db } - pub fn state_snapshots(&self) -> &Arc>> { + pub fn state_snapshots(&self) -> &Arc>>> { &self.state_snapshots } @@ -93,7 +94,7 @@ impl ForkedDatabase { &self.db } - pub fn create_state_snapshot(&self) -> ForkDbStateSnapshot { + pub fn create_state_snapshot(&self) -> ForkDbStateSnapshot { let db = self.db.db(); let state_snapshot = StateSnapshot { accounts: db.accounts.read().clone(), @@ -150,7 +151,7 @@ impl ForkedDatabase { } } -impl Database for ForkedDatabase { +impl Database for ForkedDatabase { type Error = DatabaseError; fn basic(&mut self, address: Address) -> Result, Self::Error> { @@ -173,7 +174,7 @@ impl Database for ForkedDatabase { } } -impl DatabaseRef for ForkedDatabase { +impl DatabaseRef for ForkedDatabase { type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { @@ -193,7 +194,7 @@ impl DatabaseRef for ForkedDatabase { } } -impl DatabaseCommit for ForkedDatabase { +impl DatabaseCommit for ForkedDatabase { fn commit(&mut self, changes: HashMap) { self.database_mut().commit(changes) } @@ -203,12 +204,12 @@ impl DatabaseCommit for ForkedDatabase { /// /// This mimics `revm::CacheDB` #[derive(Clone, Debug)] -pub struct ForkDbStateSnapshot { - pub local: CacheDB, +pub struct ForkDbStateSnapshot { + pub local: CacheDB>, pub state_snapshot: StateSnapshot, } -impl ForkDbStateSnapshot { +impl ForkDbStateSnapshot { fn get_storage(&self, address: Address, index: U256) -> Option { self.local .cache @@ -222,7 +223,7 @@ impl ForkDbStateSnapshot { // This `DatabaseRef` implementation works similar to `CacheDB` which prioritizes modified elements, // and uses another db as fallback // We prioritize stored changed accounts/storage -impl DatabaseRef for ForkDbStateSnapshot { +impl DatabaseRef for ForkDbStateSnapshot { type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { diff --git a/crates/evm/core/src/fork/multi.rs b/crates/evm/core/src/fork/multi.rs index 186183b9a8d2a..e5116000c4934 100644 --- a/crates/evm/core/src/fork/multi.rs +++ b/crates/evm/core/src/fork/multi.rs @@ -6,6 +6,7 @@ use super::CreateFork; use crate::Env; use alloy_consensus::BlockHeader; +use alloy_network::Network; use alloy_primitives::{U256, map::HashMap}; use alloy_provider::network::BlockResponse; use foundry_config::Config; @@ -67,14 +68,14 @@ impl> From for ForkId { /// Can send requests to the `MultiForkHandler` to create forks. #[derive(Clone, Debug)] #[must_use] -pub struct MultiFork { +pub struct MultiFork { /// Channel to send `Request`s to the handler. - handler: Sender, + handler: Sender>, /// Ensures that all rpc resources get flushed properly. - _shutdown: Arc, + _shutdown: Arc>, } -impl MultiFork { +impl MultiFork { /// Creates a new pair and spawns the `MultiForkHandler` on a background thread. pub fn spawn() -> Self { trace!(target: "fork::multi", "spawning multifork"); @@ -116,7 +117,7 @@ impl MultiFork { /// /// Use [`spawn`](Self::spawn) instead. #[doc(hidden)] - pub fn new() -> (Self, MultiForkHandler) { + pub fn new() -> (Self, MultiForkHandler) { let (handler, handler_rx) = channel(1); let _shutdown = Arc::new(ShutDownMultiFork { handler: Some(handler.clone()) }); (Self { handler, _shutdown }, MultiForkHandler::new(handler_rx)) @@ -125,7 +126,7 @@ impl MultiFork { /// Returns a fork backend. /// /// If no matching fork backend exists it will be created. - pub fn create_fork(&self, fork: CreateFork) -> eyre::Result<(ForkId, SharedBackend, Env)> { + pub fn create_fork(&self, fork: CreateFork) -> eyre::Result<(ForkId, SharedBackend, Env)> { trace!("Creating new fork, url={}, block={:?}", fork.url, fork.evm_opts.fork_block_number); let (sender, rx) = oneshot_channel(); let req = Request::CreateFork(Box::new(fork), sender); @@ -140,7 +141,7 @@ impl MultiFork { &self, fork: ForkId, block: u64, - ) -> eyre::Result<(ForkId, SharedBackend, Env)> { + ) -> eyre::Result<(ForkId, SharedBackend, Env)> { trace!(?fork, ?block, "rolling fork"); let (sender, rx) = oneshot_channel(); let req = Request::RollFork(fork, block, sender); @@ -181,7 +182,7 @@ impl MultiFork { /// Returns the corresponding fork if it exists. /// /// Returns `None` if no matching fork backend is available. - pub fn get_fork(&self, id: impl Into) -> eyre::Result> { + pub fn get_fork(&self, id: impl Into) -> eyre::Result>> { let id = id.into(); trace!(?id, "get fork backend"); let (sender, rx) = oneshot_channel(); @@ -201,21 +202,20 @@ impl MultiFork { } } -type Handler = BackendHandler; -type CreateFuture = - Pin> + Send>>; -type CreateSender = OneshotSender>; +type CreateFuture = + Pin, BackendHandler)>> + Send>>; +type CreateSender = OneshotSender, Env)>>; type GetEnvSender = OneshotSender>; /// Request that's send to the handler. #[derive(Debug)] -enum Request { +enum Request { /// Creates a new ForkBackend. - CreateFork(Box, CreateSender), + CreateFork(Box, CreateSender), /// Returns the Fork backend for the `ForkId` if it exists. - GetFork(ForkId, OneshotSender>), + GetFork(ForkId, OneshotSender>>), /// Adjusts the block that's being forked, by creating a new fork at the new block. - RollFork(ForkId, u64, CreateSender), + RollFork(ForkId, u64, CreateSender), /// Returns the environment of the fork. GetEnv(ForkId, GetEnvSender), /// Updates the block number and timestamp of the fork. @@ -228,37 +228,37 @@ enum Request { GetForkUrl(ForkId, OneshotSender>), } -enum ForkTask { +enum ForkTask { /// Contains the future that will establish a new fork. - Create(CreateFuture, ForkId, CreateSender, Vec), + Create(CreateFuture, ForkId, CreateSender, Vec>), } /// The type that manages connections in the background. #[must_use = "futures do nothing unless polled"] -pub struct MultiForkHandler { +pub struct MultiForkHandler { /// Incoming requests from the `MultiFork`. - incoming: Fuse>, + incoming: Fuse>>, /// All active handlers. /// /// It's expected that this list will be rather small (<10). - handlers: Vec<(ForkId, Handler)>, + handlers: Vec<(ForkId, BackendHandler)>, // tasks currently in progress - pending_tasks: Vec, + pending_tasks: Vec>, /// All _unique_ forkids mapped to their corresponding backend. /// /// Note: The backend can be shared by multiple ForkIds if the target the same provider and /// block number. - forks: HashMap, + forks: HashMap>, /// Optional periodic interval to flush rpc cache. flush_cache_interval: Option, } -impl MultiForkHandler { - fn new(incoming: Receiver) -> Self { +impl MultiForkHandler { + fn new(incoming: Receiver>) -> Self { Self { incoming: incoming.fuse(), handlers: Default::default(), @@ -277,7 +277,7 @@ impl MultiForkHandler { /// Returns the list of additional senders of a matching task for the given id, if any. #[expect(irrefutable_let_patterns)] - fn find_in_progress_task(&mut self, id: &ForkId) -> Option<&mut Vec> { + fn find_in_progress_task(&mut self, id: &ForkId) -> Option<&mut Vec>> { for task in &mut self.pending_tasks { if let ForkTask::Create(_, in_progress, _, additional) = task && in_progress == id @@ -288,7 +288,7 @@ impl MultiForkHandler { None } - fn create_fork(&mut self, fork: CreateFork, sender: CreateSender) { + fn create_fork(&mut self, fork: CreateFork, sender: CreateSender) { let fork_id = ForkId::new(&fork.url, fork.evm_opts.fork_block_number); trace!(?fork_id, "created new forkId"); @@ -306,9 +306,9 @@ impl MultiForkHandler { fn insert_new_fork( &mut self, fork_id: ForkId, - fork: CreatedFork, - sender: CreateSender, - additional_senders: Vec, + fork: CreatedFork, + sender: CreateSender, + additional_senders: Vec>, ) { self.forks.insert(fork_id.clone(), fork.clone()); let _ = sender.send(Ok((fork_id.clone(), fork.backend.clone(), fork.opts.env.clone()))); @@ -336,7 +336,7 @@ impl MultiForkHandler { } } - fn on_request(&mut self, req: Request) { + fn on_request(&mut self, req: Request) { match req { Request::CreateFork(fork, sender) => self.create_fork(*fork, sender), Request::GetFork(fork_id, sender) => { @@ -380,7 +380,7 @@ impl MultiForkHandler { // Drives all handler to completion. // This future will finish once all underlying BackendHandler are completed. -impl Future for MultiForkHandler { +impl Future for MultiForkHandler { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -481,18 +481,18 @@ impl Future for MultiForkHandler { /// Tracks the created Fork #[derive(Debug, Clone)] -struct CreatedFork { +struct CreatedFork { /// How the fork was initially created. opts: CreateFork, /// Copy of the sender. - backend: SharedBackend, + backend: SharedBackend, /// How many consumers there are, since a `SharedBacked` can be used by multiple /// consumers. num_senders: Arc, } -impl CreatedFork { - pub fn new(opts: CreateFork, backend: SharedBackend) -> Self { +impl CreatedFork { + pub fn new(opts: CreateFork, backend: SharedBackend) -> Self { Self { opts, backend, num_senders: Arc::new(AtomicUsize::new(1)) } } @@ -514,11 +514,11 @@ impl CreatedFork { /// This type intentionally does not implement `Clone` since it's intended that there's only once /// instance. #[derive(Debug)] -struct ShutDownMultiFork { - handler: Option>, +struct ShutDownMultiFork { + handler: Option>>, } -impl Drop for ShutDownMultiFork { +impl Drop for ShutDownMultiFork { fn drop(&mut self) { trace!(target: "fork::multi", "initiating shutdown"); let (sender, rx) = oneshot_channel(); @@ -535,11 +535,14 @@ impl Drop for ShutDownMultiFork { /// Creates a new fork. /// /// This will establish a new `Provider` to the endpoint and return the Fork Backend. -async fn create_fork(mut fork: CreateFork) -> eyre::Result<(ForkId, CreatedFork, Handler)> { +async fn create_fork( + mut fork: CreateFork, +) -> eyre::Result<(ForkId, CreatedFork, BackendHandler)> { let provider = fork.evm_opts.fork_provider_with_url(&fork.url)?; // Initialise the fork environment. - let (env, block) = fork.evm_opts.fork_evm_env_with_provider(&fork.url, &provider).await?; + let (env, block) = + fork.evm_opts.fork_evm_env_with_provider::<_, N>(&fork.url, &provider).await?; fork.env = env; let meta = BlockchainDbMeta::new(fork.env.evm_env.block_env.clone(), fork.url.clone()); diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index a5fa6f079b301..f2868ea84cd8e 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -4,14 +4,11 @@ use crate::{ constants::DEFAULT_CREATE2_DEPLOYER, fork::{CreateFork, configure_env}, }; -use alloy_network::Network; +use alloy_network::{AnyNetwork, Network}; use alloy_primitives::{Address, B256, U256}; -use alloy_provider::{Provider, network::AnyRpcBlock}; +use alloy_provider::{Provider, RootProvider, network::AnyRpcBlock}; use eyre::WrapErr; -use foundry_common::{ - ALCHEMY_FREE_TIER_CUPS, - provider::{ProviderBuilder, RetryProvider}, -}; +use foundry_common::{ALCHEMY_FREE_TIER_CUPS, provider::ProviderBuilder}; use foundry_config::{Chain, Config, GasLimit}; use foundry_evm_networks::NetworkConfigs; use revm::context::{BlockEnv, TxEnv}; @@ -116,8 +113,12 @@ impl Default for EvmOpts { } impl EvmOpts { - /// Returns a `RetryProvider` for the given fork URL configured with options in `self`. - pub fn fork_provider_with_url(&self, fork_url: &str) -> eyre::Result { + /// Returns a `RootProvider` for the given fork URL configured with options in `self` and + /// annotated `Network` type. + pub fn fork_provider_with_url( + &self, + fork_url: &str, + ) -> eyre::Result> { ProviderBuilder::new(fork_url) .maybe_max_retry(self.fork_retries) .maybe_initial_backoff(self.fork_retry_backoff) @@ -141,7 +142,7 @@ impl EvmOpts { /// Returns the `revm::Env` that is configured with settings retrieved from the endpoint, /// and the block that was used to configure the environment. pub async fn fork_evm_env(&self, fork_url: &str) -> eyre::Result<(crate::Env, AnyRpcBlock)> { - let provider = self.fork_provider_with_url(fork_url)?; + let provider = self.fork_provider_with_url::(fork_url)?; self.fork_evm_env_with_provider(fork_url, &provider).await } @@ -257,7 +258,7 @@ impl EvmOpts { /// Returns the chain ID from the RPC, if any. pub async fn get_remote_chain_id(&self) -> Option { if let Some(url) = &self.fork_url - && let Ok(provider) = self.fork_provider_with_url(url) + && let Ok(provider) = self.fork_provider_with_url::(url) { trace!(?url, "retrieving chain via eth_chainId"); diff --git a/crates/evm/evm/src/executors/corpus.rs b/crates/evm/evm/src/executors/corpus.rs index b579db628361d..d0bc971d49f6e 100644 --- a/crates/evm/evm/src/executors/corpus.rs +++ b/crates/evm/evm/src/executors/corpus.rs @@ -210,6 +210,7 @@ pub(crate) struct CorpusMetrics { impl fmt::Display for CorpusMetrics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f)?; + writeln!(f, " Edge coverage metrics:")?; writeln!(f, " - cumulative edges seen: {}", self.cumulative_edges_seen)?; writeln!(f, " - cumulative features seen: {}", self.cumulative_features_seen)?; writeln!(f, " - corpus count: {}", self.corpus_count)?; diff --git a/crates/evm/evm/src/executors/invariant/error.rs b/crates/evm/evm/src/executors/invariant/error.rs index 9f48e9da82cbd..cb476a89f1530 100644 --- a/crates/evm/evm/src/executors/invariant/error.rs +++ b/crates/evm/evm/src/executors/invariant/error.rs @@ -1,10 +1,11 @@ use super::InvariantContract; use crate::executors::RawCallResult; +use alloy_json_abi::Function; use alloy_primitives::{Address, Bytes}; -use foundry_config::InvariantConfig; use foundry_evm_core::decode::RevertDecoder; use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts}; use proptest::test_runner::TestError; +use std::{collections::HashMap, fmt}; /// Stores information about failures and reverts of the invariant tests. #[derive(Clone, Default)] @@ -14,7 +15,7 @@ pub struct InvariantFailures { /// The latest revert reason of a run. pub revert_reason: Option, /// Maps a broken invariant to its specific error. - pub error: Option, + pub errors: HashMap, } impl InvariantFailures { @@ -22,8 +23,32 @@ impl InvariantFailures { Self::default() } - pub fn into_inner(self) -> (usize, Option) { - (self.reverts, self.error) + pub fn into_inner(self) -> (usize, HashMap) { + (self.reverts, self.errors) + } + + pub fn record_failure(&mut self, invariant: &Function, failure: InvariantFuzzError) { + self.errors.insert(invariant.name.clone(), failure); + } + + pub fn has_failure(&self, invariant: &Function) -> bool { + self.errors.contains_key(&invariant.name) + } + + pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> { + self.errors.get(&invariant.name) + } + + pub fn can_continue(&self, invariants: usize) -> bool { + self.errors.len() < invariants + } +} + +impl fmt::Display for InvariantFailures { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f)?; + writeln!(f, " ❌ Failures: {}", self.errors.len())?; + Ok(()) } } @@ -70,10 +95,11 @@ pub struct FailedInvariantCaseData { impl FailedInvariantCaseData { pub fn new( invariant_contract: &InvariantContract<'_>, - invariant_config: &InvariantConfig, + shrink_run_limit: u32, + fail_on_revert: bool, targeted_contracts: &FuzzRunIdentifiedContracts, calldata: &[BasicTxDetails], - call_result: RawCallResult, + call_result: &RawCallResult, inner_sequence: &[Option], ) -> Self { // Collect abis of fuzzed and invariant contracts to decode custom error. @@ -82,7 +108,7 @@ impl FailedInvariantCaseData { .with_abi(invariant_contract.abi) .decode(call_result.result.as_ref(), call_result.exit_reason); - let func = invariant_contract.invariant_function; + let func = invariant_contract.invariant_fn; debug_assert!(func.inputs.is_empty()); let origin = func.name.as_str(); Self { @@ -95,8 +121,8 @@ impl FailedInvariantCaseData { addr: invariant_contract.address, calldata: func.selector().to_vec().into(), inner_sequence: inner_sequence.to_vec(), - shrink_run_limit: invariant_config.shrink_run_limit, - fail_on_revert: invariant_config.fail_on_revert, + shrink_run_limit, + fail_on_revert, } } } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index d75fbed4986db..5b5e8455fbeb9 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -1,10 +1,11 @@ use crate::{ executors::{ DURATION_BETWEEN_METRICS_REPORT, EarlyExit, EvmError, Executor, FuzzTestTimer, - RawCallResult, corpus::{CorpusMetrics, WorkerCorpus}, + RawCallResult, corpus::WorkerCorpus, }, inspectors::Fuzzer, }; +use alloy_json_abi::Function; use alloy_primitives::{ Address, Bytes, FixedBytes, I256, Selector, U256, map::{AddressMap, HashMap}, @@ -35,7 +36,7 @@ use foundry_evm_traces::{CallTraceArena, SparsedTraceArena}; use indicatif::ProgressBar; use parking_lot::RwLock; use proptest::{strategy::Strategy, test_runner::TestRunner}; -use result::{assert_after_invariant, assert_invariants, can_continue}; +use result::{assert_after_invariant, can_continue, invariant_preflight_check}; use revm::state::Account; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -50,13 +51,13 @@ pub use error::{InvariantFailures, InvariantFuzzError}; use foundry_evm_coverage::HitMaps; mod replay; -pub use replay::{replay_error, replay_run}; +pub use replay::{generate_counterexample, replay_error, replay_run}; mod result; pub use result::InvariantFuzzTestResult; mod shrink; -pub use shrink::{check_sequence, check_sequence_value}; +pub use shrink::check_sequence; sol! { interface IInvariantTest { @@ -136,8 +137,6 @@ struct InvariantTestData { last_run_inputs: Vec, // Additional traces for gas report. gas_report_traces: Vec>, - // Last call results of the invariant test. - last_call_results: Option, // Line coverage information collected from all fuzzed calls. line_coverage: Option, // Metrics for each fuzzed selector. @@ -171,19 +170,13 @@ impl InvariantTest { fuzz_state: EvmFuzzState, targeted_contracts: FuzzRunIdentifiedContracts, failures: InvariantFailures, - last_call_results: Option, branch_runner: TestRunner, ) -> Self { - let mut fuzz_cases = vec![]; - if last_call_results.is_none() { - fuzz_cases.push(FuzzedCases::new(vec![])); - } let test_data = InvariantTestData { - fuzz_cases, + fuzz_cases: vec![], failures, last_run_inputs: vec![], gas_report_traces: vec![], - last_call_results, line_coverage: None, metrics: Map::default(), branch_runner, @@ -199,18 +192,13 @@ impl InvariantTest { } /// Whether invariant test has errors or not. - fn has_errors(&self) -> bool { - self.test_data.failures.error.is_some() + fn has_errors(&self, invariant: &Function) -> bool { + self.test_data.failures.has_failure(invariant) } /// Set invariant test error. - fn set_error(&mut self, error: InvariantFuzzError) { - self.test_data.failures.error = Some(error); - } - - /// Set last invariant test call results. - fn set_last_call_results(&mut self, call_result: Option) { - self.test_data.last_call_results = call_result; + fn set_error(&mut self, invariant: &Function, error: InvariantFuzzError) { + self.test_data.failures.record_failure(invariant, error); } /// Set last invariant run call sequence. @@ -241,30 +229,6 @@ impl InvariantTest { } } - /// Records a broken invariant for the invariant test function. - fn record_invariant_failure(&mut self, invariant_contract: &InvariantContract<'_>) { - let metric_key = format!( - "{}.{}", - invariant_contract.identifier, invariant_contract.invariant_function.name - ); - let test_metrics = &mut self.test_data.metrics; - let invariant_metrics = test_metrics.entry(metric_key).or_default(); - invariant_metrics.failed += 1; - } - - /// Returns number of broken invariants recorded for this invariant test. - fn invariant_failure_count(&self, invariant_contract: &InvariantContract<'_>) -> usize { - let metric_key = format!( - "{}.{}", - invariant_contract.identifier, invariant_contract.invariant_function.name - ); - self.test_data - .metrics - .get(&metric_key) - .map(|metrics| metrics.failed) - .unwrap_or(0) - } - /// End invariant test run by collecting results, cleaning collected artifacts and reverting /// created fuzz state. fn end_run(&mut self, run: InvariantTestRun, gas_samples: usize) { @@ -282,13 +246,18 @@ impl InvariantTest { self.fuzz_state.revert(); } - /// Updates the optimization state if the new value is better (higher) than the current best. - fn update_optimization_value(&mut self, value: I256, sequence: &[BasicTxDetails]) { - if self.test_data.optimization_best_value.is_none_or(|best| value > best) { - self.test_data.optimization_best_value = Some(value); - self.test_data.optimization_best_sequence = sequence.to_vec(); + /// Records failed invariants in metrics. + /// + /// Each invariant can fail at most once per campaign, so we increment by one for each entry in + /// the failures map. + fn record_failed_invariant_metrics(&mut self, invariant_contract: &InvariantContract<'_>) { + let failed_invariants: Vec<_> = self.test_data.failures.errors.keys().cloned().collect(); + for invariant_name in failed_invariants { + let metric_key = format!("{}.{}", invariant_contract.identifier, invariant_name); + self.test_data.metrics.entry(metric_key).or_default().failed += 1; } } + } /// Contains data for an invariant test run. @@ -381,7 +350,7 @@ impl<'a> InvariantExecutor<'a> { early_exit: &EarlyExit, ) -> Result { // Throw an error to abort test run if the invariant function accepts input params - if !invariant_contract.invariant_function.inputs.is_empty() { + if !invariant_contract.invariant_fn.inputs.is_empty() { return Err(eyre!("Invariant test function should have no inputs")); } @@ -394,30 +363,6 @@ impl<'a> InvariantExecutor<'a> { let mut last_metrics_report = Instant::now(); // Invariant runs with edge coverage if corpus dir is set or showing edge coverage. let edge_coverage_enabled = self.config.corpus.collect_edge_coverage(); - let invariant_name = invariant_contract.invariant_function.name.as_str(); - let mut emit_edge_metrics = |force: bool, failed: usize, metrics: &CorpusMetrics| -> Result<()> { - if !edge_coverage_enabled { - return Ok(()); - } - if progress.is_some() { - return Ok(()); - } - if !force && last_metrics_report.elapsed() <= DURATION_BETWEEN_METRICS_REPORT { - return Ok(()); - } - - let metrics = json!({ - "timestamp": SystemTime::now() - .duration_since(UNIX_EPOCH)? - .as_secs(), - "invariant": invariant_name, - "failed": failed, - "metrics": metrics, - }); - let _ = sh_println!("{}", serde_json::to_string(&metrics)?); - last_metrics_report = Instant::now(); - Ok(()) - }; let continue_campaign = |runs: u32| { if early_exit.should_stop() { return false; @@ -480,9 +425,10 @@ impl<'a> InvariantExecutor<'a> { current_run.inputs.pop(); current_run.rejects += 1; if current_run.rejects > self.config.max_assume_rejects { - invariant_test.set_error(InvariantFuzzError::MaxAssumeRejects( - self.config.max_assume_rejects, - )); + invariant_test.set_error( + invariant_contract.invariant_fn, + InvariantFuzzError::MaxAssumeRejects(self.config.max_assume_rejects), + ); break 'stop; } } else { @@ -496,7 +442,7 @@ impl<'a> InvariantExecutor<'a> { // inconsistencies whenever proptest tries to use the input case after test // execution. // See . - let mut state_changeset = call_result.state_changeset.clone(); + let mut state_changeset = std::mem::take(&mut call_result.state_changeset); if !call_result.reverted { collect_data( &invariant_test, @@ -525,80 +471,22 @@ impl<'a> InvariantExecutor<'a> { .push(FuzzCase { gas: call_result.gas_used, stipend: call_result.stipend }); // Determine if test can continue or should exit. - // Check invariants based on check_interval to improve deep run performance. - // - check_interval=0: only assert on the last call - // - check_interval=1 (default): assert after every call - // - check_interval=N: assert every N calls AND always on the last call - let is_last_call = current_run.depth == self.config.depth - 1; - let should_check_invariant = if self.config.check_interval == 0 { - is_last_call - } else { - self.config.check_interval == 1 - || (current_run.depth + 1).is_multiple_of(self.config.check_interval) - || is_last_call - }; - - let result = if should_check_invariant { - can_continue( - &invariant_contract, - &mut invariant_test, - &mut current_run, - &self.config, - call_result, - &state_changeset, - ) - .map_err(|e| eyre!(e.to_string()))? - } else { - // Skip invariant check but still track reverts - if call_result.reverted { - invariant_test.test_data.failures.reverts += 1; - if self.config.fail_on_revert { - let case_data = error::FailedInvariantCaseData::new( - &invariant_contract, - &self.config, - &invariant_test.targeted_contracts, - ¤t_run.inputs, - call_result, - &[], - ); - invariant_test.test_data.failures.revert_reason = - Some(case_data.revert_reason.clone()); - invariant_test.test_data.failures.error = - Some(InvariantFuzzError::Revert(case_data)); - result::RichInvariantResults::new(false, None) - } else if !invariant_contract.is_optimization() { - // In optimization mode, keep reverted calls to preserve - // warp/roll values for correct replay during shrinking. - current_run.inputs.pop(); - result::RichInvariantResults::new(true, None) - } else { - result::RichInvariantResults::new(true, None) - } - } else { - result::RichInvariantResults::new(true, None) - } - }; - - if !result.can_continue || current_run.depth == self.config.depth - 1 { + let can_continue = can_continue( + &invariant_contract, + &mut invariant_test, + &mut current_run, + &self.config, + call_result, + &state_changeset, + ) + .map_err(|e| eyre!(e.to_string()))?; + if !can_continue || current_run.depth == self.config.depth - 1 { invariant_test.set_last_run_inputs(¤t_run.inputs); } // If test cannot continue then stop current run and exit test suite. - if !result.can_continue { - if matches!( - invariant_test.test_data.failures.error, - Some(InvariantFuzzError::BrokenInvariant(_)) - ) { - if self.config.show_metrics { - invariant_test.record_invariant_failure(&invariant_contract); - } - let failed = - invariant_test.invariant_failure_count(&invariant_contract); - emit_edge_metrics(true, failed, &corpus_manager.metrics)?; - } + if !can_continue { break 'stop; } - - invariant_test.set_last_call_results(result.call_result); current_run.depth += 1; } @@ -614,26 +502,16 @@ impl<'a> InvariantExecutor<'a> { corpus_manager.process_inputs(¤t_run.inputs, current_run.new_coverage); // Call `afterInvariant` only if it is declared and test didn't fail already. - if invariant_contract.call_after_invariant && !invariant_test.has_errors() { - let after_invariant_success = assert_after_invariant( + if invariant_contract.call_after_invariant + && !invariant_test.has_errors(invariant_contract.invariant_fn) + { + assert_after_invariant( &invariant_contract, &mut invariant_test, ¤t_run, &self.config, ) .map_err(|_| eyre!("Failed to call afterInvariant"))?; - if !after_invariant_success - && matches!( - invariant_test.test_data.failures.error, - Some(InvariantFuzzError::BrokenInvariant(_)) - ) - { - if self.config.show_metrics { - invariant_test.record_invariant_failure(&invariant_contract); - } - let failed = invariant_test.invariant_failure_count(&invariant_contract); - emit_edge_metrics(true, failed, &corpus_manager.metrics)?; - } } // End current invariant test run. @@ -641,26 +519,45 @@ impl<'a> InvariantExecutor<'a> { if let Some(progress) = progress { // If running with progress then increment completed runs. progress.inc(1); - // Display metrics in progress bar. - if edge_coverage_enabled { - progress.set_message(format!("{}", &corpus_manager.metrics)); + + let failures = &invariant_test.test_data.failures; + let mut parts = Vec::new(); + // Add failures if present + if !failures.errors.is_empty() { + parts.push(format!("{failures}")); } - } else { - let failed = invariant_test.invariant_failure_count(&invariant_contract); - if emit_edge_metrics(false, failed, &corpus_manager.metrics).is_err() { - // ignore edge metrics errors to avoid breaking invariant loop + // Add edge coverage metrics if enabled + if edge_coverage_enabled { + parts.push(format!("{}", corpus_manager.metrics)); } + progress.set_message(parts.join("")); + } else if edge_coverage_enabled + && last_metrics_report.elapsed() > DURATION_BETWEEN_METRICS_REPORT + { + // Display metrics inline if corpus dir set. + let metrics = json!({ + "timestamp": SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs(), + "invariant": invariant_contract.invariant_fn.name, + "metrics": &corpus_manager.metrics, + }); + let _ = sh_println!("{}", serde_json::to_string(&metrics)?); + last_metrics_report = Instant::now(); } runs += 1; } trace!(?fuzz_fixtures); + if self.config.show_metrics { + invariant_test.record_failed_invariant_metrics(&invariant_contract); + } invariant_test.fuzz_state.log_stats(); let result = invariant_test.test_data; Ok(InvariantFuzzTestResult { - error: result.failures.error, + errors: result.failures.errors, cases: result.fuzz_cases, reverts: result.failures.reverts, last_run_inputs: result.last_run_inputs, @@ -720,7 +617,7 @@ impl<'a> InvariantExecutor<'a> { // already know if we can early exit the invariant run. // This does not count as a fuzz run. It will just register the revert. let mut failures = InvariantFailures::new(); - let last_call_results = assert_invariants( + invariant_preflight_check( invariant_contract, &self.config, &targeted_contracts, @@ -728,7 +625,7 @@ impl<'a> InvariantExecutor<'a> { &[], &mut failures, )?; - if let Some(error) = failures.error { + if let Some(error) = failures.get_failure(invariant_contract.invariant_fn) { return Err(eyre!(error.revert_reason().unwrap_or_default())); } @@ -769,14 +666,8 @@ impl<'a> InvariantExecutor<'a> { None, Some(&targeted_contracts), )?; - - let invariant_test = InvariantTest::new( - fuzz_state, - targeted_contracts, - failures, - last_call_results, - self.runner.clone(), - ); + let invariant_test = + InvariantTest::new(fuzz_state, targeted_contracts, failures, self.runner.clone()); Ok((invariant_test, worker)) } diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index fe507123f9fd7..e43f9ef639080 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -4,7 +4,7 @@ use crate::executors::{ invariant::shrink::{shrink_sequence, shrink_sequence_value}, }; use alloy_dyn_abi::JsonAbiExt; -use alloy_primitives::{I256, Log, map::HashMap}; +use alloy_primitives::{I256, Log, U256, map::HashMap}; use eyre::Result; use foundry_common::{ContractsByAddress, ContractsByArtifact}; use foundry_config::InvariantConfig; @@ -68,7 +68,7 @@ pub fn replay_run( let (invariant_result, invariant_success) = call_invariant_function( &executor, invariant_contract.address, - invariant_contract.invariant_function.abi_encode_input(&[])?.into(), + invariant_contract.invariant_fn.abi_encode_input(&[])?.into(), )?; traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap())); logs.extend(invariant_result.logs); @@ -90,10 +90,46 @@ pub fn replay_run( Ok(counterexample_sequence) } -/// Replays and shrinks a call sequence, collecting logs and traces. -/// -/// For check mode (target_value=None): shrinks to find shortest failing sequence. -/// For optimization mode (target_value=Some): shrinks to find shortest sequence producing target. +pub fn generate_counterexample( + mut executor: Executor, + known_contracts: &ContractsByArtifact, + mut ided_contracts: ContractsByAddress, + inputs: &[BasicTxDetails], + show_solidity: bool, +) -> Result> { + // We want traces for a failed case. + if executor.inspector().tracer.is_none() { + executor.set_tracing(TraceMode::Call); + } + + let mut counterexample_sequence = vec![]; + + // Replay each call from the sequence, collect logs, traces and coverage. + for tx in inputs { + let call_result = executor.transact_raw( + tx.sender, + tx.call_details.target, + tx.call_details.calldata.clone(), + U256::ZERO, + )?; + + // Identify newly generated contracts, if they exist. + ided_contracts + .extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts)); + + // Create counter example to be used in failed case. + counterexample_sequence.push(BaseCounterExample::from_invariant_call( + tx, + &ided_contracts, + call_result.traces, + show_solidity, + )); + } + + Ok(counterexample_sequence) +} + +/// Replays the error case, shrinks the failing sequence and collects all necessary traces. #[expect(clippy::too_many_arguments)] pub fn replay_error( config: InvariantConfig, diff --git a/crates/evm/evm/src/executors/invariant/result.rs b/crates/evm/evm/src/executors/invariant/result.rs index b6cd1879d29d0..7127cad542e4e 100644 --- a/crates/evm/evm/src/executors/invariant/result.rs +++ b/crates/evm/evm/src/executors/invariant/result.rs @@ -19,7 +19,8 @@ use std::{borrow::Cow, collections::HashMap}; /// The outcome of an invariant fuzz test #[derive(Debug)] pub struct InvariantFuzzTestResult { - pub error: Option, + /// Errors recorded per invariant. + pub errors: HashMap, /// Every successful fuzz test case pub cases: Vec, /// Number of reverted fuzz calls @@ -42,61 +43,93 @@ pub struct InvariantFuzzTestResult { pub optimization_best_sequence: Vec, } -/// Enriched results of an invariant run check. -/// -/// Contains the success condition and call results of the last run -pub(crate) struct RichInvariantResults { - pub(crate) can_continue: bool, - pub(crate) call_result: Option, -} - -impl RichInvariantResults { - pub(crate) fn new(can_continue: bool, call_result: Option) -> Self { - Self { can_continue, call_result } - } -} - /// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the /// external `invariant_failures.failed_invariant` map and returns a generic error. /// Either returns the call result if successful, or nothing if there was an error. -pub(crate) fn assert_invariants( +pub(crate) fn invariant_preflight_check( invariant_contract: &InvariantContract<'_>, invariant_config: &InvariantConfig, targeted_contracts: &FuzzRunIdentifiedContracts, executor: &Executor, calldata: &[BasicTxDetails], invariant_failures: &mut InvariantFailures, -) -> Result> { - let mut inner_sequence = vec![]; - - if let Some(fuzzer) = &executor.inspector().fuzzer - && let Some(call_generator) = &fuzzer.call_generator - { - inner_sequence.extend(call_generator.last_sequence.read().iter().cloned()); - } - +) -> Result<()> { let (call_result, success) = call_invariant_function( executor, invariant_contract.address, - invariant_contract.invariant_function.abi_encode_input(&[])?.into(), + invariant_contract.invariant_fn.abi_encode_input(&[])?.into(), )?; if !success { // We only care about invariants which we haven't broken yet. - if invariant_failures.error.is_none() { - let case_data = FailedInvariantCaseData::new( + invariant_failures.record_failure( + invariant_contract.invariant_fn, + InvariantFuzzError::BrokenInvariant(FailedInvariantCaseData::new( invariant_contract, - invariant_config, + invariant_config.shrink_run_limit, + invariant_config.fail_on_revert, targeted_contracts, calldata, - call_result, - &inner_sequence, + &call_result, + &invariant_inner_sequence(executor), + )), + ); + } + + Ok(()) +} + +/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the +/// external `invariant_failures.failed_invariant` map and returns a generic error. +/// Either returns the call result if successful, or nothing if there was an error. +pub(crate) fn assert_invariants( + invariant_contract: &InvariantContract<'_>, + invariant_config: &InvariantConfig, + targeted_contracts: &FuzzRunIdentifiedContracts, + executor: &Executor, + calldata: &[BasicTxDetails], + invariant_failures: &mut InvariantFailures, +) -> Result<()> { + let inner_sequence = invariant_inner_sequence(executor); + // We only care about invariants which we haven't broken yet. + for (invariant, fail_on_revert) in &invariant_contract.invariant_fns { + // We only care about invariants which we haven't broken yet. + if invariant_failures.has_failure(invariant) { + continue; + } + + let (call_result, success) = call_invariant_function( + executor, + invariant_contract.address, + invariant.abi_encode_input(&[])?.into(), + )?; + if !success { + invariant_failures.record_failure( + invariant, + InvariantFuzzError::BrokenInvariant(FailedInvariantCaseData::new( + invariant_contract, + invariant_config.shrink_run_limit, + *fail_on_revert, + targeted_contracts, + calldata, + &call_result, + &inner_sequence, + )), ); - invariant_failures.error = Some(InvariantFuzzError::BrokenInvariant(case_data)); - return Ok(None); } } - Ok(Some(call_result)) + Ok(()) +} + +/// Helper function to initialize invariant inner sequence. +fn invariant_inner_sequence(executor: &Executor) -> Vec> { + let mut seq = vec![]; + if let Some(fuzzer) = &executor.inspector().fuzzer + && let Some(call_generator) = &fuzzer.call_generator + { + seq.extend(call_generator.last_sequence.read().iter().cloned()); + } + seq } /// Returns if invariant test can continue and last successful call result of the invariant test @@ -111,10 +144,7 @@ pub(crate) fn can_continue( invariant_config: &InvariantConfig, call_result: RawCallResult, state_changeset: &StateChangeset, -) -> Result { - let mut call_results = None; - let is_optimization = invariant_contract.is_optimization(); - +) -> Result { let handlers_succeeded = || { invariant_test.targeted_contracts.targets.lock().keys().all(|address| { invariant_run.executor.is_success( @@ -126,65 +156,46 @@ pub(crate) fn can_continue( }) }; + let failures = &mut invariant_test.test_data.failures; + // Assert invariants if the call did not revert and the handlers did not fail. if !call_result.reverted && handlers_succeeded() { if let Some(traces) = call_result.traces { invariant_run.run_traces.push(traces); } - - if is_optimization { - // Optimization mode: call invariant and track max value, never fail. - let (inv_result, success) = call_invariant_function( - &invariant_run.executor, - invariant_contract.address, - invariant_contract.invariant_function.abi_encode_input(&[])?.into(), - )?; - if success - && inv_result.result.len() >= 32 - && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32]) - { - invariant_test.update_optimization_value(value, &invariant_run.inputs); - } - call_results = Some(inv_result); - } else { - // Check mode: assert invariants and fail if broken. - call_results = assert_invariants( - invariant_contract, - invariant_config, - &invariant_test.targeted_contracts, - &invariant_run.executor, - &invariant_run.inputs, - &mut invariant_test.test_data.failures, - )?; - if call_results.is_none() { - return Ok(RichInvariantResults::new(false, None)); - } - } + assert_invariants( + invariant_contract, + invariant_config, + &invariant_test.targeted_contracts, + &invariant_run.executor, + &invariant_run.inputs, + failures, + )?; } else { // Increase the amount of reverts. - let invariant_data = &mut invariant_test.test_data; - invariant_data.failures.reverts += 1; - // If fail on revert is set, we must return immediately. - if invariant_config.fail_on_revert { - let case_data = FailedInvariantCaseData::new( - invariant_contract, - invariant_config, - &invariant_test.targeted_contracts, - &invariant_run.inputs, - call_result, - &[], - ); - invariant_data.failures.revert_reason = Some(case_data.revert_reason.clone()); - invariant_data.failures.error = Some(InvariantFuzzError::Revert(case_data)); - - return Ok(RichInvariantResults::new(false, None)); - } else if call_result.reverted && !is_optimization { - // If we don't fail test on revert then remove last reverted call from inputs. - // In optimization mode, we keep reverted calls to preserve warp/roll values - // for correct replay during shrinking. - invariant_run.inputs.pop(); + failures.reverts += 1; + // If fail on revert is set, record invariant failure. + for (invariant, fail_on_revert) in &invariant_contract.invariant_fns { + if *fail_on_revert { + let case_data = FailedInvariantCaseData::new( + invariant_contract, + invariant_config.shrink_run_limit, + *fail_on_revert, + &invariant_test.targeted_contracts, + &invariant_run.inputs, + &call_result, + &[], + ); + failures + .errors + .insert(invariant.name.clone(), InvariantFuzzError::Revert(case_data)); + } } + // Remove last reverted call from inputs. + // This improves shrinking performance as irrelevant calls won't be checked again. + invariant_run.inputs.pop(); } - Ok(RichInvariantResults::new(true, call_results)) + // Stop execution if all invariants are broken. + Ok(failures.can_continue(invariant_contract.invariant_fns.len())) } /// Given the executor state, asserts conditions within `afterInvariant` function. @@ -201,13 +212,17 @@ pub(crate) fn assert_after_invariant( if !success { let case_data = FailedInvariantCaseData::new( invariant_contract, - invariant_config, + invariant_config.shrink_run_limit, + invariant_config.fail_on_revert, &invariant_test.targeted_contracts, &invariant_run.inputs, - call_result, + &call_result, &[], ); - invariant_test.set_error(InvariantFuzzError::BrokenInvariant(case_data)); + invariant_test.set_error( + invariant_contract.invariant_fn, + InvariantFuzzError::BrokenInvariant(case_data), + ); } Ok(success) } diff --git a/crates/evm/evm/src/executors/invariant/shrink.rs b/crates/evm/evm/src/executors/invariant/shrink.rs index 1e56a019cc576..ac324b1ecbfb0 100644 --- a/crates/evm/evm/src/executors/invariant/shrink.rs +++ b/crates/evm/evm/src/executors/invariant/shrink.rs @@ -89,7 +89,7 @@ pub(crate) fn shrink_sequence( reset_shrink_progress(config, progress); let target_address = invariant_contract.address; - let calldata: Bytes = invariant_contract.invariant_function.selector().to_vec().into(); + let calldata: Bytes = invariant_contract.invariant_fn.selector().to_vec().into(); // Special case test: the invariant is *unsatisfiable* - it took 0 calls to // break the invariant -- consider emitting a warning. let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?; diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index fa9d15b0e4d12..23bd0cd97ce67 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -91,11 +91,14 @@ sol! { #[derive(Clone, Debug)] pub struct Executor { /// The underlying `revm::Database` that contains the EVM storage. + /// + /// Wrapped in `Arc` for efficient cloning during parallel fuzzing. Use [`Arc::make_mut`] + /// for copy-on-write semantics when mutation is needed. // Note: We do not store an EVM here, since we are really // only interested in the database. REVM's `EVM` is a thin // wrapper around spawning a new EVM on every call anyway, // so the performance difference should be negligible. - backend: Backend, + backend: Arc, /// The EVM environment. env: Env, /// The Revm inspector stack. @@ -107,12 +110,6 @@ pub struct Executor { } impl Executor { - /// Creates a new `ExecutorBuilder`. - #[inline] - pub fn builder() -> ExecutorBuilder { - ExecutorBuilder::new() - } - /// Creates a new `Executor` with the given arguments. #[inline] pub fn new( @@ -135,7 +132,7 @@ impl Executor { }, ); - Self { backend, env, inspector, gas_limit, legacy_assertions } + Self { backend: Arc::new(backend), env, inspector, gas_limit, legacy_assertions } } fn clone_with_backend(&self, backend: Backend) -> Self { @@ -145,7 +142,13 @@ impl Executor { self.env.tx.clone(), self.spec_id(), ); - Self::new(backend, env, self.inspector().clone(), self.gas_limit, self.legacy_assertions) + Self { + backend: Arc::new(backend), + env, + inspector: self.inspector().clone(), + gas_limit: self.gas_limit, + legacy_assertions: self.legacy_assertions, + } } /// Returns a reference to the EVM backend. @@ -154,8 +157,11 @@ impl Executor { } /// Returns a mutable reference to the EVM backend. + /// + /// Uses copy-on-write semantics: if other clones of this executor share the backend, + /// this will clone the backend first. pub fn backend_mut(&mut self) -> &mut Backend { - &mut self.backend + Arc::make_mut(&mut self.backend) } /// Returns a reference to the EVM environment. diff --git a/crates/evm/evm/src/executors/trace.rs b/crates/evm/evm/src/executors/trace.rs index d951dde0d6663..d6eaf5ac29d6e 100644 --- a/crates/evm/evm/src/executors/trace.rs +++ b/crates/evm/evm/src/executors/trace.rs @@ -29,8 +29,8 @@ impl TracingExecutor { state_overrides: Option, ) -> eyre::Result { let db = Backend::spawn(Some(fork))?; - // configures a bare version of the evm executor: no cheatcode inspector is enabled, - // tracing will be enabled only for the targeted transaction + // configures a bare version of the evm executor: no cheatcode and log_collector inspector + // is enabled, tracing will be enabled only for the targeted transaction let mut executor = ExecutorBuilder::new() .inspectors(|stack| { stack.trace_mode(trace_mode).networks(networks).create2_deployer(create2_deployer) diff --git a/crates/evm/evm/src/inspectors/logs.rs b/crates/evm/evm/src/inspectors/logs.rs index 4dd550caaaf0f..84c05f69db1c2 100644 --- a/crates/evm/evm/src/inspectors/logs.rs +++ b/crates/evm/evm/src/inspectors/logs.rs @@ -1,7 +1,9 @@ use alloy_primitives::Log; use alloy_sol_types::{SolEvent, SolInterface, SolValue}; -use foundry_common::{ErrorExt, fmt::ConsoleFmt}; -use foundry_evm_core::{InspectorExt, abi::console, constants::HARDHAT_CONSOLE_ADDRESS}; +use foundry_common::{ErrorExt, fmt::ConsoleFmt, sh_println}; +use foundry_evm_core::{ + InspectorExt, abi::console, constants::HARDHAT_CONSOLE_ADDRESS, decode::decode_console_log, +}; use revm::{ Inspector, context::ContextTr, @@ -14,13 +16,22 @@ use revm::{ /// An inspector that collects logs during execution. /// /// The inspector collects logs from the `LOG` opcodes as well as Hardhat-style `console.sol` logs. -#[derive(Clone, Debug, Default)] -pub struct LogCollector { +#[derive(Clone, Debug)] +pub enum LogCollector { /// The collected logs. Includes both `LOG` opcodes and Hardhat-style `console.sol` logs. - pub logs: Vec, + Capture { logs: Vec }, + /// Print logs directly to stdout. + LiveLogs, } impl LogCollector { + pub fn into_captured_logs(self) -> Option> { + match self { + Self::Capture { logs } => Some(logs), + Self::LiveLogs => None, + } + } + #[cold] fn do_hardhat_log(&mut self, context: &mut CTX, inputs: &CallInputs) -> Option where @@ -41,9 +52,32 @@ impl LogCollector { fn hardhat_log(&mut self, data: &[u8]) -> alloy_sol_types::Result<()> { let decoded = console::hh::ConsoleCalls::abi_decode(data)?; - self.logs.push(hh_to_ds(&decoded)); + self.push_msg(&decoded.fmt(Default::default())); Ok(()) } + + fn push_raw_log(&mut self, log: Log) { + match self { + Self::Capture { logs } => logs.push(log), + Self::LiveLogs => { + if let Some(msg) = decode_console_log(&log) { + sh_println!("{msg}").expect("fail printing to stdout"); + } else { + // This case should not happen if the users call through forge-std. + // We print the log data for the user nonetheless. + sh_println!("console.log({:?}, {})", log.data.topics(), log.data.data) + .expect("fail printing to stdout"); + } + } + } + } + + fn push_msg(&mut self, msg: &str) { + match self { + Self::Capture { logs } => logs.push(new_console_log(msg)), + Self::LiveLogs => sh_println!("{msg}").expect("fail printing to stdout"), + } + } } impl Inspector for LogCollector @@ -51,7 +85,7 @@ where CTX: ContextTr, { fn log(&mut self, _context: &mut CTX, log: Log) { - self.logs.push(log); + self.push_raw_log(log); } fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { @@ -64,17 +98,10 @@ where impl InspectorExt for LogCollector { fn console_log(&mut self, msg: &str) { - self.logs.push(new_console_log(msg)); + self.push_msg(msg); } } -/// Converts a Hardhat `console.log` call to a DSTest `log(string)` event. -fn hh_to_ds(call: &console::hh::ConsoleCalls) -> Log { - // Convert the parameters of the call to their string representation using `ConsoleFmt`. - let msg = call.fmt(Default::default()); - new_console_log(&msg) -} - /// Creates a `console.log(string)` event. fn new_console_log(msg: &str) -> Log { Log::new_unchecked( diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index 0195d39d91f3e..77404b7c1dc6a 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -59,6 +59,9 @@ pub struct InspectorStackBuilder { /// Whether to enable tracing and revert diagnostics. pub trace_mode: TraceMode, /// Whether logs should be collected. + /// - None for no log collection. + /// - Some(true) for realtime console.log-ing. + /// - Some(false) for log collection. pub logs: Option, /// Whether line coverage info should be collected. pub line_coverage: Option, @@ -134,10 +137,10 @@ impl InspectorStackBuilder { self } - /// Set whether to collect logs. + /// Set the log collector, and whether to print the logs directly to stdout. #[inline] - pub fn logs(mut self, yes: bool) -> Self { - self.logs = Some(yes); + pub fn logs(mut self, live_logs: bool) -> Self { + self.logs = Some(live_logs); self } @@ -228,7 +231,7 @@ impl InspectorStackBuilder { stack.set_chisel(chisel_state); } stack.collect_line_coverage(line_coverage.unwrap_or(false)); - stack.collect_logs(logs.unwrap_or(true)); + stack.collect_logs(logs); stack.print(print.unwrap_or(false)); stack.tracing(trace_mode); @@ -479,9 +482,18 @@ impl InspectorStack { } /// Set whether to enable the log collector. + /// - None for no log collection. + /// - Some(true) for realtime console.log-ing. + /// - Some(false) for log collection. #[inline] - pub fn collect_logs(&mut self, yes: bool) { - self.log_collector = yes.then(Default::default); + pub fn collect_logs(&mut self, live_logs: Option) { + self.log_collector = live_logs.map(|live_logs| { + Box::new(if live_logs { + LogCollector::LiveLogs + } else { + LogCollector::Capture { logs: Vec::new() } + }) + }); } /// Set whether to enable the trace printer. @@ -556,7 +568,7 @@ impl InspectorStack { }); InspectorData { - logs: log_collector.map(|logs| logs.logs).unwrap_or_default(), + logs: log_collector.and_then(|logs| logs.into_captured_logs()).unwrap_or_default(), labels: cheatcodes .as_ref() .map(|cheatcodes| cheatcodes.labels.clone()) diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index 1632b870d2bf9..06b77e0508efd 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -261,12 +261,16 @@ impl TargetedContract { /// Test contract which is testing its invariants. #[derive(Clone, Debug)] pub struct InvariantContract<'a> { + /// Identifier for the invariant contract. + pub identifier: String, /// Address of the test contract. pub address: Address, - /// Identifier of the test contract. - pub identifier: String, /// Invariant function present in the test contract. pub invariant_function: &'a Function, + /// Invariant function. + pub invariant_fn: &'a Function, + /// All invariant functions present in the test contract and their fail on revert config. + pub invariant_fns: Vec<(&'a Function, bool)>, /// If true, `afterInvariant` function is called after each invariant run. pub call_after_invariant: bool, /// ABI of the test contract. @@ -283,9 +287,11 @@ impl<'a> InvariantContract<'a> { abi: &'a JsonAbi, ) -> Self { Self { - address, identifier: identifier.into(), + address, invariant_function, + invariant_fn: invariant_function, + invariant_fns: vec![(invariant_function, false)], call_after_invariant, abi, } @@ -293,6 +299,6 @@ impl<'a> InvariantContract<'a> { /// Returns true if this is an optimization mode invariant (returns int256). pub fn is_optimization(&self) -> bool { - is_optimization_invariant(self.invariant_function) + is_optimization_invariant(self.invariant_fn) } } diff --git a/crates/evm/traces/src/identifier/external.rs b/crates/evm/traces/src/identifier/external.rs index b489882a13d39..6702287200680 100644 --- a/crates/evm/traces/src/identifier/external.rs +++ b/crates/evm/traces/src/identifier/external.rs @@ -2,7 +2,7 @@ use super::{IdentifiedAddress, TraceIdentifier}; use crate::debug::ContractSources; use alloy_primitives::{ Address, - map::{Entry, HashMap}, + map::{Entry, HashMap, HashSet}, }; use eyre::WrapErr; use foundry_block_explorers::{contract::Metadata, errors::EtherscanError}; @@ -61,7 +61,14 @@ impl ExternalIdentifier { } if let Some(config) = config { debug!(target: "evm::traces::external", chain=?config.chain, url=?config.api_url, "using etherscan identifier"); - fetchers.push(Arc::new(EtherscanFetcher::new(config.into_client()?))); + match config.into_client() { + Ok(client) => { + fetchers.push(Arc::new(EtherscanFetcher::new(client))); + } + Err(err) => { + warn!(target: "evm::traces::external", ?err, "failed to create etherscan client"); + } + } } if fetchers.is_empty() { debug!(target: "evm::traces::external", "no fetchers enabled"); @@ -151,7 +158,7 @@ impl TraceIdentifier for ExternalIdentifier { trace!(target: "evm::traces::external", "identify {} addresses", nodes.len()); let mut identities = Vec::new(); - let mut to_fetch = Vec::new(); + let mut to_fetch = HashSet::new(); // Check cache first. for &node in nodes { @@ -163,7 +170,7 @@ impl TraceIdentifier for ExternalIdentifier { // Do nothing. We know that this contract was not verified. } } else { - to_fetch.push(address); + to_fetch.insert(address); } } @@ -172,6 +179,7 @@ impl TraceIdentifier for ExternalIdentifier { } trace!(target: "evm::traces::external", "fetching {} addresses", to_fetch.len()); + let to_fetch = to_fetch.into_iter().collect::>(); let fetchers = self.fetchers.iter().map(|fetcher| ExternalFetcher::new(fetcher.clone(), &to_fetch)); let fetched_identities = foundry_common::block_on( @@ -408,8 +416,6 @@ impl ExternalFetcherT for SourcifyFetcher { let url = format!("{url}/{address}?fields=abi,compilation", url = self.url); let response = self.client.get(url).send().await?; let code = response.status(); - let response: SourcifyResponse = response.json().await?; - trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}"); match code.as_u16() { // Not verified. 404 => return Err(EtherscanError::ContractCodeNotVerified(address)), @@ -417,6 +423,8 @@ impl ExternalFetcherT for SourcifyFetcher { 429 => return Err(EtherscanError::RateLimitExceeded), _ => {} } + let response: SourcifyResponse = response.json().await?; + trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}"); match response { SourcifyResponse::Success(metadata) => Ok(Some(metadata.into())), SourcifyResponse::Error(error) => Err(EtherscanError::Unknown(format!("{error:#?}"))), diff --git a/crates/fmt/src/state/sol.rs b/crates/fmt/src/state/sol.rs index fd0e5a9e54c0a..540949f51c071 100644 --- a/crates/fmt/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -407,7 +407,9 @@ impl<'ast> State<'_, 'ast> { self.block_depth -= 1; } else { if self.print_comments(span.hi(), CommentConfig::skip_ws()).is_some() { - self.zerobreak(); + // Adjust the offset of the trailing break from comment printing + // so the closing brace is not indented + self.s.offset(-self.ind); } else if self.config.bracket_spacing { self.nbsp(); }; diff --git a/crates/fmt/testdata/CommentEmptyLine/fmt.sol b/crates/fmt/testdata/CommentEmptyLine/fmt.sol new file mode 100644 index 0000000000000..a2407017f4be9 --- /dev/null +++ b/crates/fmt/testdata/CommentEmptyLine/fmt.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.0; + +contract ProofOfConcept { + // some comment +} diff --git a/crates/fmt/testdata/CommentEmptyLine/original.sol b/crates/fmt/testdata/CommentEmptyLine/original.sol new file mode 100644 index 0000000000000..9c7f35770e9f3 --- /dev/null +++ b/crates/fmt/testdata/CommentEmptyLine/original.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.8.0; + +contract ProofOfConcept { + // some comment + +} diff --git a/crates/fmt/tests/formatter.rs b/crates/fmt/tests/formatter.rs index b0d4ed3bdcd7f..f337b5b3657fa 100644 --- a/crates/fmt/tests/formatter.rs +++ b/crates/fmt/tests/formatter.rs @@ -169,6 +169,7 @@ fmt_tests! { ArrayExpressions, BlockComments, BlockCommentsFunction, + CommentEmptyLine, ConditionalOperatorExpression, ConstructorDefinition, ConstructorModifierStyle, @@ -224,3 +225,28 @@ fmt_tests! { Yul, YulStrings, } + +#[test] +fn test_comment_empty_line_bug() { + init_tracing(); + let source = r#"pragma solidity ^0.8.0; + +contract ProofOfConcept { + // some comment + +} +"#; + + let expected = r#"pragma solidity ^0.8.0; + +contract ProofOfConcept { + // some comment +} +"#; + + let fmt_config = Arc::new(FormatterConfig::default()); + let path = Path::new("test.sol"); + let formatted = format(source, path, fmt_config); + + assert_eq!(formatted, expected, "Formatting mismatch"); +} diff --git a/crates/forge/assets/solidity/workflowTemplate.yml b/crates/forge/assets/solidity/workflowTemplate.yml index 03824c4abd03c..8aeae90e03e63 100644 --- a/crates/forge/assets/solidity/workflowTemplate.yml +++ b/crates/forge/assets/solidity/workflowTemplate.yml @@ -7,9 +7,6 @@ on: pull_request: workflow_dispatch: -env: - FOUNDRY_PROFILE: ci - jobs: check: name: Foundry project diff --git a/crates/forge/assets/tempo/workflowTemplate.yml b/crates/forge/assets/tempo/workflowTemplate.yml index e1e368ed6406a..f2294304b402a 100644 --- a/crates/forge/assets/tempo/workflowTemplate.yml +++ b/crates/forge/assets/tempo/workflowTemplate.yml @@ -7,9 +7,6 @@ on: pull_request: workflow_dispatch: -env: - FOUNDRY_PROFILE: ci - jobs: check: name: Tempo Foundry project diff --git a/crates/forge/assets/vyper/workflowTemplate.yml b/crates/forge/assets/vyper/workflowTemplate.yml index a189658705d85..7a5fc54a1b084 100644 --- a/crates/forge/assets/vyper/workflowTemplate.yml +++ b/crates/forge/assets/vyper/workflowTemplate.yml @@ -7,9 +7,6 @@ on: pull_request: workflow_dispatch: -env: - FOUNDRY_PROFILE: ci - jobs: check: name: Foundry project diff --git a/crates/forge/src/cmd/clone.rs b/crates/forge/src/cmd/clone.rs index 97337907ac201..c8f5680f16793 100644 --- a/crates/forge/src/cmd/clone.rs +++ b/crates/forge/src/cmd/clone.rs @@ -1090,7 +1090,8 @@ mod tests { /// Run the clone command with the specified contract address and assert the compilation. async fn one_test_case(address: Address, check_compilation_result: bool) { - let mut project_root = tempfile::tempdir().unwrap().path().to_path_buf(); + let temp_dir = tempfile::tempdir().unwrap(); + let mut project_root = temp_dir.path().to_path_buf(); let client = mock_etherscan(address); let meta = CloneArgs::collect_metadata_from_client(address, &client).await.unwrap(); CloneArgs::init_an_empty_project(&project_root, DependencyInstallOpts::default()) @@ -1115,7 +1116,6 @@ mod tests { pick_creation_info(&address.to_string()).expect("creation code not found"); assert_compilation_result(rv, contract_name, stripped_creation_code); } - std::fs::remove_dir_all(project_root).unwrap(); } #[tokio::test(flavor = "multi_thread")] diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 0187020721af1..eb7e855e501c2 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -455,6 +455,14 @@ impl CreateArgs { constructor: &Constructor, constructor_args: &[String], ) -> Result> { + if constructor.inputs.len() != constructor_args.len() { + eyre::bail!( + "Constructor argument count mismatch: expected {} but got {}", + constructor.inputs.len(), + constructor_args.len() + ); + } + let mut params = Vec::with_capacity(constructor.inputs.len()); for (input, arg) in constructor.inputs.iter().zip(constructor_args) { // resolve the input type directly diff --git a/crates/forge/src/cmd/eip712.rs b/crates/forge/src/cmd/eip712.rs index 00a872a1df7a4..5d6b1bace69ca 100644 --- a/crates/forge/src/cmd/eip712.rs +++ b/crates/forge/src/cmd/eip712.rs @@ -84,7 +84,7 @@ impl Eip712Args { if compiler.sess().dcx.has_errors().is_err() { eyre::bail!("{diags}"); } else { - let _ = sh_print!("{diags}"); + let _ = sh_eprint!("{diags}"); } Ok(()) diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a026a80b5231b..158d0f07e2938 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -755,7 +755,7 @@ impl TestArgs { |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 false; + return found; } let previous_snapshots: BTreeMap = diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index f7e36dc323427..8cd5aa65fe25e 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -377,6 +377,7 @@ impl TestRunnerConfig { ExecutorBuilder::new() .inspectors(|stack| { stack + .logs(self.config.live_logs) .cheatcodes(cheats_config) .trace_mode(self.trace_mode()) .line_coverage(self.line_coverage) diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 693bea6f1eeb7..e30053f7e8aa7 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -415,6 +415,9 @@ pub struct TestResult { /// still be successful (i.e self.success == true) when it's expected to fail. pub reason: Option, + /// This field will be populated if there are additional invariant broken besides the main one. + pub other_failures: Vec, + /// Minimal reproduction test case for failing test pub counterexample: Option, @@ -521,6 +524,12 @@ impl fmt::Display for TestResult { } else { s.push(']'); } + if !self.other_failures.is_empty() { + writeln!(s).unwrap(); + for failure in &self.other_failures { + writeln!(s, "{failure}").unwrap(); + } + } s.red().wrap().fmt(f) } } @@ -721,6 +730,7 @@ impl TestResult { gas_report_traces: Vec>, success: bool, reason: Option, + other_failures: Vec, counterexample: Option, cases: Vec, reverts: usize, @@ -743,6 +753,7 @@ impl TestResult { TestStatus::Failure }; self.reason = reason; + self.other_failures = other_failures; self.counterexample = counterexample; self.gas_report_traces = gas_report_traces; } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 6f08abe86ac02..fd922ee00bf0c 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -22,7 +22,8 @@ use foundry_evm::{ CallResult, EvmError, Executor, ITest, RawCallResult, fuzz::FuzzedExecutor, invariant::{ - InvariantExecutor, InvariantFuzzError, check_sequence, replay_error, replay_run, + InvariantExecutor, InvariantFuzzError, check_sequence, generate_counterexample, + replay_error, replay_run, }, }, fuzz::{ @@ -340,7 +341,9 @@ impl<'a> ContractRunner<'a> { // Invariant testing requires tracing to figure out what contracts were created. // We also want to disable `debug` for setup since we won't be using those traces. - let has_invariants = self.contract.abi.functions().any(|func| func.is_invariant_test()); + let invariant_fns: Vec<_> = + self.contract.abi.functions().filter(|func| func.is_invariant_test()).collect(); + let has_invariants = !invariant_fns.is_empty(); let prev_tracer = self.executor.inspector_mut().tracer.take(); if prev_tracer.is_some() || has_invariants { @@ -437,6 +440,7 @@ impl<'a> ContractRunner<'a> { let mut res = FunctionRunner::new(&self, &setup).run( func, + invariant_fns.clone(), kind, call_after_invariant, identified_contracts.as_ref(), @@ -514,10 +518,22 @@ impl<'a> FunctionRunner<'a> { fn run( mut self, func: &Function, + invariants: Vec<&Function>, kind: TestFunctionKind, call_after_invariant: bool, identified_contracts: Option<&ContractsByAddress>, ) -> TestResult { + let fail_on_revert_for = |f: &Function| { + if self.inline_config.contains_function(self.cr.name, &f.name) + && let Ok(config) = self.cr.inline_config(Some(f)) + { + return config.invariant.fail_on_revert; + } + self.config.invariant.fail_on_revert + }; + let invariant_fns: Vec<_> = + invariants.into_iter().map(|f| (f, fail_on_revert_for(f))).collect(); + if let Err(e) = self.apply_function_inline_config(func) { self.result.single_fail(Some(e.to_string())); return self.result; @@ -531,6 +547,7 @@ impl<'a> FunctionRunner<'a> { let test_bytecode = &self.cr.contract.bytecode; self.run_invariant_test( func, + invariant_fns, call_after_invariant, identified_contracts.unwrap(), test_bytecode, @@ -714,6 +731,7 @@ impl<'a> FunctionRunner<'a> { fn run_invariant_test( mut self, func: &Function, + invariants: Vec<(&Function, bool)>, call_after_invariant: bool, identified_contracts: &ContractsByAddress, test_bytecode: &Bytes, @@ -755,13 +773,24 @@ impl<'a> FunctionRunner<'a> { identified_contracts, &self.cr.mcr.known_contracts, ); - let invariant_contract = InvariantContract::new( - self.cr.name, - self.address, - func, + + // Filter out additional invariants to test if we already have a persisted failure. + let invariant_contract = InvariantContract { + identifier: self.cr.name.to_string(), + address: self.address, + invariant_function: func, + invariant_fn: func, + invariant_fns: invariants + .into_iter() + .filter(|(invariant_fn, _)| { + *invariant_fn == func + || (invariant_config.continuous_run + && !canonicalized(failure_dir.join(invariant_fn.name.clone())).exists()) + }) + .collect(), call_after_invariant, - &self.cr.contract.abi, - ); + abi: &self.cr.contract.abi, + }; let show_solidity = invariant_config.show_solidity; let progress = start_fuzz_progress( @@ -797,7 +826,7 @@ impl<'a> FunctionRunner<'a> { &txes, (0..min(txes.len(), invariant_config.depth as usize)).collect(), invariant_contract.address, - invariant_contract.invariant_function.selector().to_vec().into(), + invariant_contract.invariant_fn.selector().to_vec().into(), invariant_config.fail_on_revert, invariant_contract.call_after_invariant, ) && !success @@ -869,7 +898,7 @@ impl<'a> FunctionRunner<'a> { self.result.invariant_replay_fail( replayed_entirely, - &invariant_contract.invariant_function.name, + &invariant_contract.invariant_fn.name, call_sequence, ); return self.result; @@ -893,117 +922,119 @@ impl<'a> FunctionRunner<'a> { self.result.merge_coverages(invariant_result.line_coverage); let mut counterexample = None; - let success = invariant_result.error.is_none(); - let reason = invariant_result.error.as_ref().and_then(|err| err.revert_reason()); - - match invariant_result.error { - // If invariants were broken, replay the error to collect logs and traces - Some(error) => match error { - InvariantFuzzError::BrokenInvariant(case_data) - | InvariantFuzzError::Revert(case_data) => { - // Replay error to create counterexample and to collect logs, traces and - // coverage. - match case_data.test_error { - TestError::Abort(_) => {} - TestError::Fail(_, ref calls) => { - match replay_error( - evm.config(), - self.clone_executor(), - calls, - Some(case_data.inner_sequence), - None, // check mode - &invariant_contract, - &self.cr.mcr.known_contracts, - identified_contracts.clone(), - &mut self.result.logs, - &mut self.result.traces, - &mut self.result.line_coverage, - &mut self.result.deprecated_cheatcodes, - progress.as_ref(), - &self.tcfg.early_exit, - ) { - Ok(call_sequence) => { - if !call_sequence.is_empty() { - // Persist error in invariant failure dir. - record_invariant_failure( - failure_dir.as_path(), - failure_file.as_path(), - &call_sequence, - test_bytecode, - ); - - let original_seq_len = if let TestError::Fail(_, calls) = - &case_data.test_error - { - calls.len() - } else { - call_sequence.len() - }; - - counterexample = Some(CounterExample::Sequence( - original_seq_len, - call_sequence, - )) - } - } - Err(err) => { - error!(%err, "Failed to replay invariant error"); - } - } + let success = invariant_result.errors.is_empty(); + let reason = invariant_result + .errors + .get(&invariant_contract.invariant_fn.name) + .and_then(|err| err.revert_reason()); + let mut other_failures = vec![]; + + if success { + // If invariants ran successfully, replay the last run to collect logs and + // traces. + if let Err(err) = replay_run( + &invariant_contract, + self.clone_executor(), + &self.cr.mcr.known_contracts, + identified_contracts.clone(), + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.line_coverage, + &mut self.result.deprecated_cheatcodes, + &invariant_result.last_run_inputs, + show_solidity, + ) { + error!(%err, "Failed to replay last invariant run"); + } + } else { + // check if main invariant was broken and replay error + if let Some(error) = invariant_result.errors.get(&invariant_contract.invariant_fn.name) + && let InvariantFuzzError::BrokenInvariant(case_data) + | InvariantFuzzError::Revert(case_data) = error + && let TestError::Fail(_, ref calls) = case_data.test_error + { + match replay_error( + evm.config(), + self.clone_executor(), + calls, + Some(case_data.inner_sequence.clone()), + None, + &invariant_contract, + &self.cr.mcr.known_contracts, + identified_contracts.clone(), + &mut self.result.logs, + &mut self.result.traces, + &mut self.result.line_coverage, + &mut self.result.deprecated_cheatcodes, + progress.as_ref(), + &self.tcfg.early_exit, + ) { + Ok(call_sequence) => { + if !call_sequence.is_empty() { + // Persist error in invariant failure dir. + record_invariant_failure( + failure_dir.as_path(), + failure_file.as_path(), + &call_sequence, + test_bytecode, + ); + + let original_seq_len = + if let TestError::Fail(_, calls) = &case_data.test_error { + calls.len() + } else { + call_sequence.len() + }; + + counterexample = + Some(CounterExample::Sequence(original_seq_len, call_sequence)) } - }; + } + Err(err) => { + error!(%err, "Failed to replay invariant error"); + } + } + } + + for (invariant, _) in invariant_contract.invariant_fns { + if invariant == invariant_contract.invariant_fn { + continue; } - InvariantFuzzError::MaxAssumeRejects(_) => {} - }, - // If invariants ran successfully, replay the last run to collect logs and traces. - _ => { - if let Some(best_value) = invariant_result.optimization_best_value { - // Optimization mode: replay and shrink to find shortest best sequence. - match replay_error( - evm.config(), + // Generate counterexamples for broken invariant, if there is no failure persisted + // already. + let persisted_failure = canonicalized(failure_dir.join(invariant.name.clone())); + if !persisted_failure.exists() + && let Some(error) = invariant_result.errors.get(&invariant.name) + && let InvariantFuzzError::BrokenInvariant(case_data) + | InvariantFuzzError::Revert(case_data) = error + && let TestError::Fail(_, ref calls) = case_data.test_error + { + other_failures.push(format!( + "{}: {}", + invariant.name, + error.revert_reason().unwrap_or_default() + )); + match generate_counterexample( self.clone_executor(), - &invariant_result.optimization_best_sequence, - None, - Some(best_value), - &invariant_contract, &self.cr.mcr.known_contracts, identified_contracts.clone(), - &mut self.result.logs, - &mut self.result.traces, - &mut self.result.line_coverage, - &mut self.result.deprecated_cheatcodes, - progress.as_ref(), - &self.tcfg.early_exit, + calls, + show_solidity, ) { - Ok(best_sequence) => { - if !best_sequence.is_empty() { - counterexample = Some(CounterExample::Sequence( - invariant_result.optimization_best_sequence.len(), - best_sequence, - )); - } + Ok(call_sequence) => { + // Persist error in invariant failure dir. + record_invariant_failure( + failure_dir.as_path(), + persisted_failure.as_path(), + &call_sequence, + test_bytecode, + ); } Err(err) => { - error!(%err, "Failed to replay optimization best sequence"); + error!(%err, "Failed to generate and record invariant counterexample"); } } - } else { - // Standard check mode: replay last run for traces. - if let Err(err) = replay_run( - &invariant_contract, - self.clone_executor(), - &self.cr.mcr.known_contracts, - identified_contracts.clone(), - &mut self.result.logs, - &mut self.result.traces, - &mut self.result.line_coverage, - &mut self.result.deprecated_cheatcodes, - &invariant_result.last_run_inputs, - show_solidity, - ) { - error!(%err, "Failed to replay last invariant run"); - } } } } @@ -1012,6 +1043,7 @@ impl<'a> FunctionRunner<'a> { invariant_result.gas_report_traces, success, reason, + other_failures, counterexample, invariant_result.cases, invariant_result.reverts, diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 0ab26ee567deb..30f0f43001b60 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -696,7 +696,7 @@ Initializing [..] from https://github.com/foundry-rs/forge-template... }); // checks that clone works -forgetest!(can_clone, |prj, cmd| { +forgetest!(flaky_can_clone, |prj, cmd| { prj.wipe(); let foundry_toml = prj.root().join(Config::FILE_NAME); @@ -728,7 +728,7 @@ Compiler run successful! }); // Checks that quiet mode does not print anything for clone -forgetest!(can_clone_quiet, |prj, cmd| { +forgetest!(flaky_can_clone_quiet, |prj, cmd| { prj.wipe(); cmd.args([ @@ -743,7 +743,7 @@ forgetest!(can_clone_quiet, |prj, cmd| { }); // checks that clone works with sourcify -forgetest!(can_clone_sourcify, |prj, cmd| { +forgetest!(flaky_can_clone_sourcify, |prj, cmd| { prj.wipe(); let foundry_toml = prj.root().join(Config::FILE_NAME); @@ -770,7 +770,7 @@ Compiler run successful! }); // checks that clone works with --no-remappings-txt -forgetest!(can_clone_no_remappings_txt, |prj, cmd| { +forgetest!(flaky_can_clone_no_remappings_txt, |prj, cmd| { prj.wipe(); let foundry_toml = prj.root().join(Config::FILE_NAME); @@ -803,7 +803,7 @@ Compiler run successful! }); // checks that clone works with --keep-directory-structure -forgetest!(can_clone_keep_directory_structure, |prj, cmd| { +forgetest!(flaky_can_clone_keep_directory_structure, |prj, cmd| { prj.wipe(); let foundry_toml = prj.root().join(Config::FILE_NAME); @@ -935,7 +935,7 @@ Installing tempo-std in [..] (url: https://github.com/tempoxyz/tempo-std, tag: N // checks that clone works with raw src containing `node_modules` // -forgetest!(can_clone_with_node_modules, |prj, cmd| { +forgetest!(flaky_can_clone_with_node_modules, |prj, cmd| { prj.wipe(); let foundry_toml = prj.root().join(Config::FILE_NAME); diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index d8e2e9d9785cf..7769c2f2e06fa 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -60,6 +60,7 @@ optimizer_runs = 200 verbosity = 0 eth_rpc_accept_invalid_certs = false eth_rpc_no_proxy = false +eth_rpc_curl = false ignored_error_codes = [ "license", "code-size", @@ -73,6 +74,7 @@ deny = "never" test_failures_file = "cache/test-failures" show_progress = false ffi = false +live_logs = false allow_internal_expect_revert = false always_use_create_2_factory = false prompt_timeout = 120 @@ -157,6 +159,14 @@ lint_on_build = true mixed_case_exceptions = [ "ERC", "URI", + "ID", + "URL", + "API", + "JSON", + "XML", + "HTML", + "HTTP", + "HTTPS", ] [doc] @@ -205,7 +215,7 @@ show_edge_coverage = false failure_persist_dir = "cache/invariant" show_metrics = true show_solidity = false -check_interval = 1 +continuous_run = false [labels] @@ -288,6 +298,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { ..Default::default() }, ffi: true, + live_logs: true, allow_internal_expect_revert: false, always_use_create_2_factory: false, prompt_timeout: 0, @@ -315,6 +326,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { eth_rpc_jwt: None, eth_rpc_timeout: None, eth_rpc_headers: None, + eth_rpc_curl: false, etherscan_api_key: None, etherscan: Default::default(), verbosity: 4, @@ -1221,6 +1233,7 @@ forgetest_init!(test_default_config, |prj, cmd| { "eth_rpc_jwt": null, "eth_rpc_timeout": null, "eth_rpc_headers": null, + "eth_rpc_curl": false, "etherscan_api_key": null, "ignored_error_codes": [ "license", @@ -1286,11 +1299,10 @@ forgetest_init!(test_default_config, |prj, cmd| { "show_metrics": true, "timeout": null, "show_solidity": false, - "max_time_delay": null, - "max_block_delay": null, - "check_interval": 1 + "continuous_run": false }, "ffi": false, + "live_logs": false, "allow_internal_expect_revert": false, "always_use_create_2_factory": false, "prompt_timeout": 120, @@ -1362,7 +1374,15 @@ forgetest_init!(test_default_config, |prj, cmd| { "lint_on_build": true, "mixed_case_exceptions": [ "ERC", - "URI" + "URI", + "ID", + "URL", + "API", + "JSON", + "XML", + "HTML", + "HTTP", + "HTTPS" ] }, "doc": { diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index edc794a42c998..5d4999f2b65ce 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -735,8 +735,7 @@ 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/reference/forge/forge-lint"; + 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 { @@ -762,8 +761,11 @@ async fn ensure_lint_rule_docs() { // Ensure no missing lints let mut missing_lints = Vec::new(); for lint in REGISTERED_LINTS { - let selector = format!("#{}", lint.id()); - if !content.contains(&selector) { + 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()); } } diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 4a1243051dd88..7f3dc7b9cd7a5 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3422,3 +3422,72 @@ forgetest_async!(can_execute_script_with_createx_and_via_ir, |prj, cmd| { ]) .assert_success(); }); + +forgetest_async!(script_can_run_with_live_logs_flag, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + prj.add_script( + "Foo.s.sol", + r#" +import {Script, console} from "forge-std/Script.sol"; + +contract Foo is Script { + function setUp() pure public { + console.log("Setup"); + } + + function run() pure public { + console.log("Run %d", uint256(1)); + } +} + "#, + ); + + cmd.forge_fuse() + .args(["script", "script/Foo.s.sol", "--live-logs"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Setup +Run 1 +Script ran successfully. +[GAS] + +"#]]); +}); + +forgetest_async!(script_can_run_with_live_logs_config, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + prj.update_config(|config| { + config.live_logs = true; + }); + + prj.add_script( + "Foo.s.sol", + r#" +import {Script, console} from "forge-std/Script.sol"; + +contract Foo is Script { + function setUp() pure public { + console.log("Setup"); + } + + function run() pure public { + console.log("Run %d", uint256(1)); + } +} + "#, + ); + + cmd.forge_fuse().args(["script", "script/Foo.s.sol"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Setup +Run 1 +Script ran successfully. +[GAS] + +"#]]); +}); diff --git a/crates/forge/tests/cli/test_cmd/invariant/mod.rs b/crates/forge/tests/cli/test_cmd/invariant/mod.rs index 54dc449065263..f1dbe01fcb08c 100644 --- a/crates/forge/tests/cli/test_cmd/invariant/mod.rs +++ b/crates/forge/tests/cli/test_cmd/invariant/mod.rs @@ -998,182 +998,82 @@ Ran 3 test suites [ELAPSED]: 6 tests passed, 0 failed, 0 skipped (6 total tests) ); }); -// Tests that check_interval=0 only asserts on the last call of each run. -forgetest_init!(check_interval_zero_only_checks_last_call, |prj, cmd| { +forgetest_init!(continous_run, |prj, cmd| { prj.update_config(|config| { - config.invariant.runs = 5; - config.invariant.depth = 10; - config.invariant.check_interval = 0; + config.invariant.runs = 10; + config.invariant.depth = 100; + config.invariant.continuous_run = true; }); - prj.add_test( - "CheckIntervalTest.t.sol", + prj.add_source( + "Counter.sol", r#" -import {Test} from "forge-std/Test.sol"; - -contract CounterHandler { - uint256 public counter; - - function increment() public { - counter++; - } -} - -contract CheckIntervalTest is Test { - CounterHandler handler; - - function setUp() public { - handler = new CounterHandler(); - targetContract(address(handler)); - } +contract Counter { + uint256 public cond; - // This invariant would fail on intermediate calls (counter 1-9) but passes on call 10 - // With check_interval=0, only the last call is checked, so if depth=10 and counter=10 - // at the end, this should pass even though intermediate states violated the invariant. - function invariant_counter_multiple_of_depth() public view { - // Only passes when counter is 0 or 10 (depth). Fails for 1-9. - require(handler.counter() == 0 || handler.counter() == 10, "not multiple of depth"); + function work(uint256 x) public { + if (x % 2 != 0 && x < 9000) { + cond++; + } else { + revert(); + } } } "#, ); - - cmd.args(["test", "--mt", "invariant_counter"]).assert_success().stdout_eq(str![[r#" -... -[PASS] invariant_counter_multiple_of_depth() (runs: 5, calls: 50, reverts: 0) -... -"#]]); -}); - -// Tests that check_interval=1 (default) asserts after every call. -forgetest_init!(check_interval_one_checks_every_call, |prj, cmd| { - prj.update_config(|config| { - config.invariant.runs = 1; - config.invariant.depth = 10; - config.invariant.check_interval = 1; - }); prj.add_test( - "CheckIntervalTest.t.sol", + "CounterTest.t.sol", r#" import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; -contract CounterHandler { - uint256 public counter; - - function increment() public { - counter++; - } -} - -contract CheckIntervalTest is Test { - CounterHandler handler; +contract CounterTest is Test { + Counter public counter; function setUp() public { - handler = new CounterHandler(); - targetContract(address(handler)); - } - - // This invariant fails as soon as counter > 5. - // With check_interval=1, it should fail on call 6. - function invariant_counter_le_five() public view { - require(handler.counter() <= 5, "counter > 5"); - } -} - "#, - ); - - assert_invariant(cmd.args(["test", "--mt", "invariant_counter"])).failure().stdout_eq(str![[ - r#" -... -[FAIL: counter > 5] - [SEQUENCE] -... -"# - ]]); -}); - -// Tests that check_interval=N checks every N calls AND always on the last call. -forgetest_init!(check_interval_n_checks_every_n_calls, |prj, cmd| { - prj.update_config(|config| { - config.invariant.runs = 1; - config.invariant.depth = 20; - config.invariant.check_interval = 5; - }); - prj.add_test( - "CheckIntervalTest.t.sol", - r#" -import {Test} from "forge-std/Test.sol"; - -contract CounterHandler { - uint256 public counter; - - function increment() public { - counter++; + counter = new Counter(); } -} - -contract CheckIntervalTest is Test { - CounterHandler handler; - function setUp() public { - handler = new CounterHandler(); - targetContract(address(handler)); + function invariant_cond1() public view { + require(counter.cond() < 10, "condition 1 met"); } - // With check_interval=5 and depth=20, invariant is checked at calls 5,10,15,20. - // This passes because 5,10,15,20 are all multiples of 5. - function invariant_counter_multiple_of_five() public view { - require(handler.counter() % 5 == 0, "not multiple of 5"); + function invariant_cond2() public view { + require(counter.cond() < 15, "condition 2 met"); } -} - "#, - ); - - cmd.args(["test", "--mt", "invariant_counter"]).assert_success().stdout_eq(str![[r#" -... -[PASS] invariant_counter_multiple_of_five() (runs: 1, calls: 20, reverts: 0) -... -"#]]); -}); -// Tests check_interval via inline config annotation. -forgetest_init!(check_interval_inline_config, |prj, cmd| { - prj.add_test( - "CheckIntervalInlineTest.t.sol", - r#" -import {Test} from "forge-std/Test.sol"; - -contract CounterHandler { - uint256 public counter; - - function increment() public { - counter++; + function invariant_cond3() public view { + require(counter.cond() < 5, "condition 3 met"); } -} -contract CheckIntervalInlineTest is Test { - CounterHandler handler; - - function setUp() public { - handler = new CounterHandler(); - targetContract(address(handler)); + function invariant_cond4() public view { + require(counter.cond() < 111111, "condition 4 met"); } - /// forge-config: default.invariant.runs = 1 - /// forge-config: default.invariant.depth = 10 - /// forge-config: default.invariant.check_interval = 0 - function invariant_only_last_checked() public view { - // Only passes when counter is 0 or 10. With check_interval=0, only last call is checked. - require(handler.counter() == 0 || handler.counter() == 10, "not at boundary"); + /// forge-config: default.invariant.fail-on-revert = true + function invariant_cond5() public view { + require(counter.cond() < 111111, "condition 5 met"); } } "#, ); - cmd.args(["test", "--mt", "invariant_only_last_checked"]).assert_success().stdout_eq(str![[ - r#" + // Check that running single `invariant_cond3` test continue to run until it breaks all other + // invariants. + cmd.args(["test", "--mt", "invariant_cond3"]).assert_failure().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/CounterTest.t.sol:CounterTest +[FAIL: condition 3 met] + [Sequence] (original: 5, shrunk: 5) ... -[PASS] invariant_only_last_checked() (runs: 1, calls: 10, reverts: 0) + +invariant_cond1: condition 1 met +invariant_cond2: condition 2 met +invariant_cond5: EvmError: Revert + invariant_cond3() (runs: 10, calls: 1000, reverts: [..]) ... -"# - ]]); + +"#]]); }); diff --git a/crates/forge/tests/cli/test_cmd/logs.rs b/crates/forge/tests/cli/test_cmd/logs.rs index c9d1e39dc6155..0dba7fa8baa7c 100644 --- a/crates/forge/tests/cli/test_cmd/logs.rs +++ b/crates/forge/tests/cli/test_cmd/logs.rs @@ -752,3 +752,145 @@ Ran 1 test suite [ELAPSED]: 52 tests passed, 0 failed, 0 skipped (52 total tests "#]]); }); + +forgetest_init!(test_can_run_with_live_logs_flag, |prj, cmd| { + prj.add_test( + "Foo.t.sol", + r#" +import {Test, console} from "forge-std/Test.sol"; + +contract Foo is Test { + function setUp() pure public { + console.log("Setup"); + } + + function test1() pure public { + console.log("Test 1"); + } +} + "#, + ); + + cmd.forge_fuse() + .args(["test", "--live-logs", "--match-test", "test1"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Setup +Test 1 + +Ran 1 test for test/Foo.t.sol:Foo +[PASS] test1() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); +}); + +forgetest_init!(test_can_run_with_live_logs_config, |prj, cmd| { + prj.update_config(|config| { + config.live_logs = true; + }); + + prj.add_test( + "Foo.t.sol", + r#" +import {Test, console} from "forge-std/Test.sol"; + +contract Foo is Test { + function setUp() pure public { + console.log("Setup"); + } + + function test1() pure public { + console.log("Test 1"); + } +} + "#, + ); + + cmd.forge_fuse().args(["test", "--match-test", "test1"]).assert_success().stdout_eq(str![[ + r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Setup +Test 1 + +Ran 1 test for test/Foo.t.sol:Foo +[PASS] test1() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"# + ]]); +}); + +forgetest_init!(test_can_run_with_live_logs_flag_race_condition, |prj, cmd| { + prj.add_test( + "Foo.t.sol", + r#" +import {Test, console} from "forge-std/Test.sol"; + +contract Foo is Test { + function setUp() pure public { + console.log("Setup"); + } + + function test1() pure public { + console.log("Test 1"); + } + + function test2() pure public { + console.log("Test 2"); + } +} + "#, + ); + + // Two threads. Inconsistent printing order. + cmd.forge_fuse().args(["test", "--live-logs", "--threads", "2"]).assert_success().stdout_eq( + str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Setup +Test [..] +Test [..] + +Ran 2 tests for test/Foo.t.sol:Foo +[PASS] test1() ([GAS]) +[PASS] test2() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]], + ); + + // Single thread. Deterministic printing order. + + for _ in 0..10 { + cmd.forge_fuse() + .args(["test", "--live-logs", "--threads", "1"]) + .assert_success() + .stdout_eq(str![[r#" +No files changed, compilation skipped +Setup +Test 1 +Test 2 + +Ran 2 tests for test/Foo.t.sol:Foo +[PASS] test1() ([GAS]) +[PASS] test2() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); + } +}); diff --git a/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json b/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json index d8e517219b74f..6c8d55b8e13b1 100644 --- a/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json +++ b/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json @@ -5,6 +5,7 @@ "test()": { "status": "Success", "reason": null, + "other_failures": [], "counterexample": null, "logs": [], "decoded_logs": [], diff --git a/crates/forge/tests/fixtures/SimpleContractTestVerbose.json b/crates/forge/tests/fixtures/SimpleContractTestVerbose.json index 244a2d94fc7eb..035ac6fb1adc6 100644 --- a/crates/forge/tests/fixtures/SimpleContractTestVerbose.json +++ b/crates/forge/tests/fixtures/SimpleContractTestVerbose.json @@ -5,6 +5,7 @@ "test()": { "status": "Success", "reason": null, + "other_failures": [], "counterexample": null, "logs": [ { diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index ebf33383c9491..387694aea7656 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -208,7 +208,7 @@ impl<'a> Linker<'a> { .filter(|id| { // Filter out already provided libraries. let (file, name) = self.convert_artifact_id_to_lib_path(id); - !libraries.libs.contains_key(&file) || !libraries.libs[&file].contains_key(&name) + libraries.libs.get(&file).is_none_or(|lib| !lib.contains_key(&name)) }) .map(|id| { // Link library with provided libs and extract bytecode object (possibly unlinked). diff --git a/crates/lint/src/linter/early.rs b/crates/lint/src/linter/early.rs index 5a102a103b6c5..e459b83158e8b 100644 --- a/crates/lint/src/linter/early.rs +++ b/crates/lint/src/linter/early.rs @@ -36,7 +36,10 @@ pub trait EarlyLintPass<'ast>: Send + Sync { ) { } fn check_doc_comment(&mut self, _ctx: &LintContext, _cmnt: &'ast ast::DocComment) {} - // TODO: Add methods for each required AST node type + fn check_item(&mut self, _ctx: &LintContext, _item: &'ast ast::Item<'ast>) {} + fn check_stmt(&mut self, _ctx: &LintContext, _stmt: &'ast ast::Stmt<'ast>) {} + fn check_path(&mut self, _ctx: &LintContext, _path: &'ast ast::PathSlice) {} + fn check_ty(&mut self, _ctx: &LintContext, _ty: &'ast ast::Type<'ast>) {} /// Should be called after the source unit has been visited. Enables lints that require /// knowledge of the entire AST to perform their analysis. @@ -171,6 +174,31 @@ where self.walk_item_contract(contract) } - // TODO: Add methods for each required AST node type, mirroring `solar::ast::visit::Visit` - // method sigs + adding `LintContext` + fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_item(self.ctx, item) + } + self.walk_item(item) + } + + fn visit_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_stmt(self.ctx, stmt) + } + self.walk_stmt(stmt) + } + + fn visit_path(&mut self, path: &'ast ast::PathSlice) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_path(self.ctx, path) + } + self.walk_path(path) + } + + fn visit_ty(&mut self, ty: &'ast ast::Type<'ast>) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_ty(self.ctx, ty) + } + self.walk_ty(ty) + } } diff --git a/crates/lint/src/sol/macros.rs b/crates/lint/src/sol/macros.rs index e1052582b5f7a..00d764770374a 100644 --- a/crates/lint/src/sol/macros.rs +++ b/crates/lint/src/sol/macros.rs @@ -23,10 +23,6 @@ macro_rules! declare_forge_lint { help: concat!("https://book.getfoundry.sh/reference/forge/forge-lint#", $str_id), }; }; - - ($id:ident, $severity:expr, $str_id:expr, $desc:expr) => { - $crate::declare_forge_lint!($id, $severity, $str_id, $desc, ""); - }; } /// Registers Solidity linter passes that can have both early and late variants. diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index a4c06f188bf8a..fd610da96cd31 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -31,3 +31,4 @@ serde_json.workspace = true serde = { version = "1.0", features = ["derive"] } derive_more.workspace = true tempo-primitives.workspace = true +tempo-alloy.workspace = true diff --git a/crates/primitives/src/transaction/receipt.rs b/crates/primitives/src/transaction/receipt.rs index 62415ef3f1a0d..7df6494fc584a 100644 --- a/crates/primitives/src/transaction/receipt.rs +++ b/crates/primitives/src/transaction/receipt.rs @@ -391,10 +391,10 @@ impl Typed2718 for FoundryReceiptEnvelope { impl Encodable2718 for FoundryReceiptEnvelope { fn encode_2718_len(&self) -> usize { match self { - Self::Legacy(r) => ReceiptEnvelope::Legacy(r.clone()).encode_2718_len(), - Self::Eip2930(r) => ReceiptEnvelope::Eip2930(r.clone()).encode_2718_len(), - Self::Eip1559(r) => ReceiptEnvelope::Eip1559(r.clone()).encode_2718_len(), - Self::Eip4844(r) => ReceiptEnvelope::Eip4844(r.clone()).encode_2718_len(), + Self::Legacy(r) => r.length(), + Self::Eip2930(r) => 1 + r.length(), + Self::Eip1559(r) => 1 + r.length(), + Self::Eip4844(r) => 1 + r.length(), Self::Eip7702(r) => 1 + r.length(), Self::Deposit(r) => 1 + r.length(), Self::Tempo(r) => 1 + r.length(), diff --git a/crates/primitives/src/transaction/request.rs b/crates/primitives/src/transaction/request.rs index bc28b6ffbdfc9..6ee4b7adaf408 100644 --- a/crates/primitives/src/transaction/request.rs +++ b/crates/primitives/src/transaction/request.rs @@ -5,38 +5,30 @@ use alloy_network::{ use alloy_primitives::{Address, B256, ChainId, TxKind, U256}; use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest}; use alloy_serde::{OtherFields, WithOtherFields}; -use derive_more::{AsMut, AsRef, From, Into}; use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; use op_revm::transaction::deposit::DepositTransactionParts; use serde::{Deserialize, Serialize}; -use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTransaction, transaction::Call}; +use tempo_alloy::rpc::TempoTransactionRequest; +use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType}; use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx}; use crate::FoundryNetwork; /// Foundry transaction request builder. /// -/// This is implemented as a wrapper around [`WithOtherFields`], -/// which provides handling of deposit transactions. -#[derive(Clone, Debug, Default, PartialEq, Eq, From, Into, AsRef, AsMut)] -pub struct FoundryTransactionRequest(WithOtherFields); - -impl Serialize for FoundryTransactionRequest { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.as_ref().serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for FoundryTransactionRequest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - WithOtherFields::::deserialize(deserializer).map(Self) - } +/// This is a union of different transaction request types, instantiated from a +/// [`WithOtherFields`]. The specific variant is determined by the transaction +/// type field and/or the presence of certain fields: +/// - **Ethereum**: Default variant when no special fields are present +/// - **Op**: When `sourceHash`, `mint`, and `isSystemTx` fields are present, or transaction type is +/// `DEPOSIT_TX_TYPE_ID` +/// - **Tempo**: When `feeToken` or `nonceKey` fields are present, or transaction type is +/// `TEMPO_TX_TYPE_ID` +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FoundryTransactionRequest { + Ethereum(TransactionRequest), + Op(WithOtherFields), + Tempo(Box), } impl FoundryTransactionRequest { @@ -44,79 +36,40 @@ impl FoundryTransactionRequest { /// [`WithOtherFields`]. #[inline] pub fn new(inner: WithOtherFields) -> Self { - Self(inner) - } - - /// Consume self and return the inner [`WithOtherFields`]. - #[inline] - pub fn into_inner(self) -> WithOtherFields { - self.0 + inner.into() } - /// Check if this is a deposit transaction. - #[inline] - pub fn is_deposit(&self) -> bool { - self.as_ref().transaction_type == Some(DEPOSIT_TX_TYPE_ID) + /// Consume the [`FoundryTransactionRequest`] and return the inner transaction request. + pub fn into_inner(self) -> TransactionRequest { + match self { + Self::Ethereum(tx) => tx, + Self::Op(tx) => tx.inner, + Self::Tempo(tx) => tx.inner, + } } - /// Check if this is a Tempo transaction. + /// Get the deposit transaction parts from the request, calling [`get_deposit_tx_parts`] helper + /// with OtherFields. /// - /// Returns true if the transaction type is explicitly set to Tempo (0x76) or if - /// a `feeToken` is set in OtherFields. - #[inline] - pub fn is_tempo(&self) -> bool { - self.as_ref().transaction_type == Some(TEMPO_TX_TYPE_ID) - || self.as_ref().other.contains_key("feeToken") - || self.as_ref().other.contains_key("nonceKey") - } - - /// Get the Tempo fee token from OtherFields if present. - fn get_tempo_fee_token(&self) -> Option

{ - self.as_ref().other.get_deserialized::
("feeToken").transpose().ok().flatten() - } - - /// Get the Tempo nonce sequence key from OtherFields if present. - fn get_tempo_nonce_key(&self) -> U256 { - self.as_ref() - .other - .get_deserialized::("nonceKey") - .transpose() - .ok() - .flatten() - .unwrap_or_default() - } - - /// Check if all necessary keys are present to build a Tempo transaction, returning a list of - /// keys that are missing. - pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> { - let mut missing = Vec::new(); - if self.chain_id().is_none() { - missing.push("chain_id"); - } - if self.gas_limit().is_none() { - missing.push("gas_limit"); - } - if self.max_fee_per_gas().is_none() { - missing.push("max_fee_per_gas"); - } - if self.max_priority_fee_per_gas().is_none() { - missing.push("max_priority_fee_per_gas"); - } - if self.nonce().is_none() { - missing.push("nonce"); + /// # 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. + pub fn get_deposit_tx_parts(&self) -> Result> { + match self { + Self::Op(tx) => get_deposit_tx_parts(&tx.other), + // Not a deposit transaction request, so missing at least sourceHash, mint, and + // isSystemTx + _ => Err(vec!["sourceHash", "mint", "isSystemTx"]), } - if missing.is_empty() { Ok(()) } else { Err(missing) } } /// Returns the minimal transaction type this request can be converted into based on the fields /// that are set. See [`TransactionRequest::preferred_type`]. pub fn preferred_type(&self) -> FoundryTxType { - if self.is_deposit() { - FoundryTxType::Deposit - } else if self.is_tempo() { - FoundryTxType::Tempo - } else { - self.as_ref().preferred_type().into() + match self { + Self::Ethereum(tx) => tx.preferred_type().into(), + Self::Op(_) => FoundryTxType::Deposit, + Self::Tempo(_) => FoundryTxType::Tempo, } } @@ -139,23 +92,17 @@ impl FoundryTransactionRequest { /// Check if all necessary keys are present to build a Deposit transaction, returning a list of /// keys that are missing. pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> { - get_deposit_tx_parts(&self.as_ref().other).map(|_| ()) + self.get_deposit_tx_parts().map(|_| ()) } - /// Return the tx type this request can be built as. Computed by checking - /// the preferred type, and then checking for completeness. - pub fn buildable_type(&self) -> Option { - let pref = self.preferred_type(); - match pref { - FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(), - FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(), - FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(), - FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), - FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), - FoundryTxType::Deposit => self.complete_deposit().ok(), - FoundryTxType::Tempo => self.complete_tempo().ok(), - }?; - Some(pref) + /// Check if all necessary keys are present to build a Tempo transaction, returning a list of + /// keys that are missing. + pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> { + match self { + Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()), + // Not a Tempo transaction request, so missing at least feeToken and nonceKey + _ => Err(vec!["feeToken", "nonceKey"]), + } } /// Check if all necessary keys are present to build a transaction. @@ -186,8 +133,8 @@ impl FoundryTransactionRequest { /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction /// types. pub fn build_typed_tx(self) -> Result { - // Handle deposit transactions - if let Ok(deposit_tx_parts) = get_deposit_tx_parts(&self.as_ref().other) { + if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() { + // Build deposit transaction Ok(FoundryTypedTx::Deposit(TxDeposit { from: self.from().unwrap_or_default(), source_hash: deposit_tx_parts.source_hash, @@ -198,38 +145,26 @@ impl FoundryTransactionRequest { is_system_transaction: deposit_tx_parts.is_system_transaction, input: self.input().cloned().unwrap_or_default(), })) - } else if self.is_tempo() { - // Build Tempo transaction from request fields - Ok(FoundryTypedTx::Tempo(TempoTransaction { - chain_id: self.chain_id().unwrap_or_default(), - fee_token: self.get_tempo_fee_token(), - max_fee_per_gas: self.max_fee_per_gas().unwrap_or_default(), - max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or_default(), - gas_limit: self.gas_limit().unwrap_or_default(), - nonce_key: self.get_tempo_nonce_key(), - nonce: self.nonce().unwrap_or_default(), - calls: vec![Call { - to: self.kind().unwrap_or_default(), - value: self.value().unwrap_or_default(), - input: self.input().cloned().unwrap_or_default(), - }], - access_list: self.access_list().cloned().unwrap_or_default(), - ..Default::default() - })) + } else if self.complete_tempo().is_ok() + && let Self::Tempo(tx_req) = self + { + // Build Tempo transaction + Ok(FoundryTypedTx::Tempo( + tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?, + )) } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() && alloy_network::TransactionBuilder7594::blob_sidecar_7594(self.as_ref()).is_none() { // if request has eip4844 fields but no blob sidecar (neither eip4844 nor eip7594 // format), try to build to eip4844 without sidecar - self.0 - .into_inner() + self.into_inner() .build_4844_without_sidecar() - .map_err(|e| Self(e.into_value().into())) + .map_err(|e| Self::Ethereum(e.into_value())) .map(|tx| FoundryTypedTx::Eip4844(tx.into())) } else { // Use the inner transaction request to build EthereumTypedTransaction - let typed_tx = self.0.into_inner().build_typed_tx().map_err(|tx| Self(tx.into()))?; + let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?; // Convert EthereumTypedTransaction to FoundryTypedTx Ok(match typed_tx { EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx), @@ -242,14 +177,90 @@ impl FoundryTransactionRequest { } } +impl Default for FoundryTransactionRequest { + fn default() -> Self { + Self::Ethereum(TransactionRequest::default()) + } +} + +impl Serialize for FoundryTransactionRequest { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Ethereum(tx) => tx.serialize(serializer), + Self::Op(tx) => tx.serialize(serializer), + Self::Tempo(tx) => tx.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for FoundryTransactionRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + WithOtherFields::::deserialize(deserializer).map(Into::::into) + } +} + +impl AsRef for FoundryTransactionRequest { + fn as_ref(&self) -> &TransactionRequest { + match self { + Self::Ethereum(tx) => tx, + Self::Op(tx) => tx, + Self::Tempo(tx) => tx, + } + } +} + +impl AsMut for FoundryTransactionRequest { + fn as_mut(&mut self) -> &mut TransactionRequest { + match self { + Self::Ethereum(tx) => tx, + Self::Op(tx) => tx, + Self::Tempo(tx) => tx, + } + } +} + +impl From> for FoundryTransactionRequest { + fn from(tx: WithOtherFields) -> Self { + if tx.transaction_type == Some(TEMPO_TX_TYPE_ID) + || tx.other.contains_key("feeToken") + || tx.other.contains_key("nonceKey") + { + let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into(); + if let Some(fee_token) = + tx.other.get_deserialized::
("feeToken").transpose().ok().flatten() + { + tempo_tx_req.fee_token = Some(fee_token); + } + if let Some(nonce_key) = + tx.other.get_deserialized::("nonceKey").transpose().ok().flatten() + { + 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) + || get_deposit_tx_parts(&tx.other).is_ok() + { + Self::Op(tx) + } else { + Self::Ethereum(tx.into_inner()) + } + } +} + impl From for FoundryTransactionRequest { fn from(tx: FoundryTypedTx) -> Self { match tx { - FoundryTypedTx::Legacy(tx) => Self(Into::::into(tx).into()), - FoundryTypedTx::Eip2930(tx) => Self(Into::::into(tx).into()), - FoundryTypedTx::Eip1559(tx) => Self(Into::::into(tx).into()), - FoundryTypedTx::Eip4844(tx) => Self(Into::::into(tx).into()), - FoundryTypedTx::Eip7702(tx) => Self(Into::::into(tx).into()), + FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::::into(tx)), + FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::::into(tx)), + FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::::into(tx)), + FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::::into(tx)), + FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::::into(tx)), FoundryTypedTx::Deposit(tx) => { let other = OtherFields::from_iter([ ("sourceHash", tx.source_hash.to_string().into()), @@ -421,8 +432,8 @@ impl TransactionBuilder for FoundryTransactionRequest { fn can_build(&self) -> bool { self.as_ref().can_build() - || get_deposit_tx_parts(&self.as_ref().other).is_ok() - || self.is_tempo() + || self.complete_deposit().is_ok() + || self.complete_tempo().is_ok() } fn output_tx_type(&self) -> FoundryTxType { @@ -430,7 +441,17 @@ impl TransactionBuilder for FoundryTransactionRequest { } fn output_tx_type_checked(&self) -> Option { - self.buildable_type() + let pref = self.preferred_type(); + match pref { + FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(), + FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(), + FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(), + FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), + FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), + FoundryTxType::Deposit => self.complete_deposit().ok(), + FoundryTxType::Tempo => self.complete_tempo().ok(), + }?; + Some(pref) } /// Prepares [`FoundryTransactionRequest`] by trimming conflicting fields, and filling with @@ -439,19 +460,19 @@ impl TransactionBuilder for FoundryTransactionRequest { let preferred_type = self.preferred_type(); let inner = self.as_mut(); inner.transaction_type = Some(preferred_type as u8); - inner.gas_limit().is_none().then(|| inner.set_gas_limit(Default::default())); + inner.gas.is_none().then(|| inner.set_gas_limit(Default::default())); if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) { inner.trim_conflicting_keys(); inner.populate_blob_hashes(); } if preferred_type != FoundryTxType::Deposit { - inner.nonce().is_none().then(|| inner.set_nonce(Default::default())); + inner.nonce.is_none().then(|| inner.set_nonce(Default::default())); } if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) { - inner.gas_price().is_none().then(|| inner.set_gas_price(Default::default())); + inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default())); } if preferred_type == FoundryTxType::Eip2930 { - inner.access_list().is_none().then(|| inner.set_access_list(Default::default())); + inner.access_list.is_none().then(|| inner.set_access_list(Default::default())); } if matches!( preferred_type, @@ -461,13 +482,10 @@ impl TransactionBuilder for FoundryTransactionRequest { | FoundryTxType::Tempo ) { inner - .max_priority_fee_per_gas() + .max_priority_fee_per_gas .is_none() .then(|| inner.set_max_priority_fee_per_gas(Default::default())); - inner - .max_fee_per_gas() - .is_none() - .then(|| inner.set_max_fee_per_gas(Default::default())); + inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default())); } if preferred_type == FoundryTxType::Eip4844 { inner @@ -545,3 +563,121 @@ pub fn get_deposit_tx_parts( Err(missing) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn default_tx_req() -> TransactionRequest { + TransactionRequest::default() + .with_to(Address::random()) + .with_nonce(1) + .with_value(U256::from(1000000)) + .with_gas_limit(1000000) + .with_max_fee_per_gas(1000000) + .with_max_priority_fee_per_gas(1000000) + } + + #[test] + fn test_routing_ethereum_default() { + let tx = default_tx_req(); + let req: FoundryTransactionRequest = WithOtherFields::new(tx).into(); + + assert!(matches!(req, FoundryTransactionRequest::Ethereum(_))); + assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_)))); + } + + #[test] + fn test_routing_tempo_by_fee_token() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap()); + + let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + assert!(matches!(req, FoundryTransactionRequest::Tempo(_))); + assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_)))); + } + + #[test] + fn test_routing_op_by_deposit_fields() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap()); + other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap()); + other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap()); + + let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + assert!(matches!(req, FoundryTransactionRequest::Op(_))); + assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_)))); + } + + #[test] + fn test_op_incomplete_routes_to_ethereum() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + // Only provide 2 of 3 required Op fields + other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap()); + other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap()); + + let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + assert!(matches!(req, FoundryTransactionRequest::Ethereum(_))); + assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_)))); + } + + #[test] + fn test_ethereum_with_unrelated_other_fields() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap()); + + let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + assert!(matches!(req, FoundryTransactionRequest::Ethereum(_))); + assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_)))); + } + + #[test] + fn test_serialization_ethereum() { + let tx = default_tx_req(); + let original: FoundryTransactionRequest = WithOtherFields::new(tx).into(); + + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap(); + + assert!(matches!(deserialized, FoundryTransactionRequest::Ethereum(_))); + } + + #[test] + fn test_serialization_op() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap()); + other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap()); + other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap()); + + let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap(); + + assert!(matches!(deserialized, FoundryTransactionRequest::Op(_))); + } + + #[test] + fn test_serialization_tempo() { + let tx = default_tx_req(); + let mut other = OtherFields::default(); + other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap()); + other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap()); + + let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into(); + + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap(); + + assert!(matches!(deserialized, FoundryTransactionRequest::Tempo(_))); + } +} diff --git a/crates/script-sequence/src/sequence.rs b/crates/script-sequence/src/sequence.rs index 5da88735a70ec..c22f4987656d2 100644 --- a/crates/script-sequence/src/sequence.rs +++ b/crates/script-sequence/src/sequence.rs @@ -166,8 +166,8 @@ impl ScriptSequence { } /// Gets paths in the formats - /// `./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json` and - /// `./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json`. + /// `./broadcast/[contract_filename]/[chain_id]/[sig]-latest.json` and + /// `./cache/[contract_filename]/[chain_id]/[sig]-latest.json`. pub fn get_paths( config: &Config, sig: &str, diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index 52170b62087e8..a25f0d6de15ca 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -543,8 +543,12 @@ impl BundledState { (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price) }); let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string()); - let avg_gas_price = format_units(total_gas_price / sequence.receipts.len() as u64, 9) - .unwrap_or_else(|_| "N/A".to_string()); + let avg_gas_price = if sequence.receipts.is_empty() { + "N/A".to_string() + } else { + format_units(total_gas_price / sequence.receipts.len() as u64, 9) + .unwrap_or_else(|_| "N/A".to_string()) + }; let token_symbol = NamedChain::try_from(sequence.chain) .unwrap_or_default() diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 12f57c45641fe..72aaae47a0c59 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -650,6 +650,7 @@ impl ScriptConfig { let mut builder = ExecutorBuilder::new() .inspectors(|stack| { stack + .logs(self.config.live_logs) .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call }) .networks(self.evm_opts.networks) .create2_deployer(self.evm_opts.create2_deployer) diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index ab57786398428..00869a34dd3cf 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -60,7 +60,7 @@ impl PreSimulationState { .map(|tx| { let rpc = tx.rpc.expect("missing broadcastable tx rpc url"); let sender = tx.transaction.from().expect("all transactions should have a sender"); - let nonce = tx.transaction.nonce().expect("all transactions should have a sender"); + let nonce = tx.transaction.nonce().expect("all transactions should have a nonce"); let to = tx.transaction.to(); let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc); diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 705107b98eb9f..f98bfb941cfc0 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -235,6 +235,7 @@ 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) } diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 67301a6c83dcf..1047cded2f34d 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -335,6 +335,7 @@ impl VerifyBytecodeArgs { ); }; + let creation_block = transaction.block_number; let mut transaction: TransactionRequest = match transaction.inner.inner.inner() { AnyTxEnvelope::Ethereum(tx) => tx.clone().into(), AnyTxEnvelope::Unknown(_) => unreachable!("Unknown transaction type"), @@ -440,13 +441,7 @@ impl VerifyBytecodeArgs { Some(BlockId::Number(BlockNumberOrTag::Number(block))) => block, Some(_) => eyre::bail!("Invalid block number"), None => { - let provider = utils::get_provider(&config)?; - provider - .get_transaction_by_hash(creation_data.transaction_hash) - .await.or_else(|e| eyre::bail!("Couldn't fetch transaction from RPC: {:?}", e))?.ok_or_else(|| { - eyre::eyre!("Transaction not found for hash {}", creation_data.transaction_hash) - })? - .block_number.ok_or_else(|| { + creation_block.ok_or_else(|| { eyre::eyre!("Failed to get block number of the contract creation tx, specify using the --block flag") })? } diff --git a/crates/verify/src/verify.rs b/crates/verify/src/verify.rs index add63ed36a767..a127c24dbd229 100644 --- a/crates/verify/src/verify.rs +++ b/crates/verify/src/verify.rs @@ -544,7 +544,10 @@ impl figment::Provider for VerifyCheckArgs { /// alloy-chains. This function returns the properly formatted URL for such chains. fn sourcify_api_url(chain: Chain) -> Option { if chain.is_custom_sourcify() { - chain.etherscan_urls().map(|(api_url, _)| format!("{api_url}/").replace("//", "/")) + chain.etherscan_urls().map(|(api_url, _)| { + let api_url = api_url.trim_end_matches('/'); + format!("{api_url}/") + }) } else { None } diff --git a/crates/wallets/Cargo.toml b/crates/wallets/Cargo.toml index 24013c74f925a..e399f29e87803 100644 --- a/crates/wallets/Cargo.toml +++ b/crates/wallets/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] foundry-config.workspace = true -foundry-primitives.workspace = true alloy-primitives.workspace = true alloy-signer = { workspace = true, features = ["eip712"] } @@ -26,8 +25,6 @@ alloy-consensus.workspace = true alloy-sol-types.workspace = true alloy-dyn-abi.workspace = true -tempo-primitives.workspace = true - # browser wallet alloy-rpc-types.workspace = true axum.workspace = true diff --git a/crates/wallets/src/signer.rs b/crates/wallets/src/signer.rs index 21ab2c8530162..93f721e5703e9 100644 --- a/crates/wallets/src/signer.rs +++ b/crates/wallets/src/signer.rs @@ -1,7 +1,7 @@ use crate::{error::WalletSignerError, wallet_browser::signer::BrowserSigner}; -use alloy_consensus::{Sealed, SignableTransaction}; +use alloy_consensus::SignableTransaction; use alloy_dyn_abi::TypedData; -use alloy_network::{NetworkWallet, TransactionBuilder, TxSigner}; +use alloy_network::TxSigner; use alloy_primitives::{Address, B256, ChainId, Signature, hex}; use alloy_signer::Signer; use alloy_signer_ledger::{HDPath as LedgerHDPath, LedgerSigner}; @@ -9,11 +9,7 @@ use alloy_signer_local::{MnemonicBuilder, PrivateKeySigner, coins_bip39::English use alloy_signer_trezor::{HDPath as TrezorHDPath, TrezorSigner}; use alloy_sol_types::{Eip712Domain, SolStruct}; use async_trait::async_trait; -use foundry_primitives::{ - FoundryNetwork, FoundryTransactionRequest, FoundryTxEnvelope, FoundryTypedTx, -}; use std::{collections::HashSet, path::PathBuf, time::Duration}; -use tempo_primitives::TempoSignature; use tracing::warn; #[cfg(feature = "aws-kms")] @@ -337,74 +333,6 @@ impl TxSigner for WalletSigner { } } -impl NetworkWallet for WalletSigner { - fn default_signer_address(&self) -> Address { - alloy_signer::Signer::address(self) - } - - fn has_signer_for(&self, address: &Address) -> bool { - self.default_signer_address() == *address - } - - fn signer_addresses(&self) -> impl Iterator { - std::iter::once(self.default_signer_address()) - } - - async fn sign_transaction_from( - &self, - sender: Address, - tx: FoundryTypedTx, - ) -> alloy_signer::Result { - if sender != self.default_signer_address() { - return Err(alloy_signer::Error::other("Signer address mismatch")); - } - - match tx { - FoundryTypedTx::Legacy(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - Ok(FoundryTxEnvelope::Legacy(inner.into_signed(sig))) - } - FoundryTypedTx::Eip2930(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - Ok(FoundryTxEnvelope::Eip2930(inner.into_signed(sig))) - } - FoundryTypedTx::Eip1559(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - Ok(FoundryTxEnvelope::Eip1559(inner.into_signed(sig))) - } - FoundryTypedTx::Eip4844(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - Ok(FoundryTxEnvelope::Eip4844(inner.into_signed(sig))) - } - FoundryTypedTx::Eip7702(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - Ok(FoundryTxEnvelope::Eip7702(inner.into_signed(sig))) - } - FoundryTypedTx::Deposit(inner) => { - // Deposit transactions don't require signing - Ok(FoundryTxEnvelope::Deposit(Sealed::new(inner))) - } - FoundryTypedTx::Tempo(mut inner) => { - let sig = TxSigner::sign_transaction(self, &mut inner).await?; - let tempo_sig: TempoSignature = sig.into(); - Ok(FoundryTxEnvelope::Tempo(inner.into_signed(tempo_sig))) - } - } - } - - #[doc(hidden)] - async fn sign_request( - &self, - request: FoundryTransactionRequest, - ) -> alloy_signer::Result { - let sender = request.from().unwrap_or_else(|| self.default_signer_address()); - let tx = request.build_typed_tx().map_err(|_| { - alloy_signer::Error::other("Failed to build typed transaction from request") - })?; - self.sign_transaction_from(sender, tx).await - } -} - /// Signers that require user action to be obtained. #[derive(Debug, Clone)] pub enum PendingSigner { diff --git a/deny.toml b/deny.toml index ac67b4309e72a..2b9258cd3991a 100644 --- a/deny.toml +++ b/deny.toml @@ -63,7 +63,6 @@ exceptions = [ # CC0 is a permissive license but somewhat unclear status for source code # so we prefer to not have dependencies using it # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal - { allow = ["CC0-1.0"], name = "tiny-keccak" }, { allow = ["CC0-1.0"], name = "trezor-client" }, { allow = ["CC0-1.0"], name = "notify" }, { allow = ["CC0-1.0"], name = "dunce" }, diff --git a/flake.lock b/flake.lock index d8fd982357a31..9b2a50aa4a325 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1769842381, - "narHash": "sha256-0dPzo1ElvAIZ0RwEwx5FfqAUiFj22K9QJOU9stiMCrw=", + "lastModified": 1771052630, + "narHash": "sha256-R/52n/XyyWHYL1Mw0Q0GFF/nxyRyaE8gkLyfvi+ebxU=", "owner": "nix-community", "repo": "fenix", - "rev": "b2344f384a82db1410ab09769eb8c4a820de667f", + "rev": "d0555da98576b8611c25df0c208e51e9a182d95f", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769740369, - "narHash": "sha256-xKPyJoMoXfXpDM5DFDZDsi9PHArf2k5BJjvReYXoFpM=", + "lastModified": 1770843696, + "narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6308c3b21396534d8aaeac46179c14c439a89b8a", + "rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1769786006, - "narHash": "sha256-ax6cH54Nc20QuxlHNC8RMt1P8quMECY4gaACFAdd5ec=", + "lastModified": 1771007332, + "narHash": "sha256-K5Ym2R9oXBBAew2DfIC/dVjjkYo7nB5DbRGb5a3Fkq8=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "eb0588812b041ebbf2645555f2a4df3bcd853c6d", + "rev": "bbc84d335fbbd9b3099d3e40c7469ee57dbd1873", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5caaab9934aec..6ecc79752a079 100644 --- a/flake.nix +++ b/flake.nix @@ -18,13 +18,24 @@ }; lib = pkgs.lib; - toolchain = fenix.packages.${system}.stable.toolchain; + toolchain = fenix.packages.${system}.stable.withComponents [ + "rustc" + "cargo" + "rust-std" + "clippy-preview" + "rust-analyzer-preview" + "rust-src" + ]; + nightlyToolchain = fenix.packages.${system}.latest.withComponents [ + "rustfmt-preview" + ]; in { default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ pkg-config toolchain + nightlyToolchain # test dependencies solc diff --git a/foundryup/foundryup b/foundryup/foundryup index 2e867184de9d2..a63d9b5c9a7ca 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -55,11 +55,6 @@ main() { esac; shift done - # Tempo only distributes a subset of the binaries - if [[ "$FOUNDRYUP_NETWORK" == "tempo" ]]; then - BINS=(forge cast) - fi - CARGO_BUILD_ARGS=(--release) if [ -n "$FOUNDRYUP_JOBS" ]; then diff --git a/testdata/default/cheats/Ed25519.t.sol b/testdata/default/cheats/Ed25519.t.sol new file mode 100644 index 0000000000000..a7cdeae4ce3fc --- /dev/null +++ b/testdata/default/cheats/Ed25519.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "utils/Test.sol"; + +contract Ed25519Test is Test { + function testCreateEd25519Key() public { + bytes32 salt = bytes32(uint256(1)); + (bytes32 publicKey, bytes32 privateKey) = vm.createEd25519Key(salt); + assertTrue(publicKey != bytes32(0), "public key should not be zero"); + assertEq(privateKey, salt, "private key should equal salt"); + } + + function testCreateEd25519KeyDeterministic() public { + bytes32 salt = bytes32(uint256(42)); + (bytes32 pub1, bytes32 priv1) = vm.createEd25519Key(salt); + (bytes32 pub2, bytes32 priv2) = vm.createEd25519Key(salt); + assertEq(pub1, pub2, "same salt should produce same public key"); + assertEq(priv1, priv2, "same salt should produce same private key"); + } + + function testCreateEd25519KeyDifferentSalts() public { + bytes32 salt1 = bytes32(uint256(1)); + bytes32 salt2 = bytes32(uint256(2)); + (bytes32 pub1,) = vm.createEd25519Key(salt1); + (bytes32 pub2,) = vm.createEd25519Key(salt2); + assertTrue(pub1 != pub2, "different salts should produce different public keys"); + } + + function testPublicKeyEd25519() public { + bytes32 salt = bytes32(uint256(123)); + (bytes32 expectedPub, bytes32 privateKey) = vm.createEd25519Key(salt); + bytes32 derivedPub = vm.publicKeyEd25519(privateKey); + assertEq(derivedPub, expectedPub, "derived public key should match created one"); + } + + function testSignAndVerifyEd25519() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (bytes32 publicKey, bytes32 privateKey) = vm.createEd25519Key(salt); + + bytes memory namespace = "test.namespace"; + bytes memory message = "hello world"; + + bytes memory signature = vm.signEd25519(namespace, message, privateKey); + assertEq(signature.length, 64, "signature should be 64 bytes"); + + bool valid = vm.verifyEd25519(signature, namespace, message, publicKey); + assertTrue(valid, "signature should be valid"); + } + + function testVerifyEd25519WrongMessage() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (bytes32 publicKey, bytes32 privateKey) = vm.createEd25519Key(salt); + + bytes memory namespace = "ns"; + bytes memory signature = vm.signEd25519(namespace, "correct message", privateKey); + + bool valid = vm.verifyEd25519(signature, namespace, "wrong message", publicKey); + vm.assertFalse(valid, "signature should not verify with wrong message"); + } + + function testVerifyEd25519NamespaceSeparation() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (bytes32 publicKey, bytes32 privateKey) = vm.createEd25519Key(salt); + + bytes memory message = "message"; + bytes memory signature = vm.signEd25519("namespace.a", message, privateKey); + + bool valid = vm.verifyEd25519(signature, "namespace.b", message, publicKey); + vm.assertFalse(valid, "signature should not verify with different namespace"); + + valid = vm.verifyEd25519(signature, "namespace.a", message, publicKey); + assertTrue(valid, "signature should verify with correct namespace"); + } + + function testVerifyEd25519InvalidSignature() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (bytes32 publicKey,) = vm.createEd25519Key(salt); + + bytes memory invalidSig = new bytes(64); + bool valid = vm.verifyEd25519(invalidSig, "ns", "msg", publicKey); + vm.assertFalse(valid, "zero signature should not verify"); + } + + function testVerifyEd25519WrongSignatureLength() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (bytes32 publicKey,) = vm.createEd25519Key(salt); + + bytes memory shortSig = new bytes(32); + bool valid = vm.verifyEd25519(shortSig, "ns", "msg", publicKey); + vm.assertFalse(valid, "short signature should not verify"); + } + + function testSignEd25519Deterministic() public { + bytes32 salt = bytes32(uint256(0xdeadbeef)); + (, bytes32 privateKey) = vm.createEd25519Key(salt); + + bytes memory namespace = "ns"; + bytes memory message = "msg"; + + bytes memory sig1 = vm.signEd25519(namespace, message, privateKey); + bytes memory sig2 = vm.signEd25519(namespace, message, privateKey); + assertEq(sig1, sig2, "same inputs should produce same signature"); + } +} diff --git a/testdata/default/cheats/ExecuteTransaction.t.sol b/testdata/default/cheats/ExecuteTransaction.t.sol new file mode 100644 index 0000000000000..b86a3b96c42ff --- /dev/null +++ b/testdata/default/cheats/ExecuteTransaction.t.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "utils/Test.sol"; + +contract ExecuteTransactionTest is Test { + function test_revert_not_a_tx() public { + vm._expectCheatcodeRevert("failed to decode RLP-encoded transaction: unexpected string"); + vm.executeTransaction(hex"0102"); + } + + function test_execute_legacy_transfer() public { + vm.fee(1); + vm.chainId(1); + + address from = 0x5316812db67073C4d4af8BB3000C5B86c2877e94; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + + uint256 balance = 1 ether; + uint256 amountSent = 17; + + vm.deal(address(from), balance); + assertEq(address(from).balance, balance); + assertEq(address(to).balance, 0); + + /* + Legacy signed transaction (type 0): + { from: 0x5316812db67073c4d4af8bb3000c5b86c2877e94, to: 0x6fd0a0cff9a87adf51695b40b4fa267855a8f4c6, gas: 200000, gasPrice: 100, value: 17, nonce: 0, chainId: 1 } + */ + vm.executeTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + + // Gas price is set to 0 in isolated execution, so no gas cost deducted. + assertEq(address(from).balance, balance - amountSent); + assertEq(address(to).balance, amountSent); + } + + function test_execute_eip1559_transfer() public { + vm.chainId(1); + + address from = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + + uint256 balance = 1 ether; + uint256 amountSent = 42; + + vm.deal(from, balance); + assertEq(from.balance, balance); + assertEq(to.balance, 0); + + /* + EIP-1559 signed transaction (type 2): + { from: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, to: 0x6fd0a0cff9a87adf51695b40b4fa267855a8f4c6, gas: 21000, maxFeePerGas: 100, maxPriorityFeePerGas: 10, value: 42, nonce: 0, chainId: 1 } + */ + vm.executeTransaction( + hex"02f86201800a64825208946fd0a0cff9a87adf51695b40b4fa267855a8f4c62a80c080a03447a5bb5068bea134c052824759b5dd973aefcf745d0d67a6e2ee6543571f2ca05f3ee9f04a4d3cbc883f5a8b68cb6149fbc47083bb7f4abf644df780f2f11638" + ); + + // Gas price is set to 0 in isolated execution, so no gas cost deducted. + assertEq(from.balance, balance - amountSent); + assertEq(to.balance, amountSent); + } + + function test_execute_erc20_transfer() public { + vm.fee(1); + vm.chainId(1); + + address alice = 0x7ED31830602f9F7419307235c0610Fb262AA0375; + address bob = 0x70CF146aB98ffD5dE24e75dd7423F16181Da8E13; + address charlie = 0xae0900Cf97f8C233c64F7089cEC7d5457215BB8d; + + bytes memory code = + hex"608060405234801561001057600080fd5b50600436106100625760003560e01c8063095ea7b31461006757806323b872dd1461008f57806370a08231146100a257806394bf804d146100d9578063a9059cbb146100ee578063dd62ed3e14610101575b600080fd5b61007a61007536600461051d565b61013a565b60405190151581526020015b60405180910390f35b61007a61009d366004610547565b610152565b6100cb6100b0366004610583565b6001600160a01b031660009081526020819052604090205490565b604051908152602001610086565b6100ec6100e73660046105a5565b610176565b005b61007a6100fc36600461051d565b610184565b6100cb61010f3660046105d1565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b600033610148818585610192565b5060019392505050565b600033610160858285610286565b61016b858585610318565b506001949350505050565b6101808183610489565b5050565b600033610148818585610318565b6001600160a01b0383166101f95760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b60648201526084015b60405180910390fd5b6001600160a01b03821661025a5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b60648201526084016101f0565b6001600160a01b0392831660009081526001602090815260408083209490951682529290925291902055565b6001600160a01b03838116600090815260016020908152604080832093861683529290522054600019811461031257818110156103055760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e636500000060448201526064016101f0565b6103128484848403610192565b50505050565b6001600160a01b03831661037c5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b60648201526084016101f0565b6001600160a01b0382166103de5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b60648201526084016101f0565b6001600160a01b038316600090815260208190526040902054818110156104565760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b60648201526084016101f0565b6001600160a01b039384166000908152602081905260408082209284900390925592909316825291902080549091019055565b6001600160a01b0382166104df5760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f20616464726573730060448201526064016101f0565b6001600160a01b03909116600090815260208190526040902080549091019055565b80356001600160a01b038116811461051857600080fd5b919050565b6000806040838503121561053057600080fd5b61053983610501565b946020939093013593505050565b60008060006060848603121561055c57600080fd5b61056584610501565b925061057360208501610501565b9150604084013590509250925092565b60006020828403121561059557600080fd5b61059e82610501565b9392505050565b600080604083850312156105b857600080fd5b823591506105c860208401610501565b90509250929050565b600080604083850312156105e457600080fd5b6105ed83610501565b91506105c86020840161050156fea2646970667358221220e1fee5cd1c5bbf066a9ce9228e1baf7e7fcb77b5050506c7d614aaf8608b42e364736f6c63430008110033"; + + MyERC20 token = MyERC20(address(uint160(uint256(keccak256(abi.encodePacked("mytoken")))))); + vm.etch(address(token), code); + + token.mint(100, alice); + + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(charlie), 0); + + vm.deal(alice, 10 ether); + + /* + Signed transaction: + { + from: '0x7ED31830602f9F7419307235c0610Fb262AA0375', + to: '0x5bF11839F61EF5ccEEaf1F4153e44df5D02825f7', + value: 0, + data: '0x095ea7b300000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e130000000000000000000000000000000000000000000000000000000000000032', + nonce: 0, + gasPrice: 100, + gasLimit: 200000, + chainId: 1 + } + */ + // alice approves bob for 50 tokens + vm.executeTransaction( + hex"f8a5806483030d40945bf11839f61ef5cceeaf1f4153e44df5d02825f780b844095ea7b300000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e13000000000000000000000000000000000000000000000000000000000000003225a0e25b9ef561d9a413b21755cc0e4bb6e80f2a88a8a52305690956130d612074dfa07bfd418bc2ad3c3f435fa531cdcdc64887f64ed3fb0d347d6b0086e320ad4eb1" + ); + + assertEq(token.allowance(alice, bob), 50); + + // Use the allowance via a normal prank call. + vm.deal(bob, 1 ether); + vm.prank(bob); + token.transferFrom(alice, charlie, 20); + + assertEq(token.balanceOf(alice), 80); + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(charlie), 20); + } + + // Verify state isolation: operations after executeTransaction should work correctly. + function test_execute_then_interact() public { + vm.fee(1); + vm.chainId(1); + + address from = 0x5316812db67073C4d4af8BB3000C5B86c2877e94; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + address random = address(uint160(uint256(keccak256(abi.encodePacked("random"))))); + + uint256 balance = 1 ether; + + vm.deal(address(from), balance); + + vm.executeTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + + assertEq(address(to).balance, 17); + + // Interact with the state after executeTransaction. + uint256 value = 5; + vm.prank(to); + (bool success,) = random.call{value: value}(""); + require(success); + assertEq(address(to).balance, 17 - value); + assertEq(address(random).balance, value); + } +} + +contract MyERC20 { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + function mint(uint256 amount, address to) public { + _mint(to, amount); + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function transfer(address to, uint256 amount) public returns (bool) { + address owner = msg.sender; + _transfer(owner, to, amount); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public returns (bool) { + address owner = msg.sender; + _approve(owner, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + address spender = msg.sender; + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + function _transfer(address from, address to, uint256 amount) internal { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + _balances[to] += amount; + } + } + + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + unchecked { + _balances[account] += amount; + } + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + _allowances[owner][spender] = amount; + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } +} diff --git a/testdata/default/cheats/ExpectRevert.t.sol b/testdata/default/cheats/ExpectRevert.t.sol index 6f3ddf305b302..839d97962aa94 100644 --- a/testdata/default/cheats/ExpectRevert.t.sol +++ b/testdata/default/cheats/ExpectRevert.t.sol @@ -413,6 +413,18 @@ contract ExpectRevertCountWithReverter is Test { } } +contract ExpectRevertPrecompileTest is Test { + /// Test that vm.expectRevert works when the next external call targets a + /// precompile address directly. Precompile calls don't create an interpreter + /// frame (no `initialize_interp`), so depth tracking must account for them. + function testExpectRevertDirectPrecompileCall() public { + // BLAKE2F precompile (0x09) expects exactly 213 bytes of input. + // Calling it with invalid input reverts. + vm.expectRevert(); + address(0x09).call(hex"00"); + } +} + contract ExpectRevertWithErrorTest is Test { /// Ref: function test_f() external { diff --git a/testdata/forge-std-rev b/testdata/forge-std-rev index 213471e492846..8bc543e5cf68b 100644 --- a/testdata/forge-std-rev +++ b/testdata/forge-std-rev @@ -1 +1 @@ -1801b0541f4fda118a10798fd3486bb7051c5dd6 \ No newline at end of file +0844d7e1fc5e60d77b68e469bff60265f236c398 \ No newline at end of file diff --git a/testdata/utils/Vm.sol b/testdata/utils/Vm.sol index e06ed8facda10..e4b11e8d42e64 100644 --- a/testdata/utils/Vm.sol +++ b/testdata/utils/Vm.sol @@ -182,6 +182,7 @@ interface Vm { function copyFile(string calldata from, string calldata to) external returns (uint64 copied); function copyStorage(address from, address to) external; function createDir(string calldata path, bool recursive) external; + function createEd25519Key(bytes32 salt) external pure returns (bytes32 publicKey, bytes32 privateKey); function createFork(string calldata urlOrAlias) external returns (uint256 forkId); function createFork(string calldata urlOrAlias, uint256 blockNumber) external returns (uint256 forkId); function createFork(string calldata urlOrAlias, bytes32 txHash) external returns (uint256 forkId); @@ -247,6 +248,7 @@ interface Vm { function envUint(string calldata name, string calldata delim) external view returns (uint256[] memory value); function etch(address target, bytes calldata newRuntimeBytecode) external; function eth_getLogs(uint256 fromBlock, uint256 toBlock, address target, bytes32[] calldata topics) external view returns (EthGetLogs[] memory logs); + function executeTransaction(bytes calldata rawTx) external returns (bytes memory); function exists(string calldata path) external view returns (bool result); function expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data) external; function expectCallMinGas(address callee, uint256 msgValue, uint64 minGas, bytes calldata data, uint64 count) external; @@ -414,6 +416,7 @@ interface Vm { function promptSecret(string calldata promptText) external returns (string memory input); function promptSecretUint(string calldata promptText) external returns (uint256); function promptUint(string calldata promptText) external returns (uint256); + function publicKeyEd25519(bytes32 privateKey) external pure returns (bytes32 publicKey); function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); function randomAddress() external view returns (address); function randomBool() external view returns (bool); @@ -500,6 +503,7 @@ interface Vm { function signDelegation(address implementation, uint256 privateKey) external returns (SignedDelegation memory signedDelegation); function signDelegation(address implementation, uint256 privateKey, uint64 nonce) external returns (SignedDelegation memory signedDelegation); function signDelegation(address implementation, uint256 privateKey, bool crossChain) external returns (SignedDelegation memory signedDelegation); + function signEd25519(bytes calldata namespace, bytes calldata message, bytes32 privateKey) external pure returns (bytes memory signature); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); function signWithNonceUnsafe(uint256 privateKey, bytes32 digest, uint256 nonce) external pure returns (uint8 v, bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); @@ -559,6 +563,7 @@ interface Vm { function tryFfi(string[] calldata commandInput) external returns (FfiResult memory result); function txGasPrice(uint256 newGasPrice) external; function unixTime() external view returns (uint256 milliseconds); + function verifyEd25519(bytes calldata signature, bytes calldata namespace, bytes calldata message, bytes32 publicKey) external pure returns (bool valid); function warmSlot(address target, bytes32 slot) external; function warp(uint256 newTimestamp) external; function writeFile(string calldata path, string calldata data) external;