diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 000000000..929bd589b --- /dev/null +++ b/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +skip = .git,target,testdata,Cargo.toml,Cargo.lock +ignore-words-list = crate,ser,ratatui,Caf,froms,strat diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ad14cb00..27a208149 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,6 +71,15 @@ jobs: shell: bash run: ./.github/scripts/format.sh --check + codespell: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@v2 + with: + skip: "*.json" + crate-checks: runs-on: ubuntu-22.04-github-hosted-16core timeout-minutes: 60 diff --git a/.gitignore b/.gitignore index 19f666e45..5b61e3202 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_STORE /target out/ +snapshots/ out.json .idea .vscode diff --git a/Cargo.lock b/Cargo.lock index cf76344a2..89c2cd439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,21 +108,23 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", "alloy-serde", + "auto_impl", "c-kzg", + "derive_more 1.0.0", "serde", ] [[package]] name = "alloy-contract" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -189,8 +191,8 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -206,8 +208,8 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "alloy-serde", @@ -228,8 +230,8 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -241,8 +243,8 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-eips", @@ -261,9 +263,10 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ + "alloy-consensus", "alloy-eips", "alloy-primitives", "alloy-serde", @@ -304,8 +307,8 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-chains", "alloy-consensus", @@ -342,8 +345,8 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -382,8 +385,8 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -406,9 +409,10 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ + "alloy-primitives", "alloy-rpc-types-anvil", "alloy-rpc-types-engine", "alloy-rpc-types-eth", @@ -420,8 +424,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "alloy-serde", @@ -430,8 +434,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-eips", @@ -442,12 +446,13 @@ dependencies = [ "jsonwebtoken 9.3.0", "rand 0.8.5", "serde", + "strum", ] [[package]] name = "alloy-rpc-types-eth" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-eips", @@ -456,9 +461,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types", - "cfg-if 1.0.0", "derive_more 1.0.0", - "hashbrown 0.14.5", "itertools 0.13.0", "serde", "serde_json", @@ -466,8 +469,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -479,8 +482,8 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -490,8 +493,8 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-primitives", "serde", @@ -500,8 +503,8 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -515,8 +518,8 @@ dependencies = [ [[package]] name = "alloy-signer-aws" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-network", @@ -532,8 +535,8 @@ dependencies = [ [[package]] name = "alloy-signer-gcp" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-network", @@ -549,8 +552,8 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -568,8 +571,8 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-network", @@ -578,7 +581,6 @@ dependencies = [ "async-trait", "coins-bip32 0.12.0", "coins-bip39 0.12.0", - "elliptic-curve 0.13.8", "eth-keystore", "k256 0.13.4", "rand 0.8.5", @@ -587,8 +589,8 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-consensus", "alloy-network", @@ -676,8 +678,8 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-json-rpc", "base64 0.22.1", @@ -694,8 +696,8 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -708,8 +710,8 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -728,8 +730,8 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "0.3.6" -source = "git+https://github.com/Karrq/alloy?rev=d016019#d016019ce94b2530b426f596831a099af933d10b" +version = "0.4.2" +source = "git+https://github.com/Karrq/alloy?branch=zksync-v0.4.2#36f6a2ef91341075338bddcfa14cfb5bd5203ef8" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -738,21 +740,20 @@ dependencies = [ "rustls 0.23.15", "serde_json", "tokio", - "tokio-tungstenite 0.23.1", + "tokio-tungstenite 0.24.0", "tracing", "ws_stream_wasm", ] [[package]] name = "alloy-trie" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a46c9c4fdccda7982e7928904bd85fe235a0404ee3d7e197fff13d61eac8b4f" +checksum = "e9703ce68b97f8faae6f7739d1e003fc97621b856953cbcdbb2b515743f23288" dependencies = [ "alloy-primitives", "alloy-rlp", "derive_more 1.0.0", - "hashbrown 0.14.5", "nybbles", "serde", "smallvec", @@ -787,12 +788,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "ansiterm" version = "0.12.2" @@ -882,7 +877,6 @@ dependencies = [ "anvil-rpc", "anvil-server", "async-trait", - "auto_impl", "axum", "bytes", "chrono", @@ -902,6 +896,7 @@ dependencies = [ "hyper 1.5.0", "itertools 0.13.0", "k256 0.13.4", + "op-alloy-consensus", "op-alloy-rpc-types", "parking_lot 0.12.3", "rand 0.8.5", @@ -936,6 +931,7 @@ dependencies = [ "bytes", "foundry-common", "foundry-evm", + "op-alloy-consensus", "rand 0.8.5", "revm", "serde", @@ -2034,6 +2030,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling 0.20.10", + "ident_case", + "proc-macro2 1.0.88", + "quote 1.0.37", + "syn 2.0.79", +] + [[package]] name = "boojum" version = "0.30.1" @@ -2276,9 +2295,63 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +version = "0.0.2" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-contract", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-json-rpc", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-types", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-sol-types", + "alloy-transport", + "anvil", + "async-trait", + "aws-sdk-kms", + "chrono", + "clap", + "clap_complete", + "clap_complete_fig", + "comfy-table", + "divan", + "dunce", + "evmole", + "eyre", + "foundry-block-explorers", + "foundry-cli", + "foundry-common", + "foundry-compilers", + "foundry-config", + "foundry-evm", + "foundry-test-utils", + "foundry-wallets", + "foundry-zksync-core", + "futures 0.3.31", + "indicatif", + "itertools 0.13.0", + "rand 0.8.5", + "rayon", + "regex", + "rpassword", + "semver 1.0.23", + "serde", + "serde_json", + "tempfile", + "tikv-jemallocator", + "tokio", + "tracing", + "vergen", + "yansi 1.0.1", + "zksync-web3-rs", +] [[package]] name = "castaway" @@ -2348,7 +2421,6 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types", "clap", - "criterion", "dirs 5.0.1", "eyre", "forge-fmt", @@ -2631,6 +2703,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "cliclack" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a80570d35684e725e9d2d4aaaf32bc0cbfcfb8539898f9afea3da0d2e5189e4" +dependencies = [ + "console", + "indicatif", + "once_cell", + "strsim 0.11.1", + "textwrap", + "zeroize", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -2869,6 +2955,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console" version = "0.15.8" @@ -3001,44 +3093,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "futures 0.3.31", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "tokio", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam" version = "0.8.4" @@ -3595,6 +3649,31 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "divan" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d567df2c9c2870a43f3f2bd65aaeb18dbce1c18f217c3e564b4fbaeb3ee56c" +dependencies = [ + "cfg-if 1.0.0", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2" +dependencies = [ + "proc-macro2 1.0.88", + "quote 1.0.37", + "syn 2.0.79", +] + [[package]] name = "doctest-file" version = "1.0.0" @@ -4598,7 +4677,6 @@ dependencies = [ "clap_complete_fig", "clearscreen", "comfy-table", - "criterion", "dialoguer", "dunce", "ethers-contract-abigen", @@ -4640,14 +4718,13 @@ dependencies = [ "regex", "reqwest 0.12.8", "revm-inspectors", - "rustc-hash 2.0.0", "semver 1.0.23", "serde", "serde_json", "similar", "similar-asserts", "solang-parser", - "soldeer", + "soldeer-commands", "strum", "svm-rs 0.5.8", "tempfile", @@ -4673,7 +4750,6 @@ name = "forge-doc" version = "0.0.2" dependencies = [ "alloy-primitives", - "auto_impl", "derive_more 1.0.0", "eyre", "forge-fmt", @@ -4728,6 +4804,7 @@ dependencies = [ "dialoguer", "dunce", "eyre", + "forge-script-sequence", "forge-verify", "foundry-cheatcodes", "foundry-cli", @@ -4749,11 +4826,29 @@ dependencies = [ "serde", "serde_json", "tempfile", + "tokio", "tracing", "yansi 1.0.1", "zksync-web3-rs", ] +[[package]] +name = "forge-script-sequence" +version = "0.0.2" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types", + "eyre", + "foundry-common", + "foundry-compilers", + "foundry-config", + "foundry-zksync-core", + "revm-inspectors", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "forge-sol-macro-gen" version = "0.0.2" @@ -4817,7 +4912,7 @@ dependencies = [ [[package]] name = "foundry-block-explorers" version = "0.7.3" -source = "git+https://github.com/Moonsong-Labs/block-explorers?branch=zksync-v0.7.3#9ebec2f71f0ee739f4ec4fb21df4d301aeb61936" +source = "git+https://github.com/Moonsong-Labs/block-explorers?branch=zksync-v0.7.3#ef81bf5a733517de496ae1d14024ea9b4fcd4d3c" dependencies = [ "alloy-chains", "alloy-json-abi", @@ -4831,67 +4926,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "foundry-cast" -version = "0.0.2" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-contract", - "alloy-dyn-abi", - "alloy-json-abi", - "alloy-json-rpc", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rlp", - "alloy-rpc-types", - "alloy-serde", - "alloy-signer", - "alloy-signer-local", - "alloy-sol-types", - "alloy-transport", - "anvil", - "async-trait", - "aws-sdk-kms", - "chrono", - "clap", - "clap_complete", - "clap_complete_fig", - "comfy-table", - "criterion", - "dunce", - "evm-disassembler", - "evmole", - "eyre", - "foundry-block-explorers", - "foundry-cli", - "foundry-common", - "foundry-compilers", - "foundry-config", - "foundry-evm", - "foundry-test-utils", - "foundry-wallets", - "foundry-zksync-core", - "futures 0.3.31", - "indicatif", - "itertools 0.13.0", - "rand 0.8.5", - "rayon", - "regex", - "rpassword", - "semver 1.0.23", - "serde", - "serde_json", - "tempfile", - "tikv-jemallocator", - "tokio", - "tracing", - "vergen", - "yansi 1.0.1", - "zksync-web3-rs", -] - [[package]] name = "foundry-cheatcodes" version = "0.0.2" @@ -4918,6 +4952,7 @@ dependencies = [ "foundry-compilers", "foundry-config", "foundry-evm-core", + "foundry-evm-traces", "foundry-wallets", "foundry-zksync-compiler", "foundry-zksync-core", @@ -4931,7 +4966,7 @@ dependencies = [ "proptest", "rand 0.8.5", "revm", - "rustc-hash 2.0.0", + "revm-inspectors", "semver 1.0.23", "serde_json", "thiserror", @@ -5027,12 +5062,10 @@ dependencies = [ "foundry-common-fmt", "foundry-compilers", "foundry-config", - "foundry-linking", "foundry-zksync-compiler", "globset", "num-format", "reqwest 0.12.8", - "rustc-hash 2.0.0", "semver 1.0.23", "serde", "serde_json", @@ -5067,8 +5100,8 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5107,8 +5140,8 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -5117,8 +5150,8 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5140,8 +5173,8 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5154,8 +5187,8 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-zksolc" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5175,14 +5208,13 @@ dependencies = [ [[package]] name = "foundry-compilers-core" -version = "0.11.1" -source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.1#e3f44379f6bfb3c50e3a69acbeaa8f6da0cb4aae" +version = "0.11.4" +source = "git+https://github.com/Moonsong-Labs/compilers?branch=zksync-v0.11.4#04cb9bd38c9dbd6388488a0d6cd9377db1e44a44" dependencies = [ "alloy-primitives", "cfg-if 1.0.0", "dunce", "fs_extra", - "memmap2", "once_cell", "path-slash", "regex", @@ -5229,6 +5261,7 @@ dependencies = [ "toml_edit 0.22.22", "tracing", "walkdir", + "yansi 1.0.1", ] [[package]] @@ -5240,6 +5273,7 @@ dependencies = [ "eyre", "foundry-common", "foundry-compilers", + "foundry-evm-core", "foundry-evm-traces", "ratatui", "revm", @@ -5287,7 +5321,6 @@ dependencies = [ "foundry-macros", "foundry-test-utils", "itertools 0.13.0", - "rustc-hash 2.0.0", ] [[package]] @@ -5317,7 +5350,6 @@ dependencies = [ "parking_lot 0.12.3", "revm", "revm-inspectors", - "rustc-hash 2.0.0", "serde", "serde_json", "thiserror", @@ -5336,7 +5368,6 @@ dependencies = [ "foundry-evm-core", "rayon", "revm", - "rustc-hash 2.0.0", "semver 1.0.23", "tracing", ] @@ -5390,7 +5421,6 @@ dependencies = [ "rayon", "revm", "revm-inspectors", - "rustc-hash 2.0.0", "serde", "solang-parser", "tempfile", @@ -5400,8 +5430,8 @@ dependencies = [ [[package]] name = "foundry-fork-db" -version = "0.3.1" -source = "git+https://github.com/Moonsong-Labs/foundry-zksync-fork-db?branch=zksync-v0.3.1#3bd8ea2c69cd51991ecf5513444d0d1425b07de3" +version = "0.4.0" +source = "git+https://github.com/Moonsong-Labs/foundry-zksync-fork-db?branch=zksync-v0.4.0#183df1a1865ff9af3cb8e3a1675e1bafe0bca69d" dependencies = [ "alloy-primitives", "alloy-provider", @@ -5412,7 +5442,6 @@ dependencies = [ "futures 0.3.31", "parking_lot 0.12.3", "revm", - "rustc-hash 2.0.0", "serde", "serde_json", "thiserror", @@ -6374,7 +6403,6 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", - "serde", ] [[package]] @@ -8361,7 +8389,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.88", "quote 1.0.37", "syn 2.0.79", @@ -8410,17 +8438,11 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "oorandom" -version = "11.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" - [[package]] name = "op-alloy-consensus" -version = "0.2.12" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21aad1fbf80d2bcd7406880efc7ba109365f44bbb72896758ddcbfa46bf1592c" +checksum = "c4f7f318f885db6e1455370ca91f74b7faed152c8142f6418f0936d606e582ff" dependencies = [ "alloy-consensus", "alloy-eips", @@ -8434,17 +8456,16 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.2.12" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281fbfc2198b7c0c16457d6524f83d192662bc9f3df70f24c3038d4521616df" +checksum = "547d29c5ab957ff32e14edddb93652dad748d2ef6cbe4b0fe8615ce06b0a3ddb" dependencies = [ + "alloy-consensus", "alloy-eips", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", "alloy-serde", - "cfg-if 1.0.0", - "hashbrown 0.14.5", "op-alloy-consensus", "serde", "serde_json", @@ -9140,34 +9161,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "portable-atomic" version = "1.9.0" @@ -10230,9 +10223,9 @@ dependencies = [ [[package]] name = "revm-inspectors" -version = "0.7.7" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8e3bae0d5c824da0ac883e2521c5e83870d6521eeeccd4ee54266aa3cc1a51" +checksum = "43c44af0bf801f48d25f7baf25cf72aff4c02d610f83b428175228162fef0246" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -10263,6 +10256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3198c06247e8d4ad0d1312591edf049b0de4ddffa9fecb625c318fd67db8639b" dependencies = [ "aurora-engine-modexp", + "blst", "c-kzg", "cfg-if 1.0.0", "k256 0.13.4", @@ -11592,6 +11586,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "snapbox" version = "0.6.18" @@ -11658,24 +11658,39 @@ dependencies = [ ] [[package]] -name = "soldeer" -version = "0.3.4" +name = "soldeer-commands" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ed763c2bb43241ca0fb6c00feea54187895b7f4eb1090654fbf82807127369" +checksum = "8daf7e07f2b6002f8410811915a2f6142f2d1084764dd88cba3f4ebf22232975" dependencies = [ - "chrono", "clap", + "cliclack", + "derive_more 1.0.0", + "email-address-parser", + "rayon", + "soldeer-core", +] + +[[package]] +name = "soldeer-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea249d0281f3755c3c2b095ad94a554a782cc29138f46c407b8080cfd3918996" +dependencies = [ + "bon", + "chrono", + "cliclack", "const-hex", + "derive_more 1.0.0", "dunce", - "email-address-parser", - "futures 0.3.31", "home", "ignore", "path-slash", + "rayon", "regex", "reqwest 0.12.8", - "rpassword", "sanitize-filename", + "semver 1.0.23", "serde", "serde_json", "sha2 0.10.8", @@ -11683,7 +11698,6 @@ dependencies = [ "tokio", "toml_edit 0.22.22", "uuid 1.11.0", - "yansi 1.0.1", "zip 2.2.0", "zip-extract", ] @@ -12316,6 +12330,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.1.14", +] + [[package]] name = "thiserror" version = "1.0.64" @@ -12426,16 +12451,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.8.0" @@ -12552,9 +12567,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", @@ -12562,20 +12577,8 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", - "tungstenite 0.23.0", - "webpki-roots 0.26.6", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "tokio", "tungstenite 0.24.0", + "webpki-roots 0.26.6", ] [[package]] @@ -12976,26 +12979,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "tungstenite" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.1.0", - "httparse", - "log", - "rand 0.8.5", - "rustls 0.23.15", - "rustls-pki-types", - "sha1", - "thiserror", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.24.0" @@ -13009,6 +12992,8 @@ dependencies = [ "httparse", "log", "rand 0.8.5", + "rustls 0.23.15", + "rustls-pki-types", "sha1", "thiserror", "utf-8", @@ -13095,6 +13080,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index 167503ca3..af3a4915a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/evm/traces/", "crates/fmt/", "crates/forge/", + "crates/script-sequence/", "crates/macros/", "crates/test-utils/", ] @@ -150,6 +151,7 @@ forge-fmt = { path = "crates/fmt" } forge-verify = { path = "crates/verify" } forge-script = { path = "crates/script" } forge-sol-macro-gen = { path = "crates/sol-macro-gen" } +forge-script-sequence = { path = "crates/script-sequence" } foundry-cheatcodes = { path = "crates/cheatcodes" } foundry-cheatcodes-spec = { path = "crates/cheatcodes/spec" } foundry-cheatcodes-common = { path = "crates/cheatcodes/common" } @@ -177,57 +179,61 @@ foundry-zksync-inspectors = { path = "crates/zksync/inspectors" } # foundry-compilers = { version = "0.11.1", default-features = false } # foundry-fork-db = "0.3" foundry-block-explorers = { git = "https://github.com/Moonsong-Labs/block-explorers", branch = "zksync-v0.7.3", default-features = false } -foundry-compilers = { git = "https://github.com/Moonsong-Labs/compilers", branch = "zksync-v0.11.1" } -foundry-fork-db = { git = "https://github.com/Moonsong-Labs/foundry-zksync-fork-db", branch = "zksync-v0.3.1" } +foundry-compilers = { git = "https://github.com/Moonsong-Labs/compilers", branch = "zksync-v0.11.4" } +foundry-fork-db = { git = "https://github.com/Moonsong-Labs/foundry-zksync-fork-db", branch = "zksync-v0.4.0" } solang-parser = "=0.3.3" ## revm -# no default features to avoid c-kzg -revm = { version = "14.0.2", default-features = false } +revm = { version = "14.0.3", default-features = false } revm-primitives = { version = "10.0.0", default-features = false } -revm-inspectors = { version = "0.7", features = ["serde"] } +revm-inspectors = { version = "0.8.0", features = ["serde"] } ## ethers ethers-contract-abigen = { version = "2.0.14", default-features = false } ## alloy -alloy-consensus = { version = "0.3.6", default-features = false } -alloy-contract = { version = "0.3.6", default-features = false } -alloy-eips = { version = "0.3.6", default-features = false } -alloy-genesis = { version = "0.3.6", default-features = false } -alloy-json-rpc = { version = "0.3.6", default-features = false } -alloy-network = { version = "0.3.6", default-features = false } -alloy-provider = { version = "0.3.6", default-features = false } -alloy-pubsub = { version = "0.3.6", default-features = false } -alloy-rpc-client = { version = "0.3.6", default-features = false } -alloy-rpc-types = { version = "0.3.6", default-features = true } -alloy-serde = { version = "0.3.6", default-features = false } -alloy-signer = { version = "0.3.6", default-features = false } -alloy-signer-aws = { version = "0.3.6", default-features = false } -alloy-signer-gcp = { version = "0.3.6", default-features = false } -alloy-signer-ledger = { version = "0.3.6", default-features = false } -alloy-signer-local = { version = "0.3.6", default-features = false } -alloy-signer-trezor = { version = "0.3.6", default-features = false } -alloy-transport = { version = "0.3.6", default-features = false } -alloy-transport-http = { version = "0.3.6", default-features = false } -alloy-transport-ipc = { version = "0.3.6", default-features = false } -alloy-transport-ws = { version = "0.3.6", default-features = false } +alloy-consensus = { version = "0.4.2", default-features = false } +alloy-contract = { version = "0.4.2", default-features = false } +alloy-eips = { version = "0.4.2", default-features = false } +alloy-genesis = { version = "0.4.2", default-features = false } +alloy-json-rpc = { version = "0.4.2", default-features = false } +alloy-network = { version = "0.4.2", default-features = false } +alloy-provider = { version = "0.4.2", default-features = false } +alloy-pubsub = { version = "0.4.2", default-features = false } +alloy-rpc-client = { version = "0.4.2", default-features = false } +alloy-rpc-types = { version = "0.4.2", default-features = true } +alloy-serde = { version = "0.4.2", default-features = false } +alloy-signer = { version = "0.4.2", default-features = false } +alloy-signer-aws = { version = "0.4.2", default-features = false } +alloy-signer-gcp = { version = "0.4.2", default-features = false } +alloy-signer-ledger = { version = "0.4.2", default-features = false } +alloy-signer-local = { version = "0.4.2", default-features = false } +alloy-signer-trezor = { version = "0.4.2", default-features = false } +alloy-transport = { version = "0.4.2", default-features = false } +alloy-transport-http = { version = "0.4.2", default-features = false } +alloy-transport-ipc = { version = "0.4.2", default-features = false } +alloy-transport-ws = { version = "0.4.2", default-features = false } ## alloy-core -alloy-dyn-abi = "0.8.1" -alloy-json-abi = "0.8.1" -alloy-primitives = { version = "0.8.1", features = ["getrandom", "rand"] } -alloy-sol-macro-expander = "0.8.1" -alloy-sol-macro-input = "0.8.1" -alloy-sol-types = "0.8.1" -syn-solidity = "0.8.1" +alloy-dyn-abi = "0.8.5" +alloy-json-abi = "0.8.5" +alloy-primitives = { version = "0.8.5", features = [ + "getrandom", + "rand", + "map-foldhash", +] } +alloy-sol-macro-expander = "0.8.5" +alloy-sol-macro-input = "0.8.5" +alloy-sol-types = "0.8.5" +syn-solidity = "0.8.5" alloy-chains = "0.1" alloy-rlp = "0.3" -alloy-trie = "0.5.0" +alloy-trie = "0.6.0" -## op-alloy for tests in anvil -op-alloy-rpc-types = "0.2.9" +## op-alloy +op-alloy-rpc-types = "0.3.3" +op-alloy-consensus = "0.3.3" ## zksync era_test_node = { git="https://github.com/matter-labs/era-test-node.git" , rev = "56c4e92693b5dd5ab166e368b066d9169b438855" } @@ -241,13 +247,20 @@ zksync_web3_decl = { git = "https://github.com/matter-labs/zksync-era.git", rev zksync_utils = { git = "https://github.com/matter-labs/zksync-era.git", rev = "cfbcc11be0826e8c55fafa84ae01b2aead25d127" } zksync_contracts = { git = "https://github.com/matter-labs/zksync-era.git", rev = "cfbcc11be0826e8c55fafa84ae01b2aead25d127" } -## misc -async-trait = "0.1" -auto_impl = "1" -walkdir = "2" +# macros proc-macro2 = "1.0.82" quote = "1.0" syn = "2.0" +async-trait = "0.1" +derive_more = { version = "1.0", features = ["full"] } +thiserror = "1" + +# bench +divan = "0.1" + +# misc +auto_impl = "1" +walkdir = "2" prettyplease = "0.2.20" ahash = "0.8" base64 = "0.22" @@ -255,71 +268,103 @@ chrono = { version = "0.4", default-features = false, features = [ "clock", "std", ] } +axum = "0.7" color-eyre = "0.6" -derive_more = { version = "1.0", features = ["full"] } +comfy-table = "7" dunce = "1" evm-disassembler = "0.5" +evmole = "0.5" eyre = "0.6" figment = "0.10" futures = "0.3" +hyper = "1.0" +indexmap = "2.2" itertools = "0.13" jsonpath_lib = "0.3" k256 = "0.13" -parking_lot = "0.12" mesc = "0.3" +num-format = "0.4.4" +parking_lot = "0.12" +proptest = "1" rand = "0.8" -rustc-hash = "2.0" +rayon = "1" +reqwest = { version = "0.12", default-features = false } semver = "1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["arbitrary_precision"] } similar-asserts = "1.5" +soldeer-commands = "=0.4.1" strum = "0.26" -thiserror = "1" +tempfile = "3.10" +tikv-jemallocator = "0.6" +tokio = "1" toml = "0.8" +tower = "0.4" +tower-http = "0.5" tracing = "0.1" tracing-subscriber = "0.3" -vergen = { version = "8", default-features = false } -indexmap = "2.2" -tikv-jemallocator = "0.6" url = "2" -num-format = "0.4.4" +vergen = { version = "8", default-features = false } yansi = { version = "1.0", features = ["detect-tty", "detect-env"] } -tempfile = "3.10" -tokio = "1" -rayon = "1" -evmole = "0.5" -axum = "0.7" -hyper = "1.0" -reqwest = { version = "0.12", default-features = false } -tower = "0.4" -tower-http = "0.5" -# soldeer -soldeer = "=0.3.4" -proptest = "1" -comfy-table = "7" [patch.crates-io] -alloy-consensus = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-contract = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-eips = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-genesis = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-json-rpc = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-network = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-network-primitives = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-provider = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-pubsub = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-rpc-client = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-rpc-types = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-rpc-types-trace = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-rpc-types-eth = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-serde = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer-aws = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer-gcp = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer-ledger = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer-local = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-signer-trezor = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-transport = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-transport-http = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-transport-ipc = { git = "https://github.com/Karrq/alloy", rev = "d016019" } -alloy-transport-ws = { git = "https://github.com/Karrq/alloy", rev = "d016019" } +## alloy-core +# alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" } +# alloy-json-abi = { path = "../../alloy-rs/core/crates/json-abi" } +# alloy-primitives = { path = "../../alloy-rs/core/crates/primitives" } +# alloy-sol-macro = { path = "../../alloy-rs/core/crates/sol-macro" } +# alloy-sol-macro-expander = { path = "../../alloy-rs/core/crates/sol-macro-expander" } +# alloy-sol-macro-input = { path = "../../alloy-rs/core/crates/sol-macro-input" } +# alloy-sol-type-parser = { path = "../../alloy-rs/core/crates/sol-type-parser" } +# alloy-sol-types = { path = "../../alloy-rs/core/crates/sol-types" } +# syn-solidity = { path = "../../alloy-rs/core/crates/syn-solidity" } + +## alloy +# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-aws = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-gcp = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-ledger = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-signer-trezor = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } +# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "7fab7ee" } + +alloy-consensus = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-contract = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-eips = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-genesis = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-json-rpc = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-network = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-network-primitives = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-provider = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-pubsub = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-rpc-client = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-rpc-types = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-rpc-types-trace = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-rpc-types-eth = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-serde = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer-aws = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer-gcp = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer-ledger = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer-local = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-signer-trezor = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-transport = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-transport-http = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-transport-ipc = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } +alloy-transport-ws = { git = "https://github.com/Karrq/alloy", branch = "zksync-v0.4.2" } diff --git a/Dockerfile b/Dockerfile index 648bbbff6..1b4a14ca0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.4 -FROM alpine:3.18 as build-environment +FROM alpine:3.20 as build-environment ARG TARGETARCH WORKDIR /opt @@ -19,7 +19,7 @@ COPY . . RUN git update-index --force-write-index RUN --mount=type=cache,target=/root/.cargo/registry --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/opt/foundry/target \ - source $HOME/.profile && cargo build --release --features foundry-cast/aws-kms,forge/aws-kms \ + source $HOME/.profile && cargo build --release --features cast/aws-kms,forge/aws-kms \ && mkdir out \ && mv target/local/forge out/forge \ && mv target/local/cast out/cast \ @@ -30,9 +30,9 @@ RUN --mount=type=cache,target=/root/.cargo/registry --mount=type=cache,target=/r && strip out/chisel \ && strip out/anvil; -FROM alpine:3.18 as foundry-client +FROM alpine:3.20 as foundry-client -RUN apk add --no-cache linux-headers git clang openssl +RUN apk add --no-cache linux-headers git clang openssl gcompat libstdc++ COPY --from=build-environment /opt/foundry/out/forge /usr/local/bin/forge COPY --from=build-environment /opt/foundry/out/cast /usr/local/bin/cast diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..920e4de2d --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +# Heavily inspired by: +# - Lighthouse: https://github.com/sigp/lighthouse/blob/693886b94176faa4cb450f024696cb69cda2fe58/Makefile +# - Reth: https://github.com/paradigmxyz/reth/blob/1f642353ca083b374851ab355b5d80207b36445c/Makefile +.DEFAULT_GOAL := help + +# Cargo profile for builds. +PROFILE ?= dev + +# List of features to use when building. Can be overridden via the environment. +# No jemalloc on Windows +ifeq ($(OS),Windows_NT) + FEATURES ?= rustls aws-kms cli asm-keccak +else + FEATURES ?= jemalloc rustls aws-kms cli asm-keccak +endif + +##@ Help + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +.PHONY: build +build: ## Build the project. + cargo build --features "$(FEATURES)" --profile "$(PROFILE)" + +##@ Other + +.PHONY: clean +clean: ## Clean the project. + cargo clean + +## Linting + +fmt: ## Run all formatters. + cargo +nightly fmt + ./.github/scripts/format.sh --check + +lint-foundry: + RUSTFLAGS="-Dwarnings" cargo clippy --workspace --all-targets --all-features + +lint-codespell: ensure-codespell + codespell --skip "*.json" + +ensure-codespell: + @if ! command -v codespell &> /dev/null; then \ + echo "codespell not found. Please install it by running the command `pip install codespell` or refer to the following link for more information: https://github.com/codespell-project/codespell" \ + exit 1; \ + fi + +lint: ## Run all linters. + make fmt && \ + make lint-foundry && \ + make lint-codespell + +## Testing + +test-foundry: + cargo nextest run -E 'kind(test) & !test(/issue|forge_std|ext_integration/)' + +test-doc: + cargo test --doc --workspace + +test: ## Run all tests. + make test-foundry && \ + make test-doc + +pr: ## Run all tests and linters in preparation for a PR. + make lint && \ + make test \ No newline at end of file diff --git a/README.md b/README.md index 0feaddc6d..aa90178e4 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,3 @@ See our [contributing guidelines](./CONTRIBUTING.md). ### Foundry ZKsync - [Moonsong Labs](https://moonsonglabs.com/): Implemented [ZKsync crates](./crates/zksync/), and resolved a number of different challenges to enable ZKsync support. - -[foundry-book]: https://book.getfoundry.sh -[foundry-gha]: https://github.com/foundry-rs/foundry-toolchain -[ethers-solc]: https://github.com/gakonst/ethers-rs/tree/master/ethers-solc/ -[vscode-setup]: https://book.getfoundry.sh/config/vscode.html -[shell-setup]: https://book.getfoundry.sh/config/shell-autocompletion.html diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index 77faec1f1..922cb6efc 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -67,6 +67,7 @@ alloy-transport.workspace = true alloy-chains.workspace = true alloy-genesis.workspace = true alloy-trie.workspace = true +op-alloy-consensus.workspace = true # axum related axum.workspace = true @@ -103,7 +104,6 @@ clap = { version = "4", features = [ ], optional = true } clap_complete = { version = "4", optional = true } chrono.workspace = true -auto_impl.workspace = true ctrlc = { version = "3", optional = true } fdlimit = { version = "0.3", optional = true } clap_complete_fig = "4" @@ -122,8 +122,10 @@ alloy-pubsub.workspace = true foundry-test-utils.workspace = true similar-asserts.workspace = true tokio = { workspace = true, features = ["full"] } + op-alloy-rpc-types.workspace = true + [features] default = ["cli", "jemalloc"] cmd = ["clap", "clap_complete", "ctrlc", "anvil-server/clap"] diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index 8c2720c8f..6ea5e5318 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -30,6 +30,7 @@ alloy-eips.workspace = true alloy-consensus = { workspace = true, features = ["k256", "kzg"] } alloy-dyn-abi = { workspace = true, features = ["std", "eip712"] } alloy-trie.workspace = true +op-alloy-consensus.workspace = true serde = { workspace = true, optional = true } serde_json.workspace = true diff --git a/crates/anvil/core/src/eth/block.rs b/crates/anvil/core/src/eth/block.rs index 1b0895dcd..337c5cfd8 100644 --- a/crates/anvil/core/src/eth/block.rs +++ b/crates/anvil/core/src/eth/block.rs @@ -90,16 +90,16 @@ pub struct PartialHeader { pub logs_bloom: Bloom, pub difficulty: U256, pub number: u64, - pub gas_limit: u128, - pub gas_used: u128, + pub gas_limit: u64, + pub gas_used: u64, pub timestamp: u64, pub extra_data: Bytes, pub mix_hash: B256, - pub blob_gas_used: Option, - pub excess_blob_gas: Option, + pub blob_gas_used: Option, + pub excess_blob_gas: Option, pub parent_beacon_block_root: Option, pub nonce: B64, - pub base_fee: Option, + pub base_fee: Option, } impl From
for PartialHeader { @@ -150,7 +150,7 @@ mod tests { difficulty: Default::default(), number: 124u64, gas_limit: Default::default(), - gas_used: 1337u128, + gas_used: 1337u64, timestamp: 0, extra_data: Default::default(), mix_hash: Default::default(), @@ -167,7 +167,7 @@ mod tests { let decoded: Header = Header::decode(&mut encoded.as_ref()).unwrap(); assert_eq!(header, decoded); - header.base_fee_per_gas = Some(12345u128); + header.base_fee_per_gas = Some(12345u64); let encoded = alloy_rlp::encode(&header); let decoded: Header = Header::decode(&mut encoded.as_ref()).unwrap(); @@ -190,8 +190,8 @@ mod tests { logs_bloom: Bloom::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), difficulty: U256::from(2222), number: 0xd05u64, - gas_limit: 0x115cu128, - gas_used: 0x15b3u128, + gas_limit: 0x115cu64, + gas_used: 0x15b3u64, timestamp: 0x1a0au64, extra_data: hex::decode("7788").unwrap().into(), mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), @@ -223,8 +223,8 @@ mod tests { logs_bloom: <[u8; 256]>::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().into(), difficulty: U256::from(2222), number: 0xd05u64, - gas_limit: 0x115cu128, - gas_used: 0x15b3u128, + gas_limit: 0x115cu64, + gas_used: 0x15b3u64, timestamp: 0x1a0au64, extra_data: hex::decode("7788").unwrap().into(), mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), @@ -255,8 +255,8 @@ mod tests { logs_bloom: Bloom::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), difficulty: U256::from(0x020000), number: 1u64, - gas_limit: U256::from(0x016345785d8a0000u128).to::(), - gas_used: U256::from(0x015534).to::(), + gas_limit: U256::from(0x016345785d8a0000u128).to::(), + gas_used: U256::from(0x015534).to::(), timestamp: 0x079e, extra_data: hex::decode("42").unwrap().into(), mix_hash: B256::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 70c62ed56..d53473666 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -522,7 +522,7 @@ pub enum EthRequest { EvmSetTime(U256), /// Serializes the current state (including contracts code, contract's storage, accounts - /// properties, etc.) into a savable data blob + /// properties, etc.) into a saveable data blob #[cfg_attr(feature = "serde", serde(rename = "anvil_dumpState", alias = "hardhat_dumpState"))] DumpState(#[cfg_attr(feature = "serde", serde(default))] Option>>), diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index 128f6e9cd..c31207920 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -1,6 +1,6 @@ //! Transaction related types -use crate::eth::transaction::optimism::{DepositTransaction, DepositTransactionRequest}; +use crate::eth::transaction::optimism::DepositTransaction; use alloy_consensus::{ transaction::{ eip4844::{TxEip4844, TxEip4844Variant, TxEip4844WithSidecar}, @@ -21,6 +21,7 @@ use alloy_rpc_types::{ use alloy_serde::{OtherFields, WithOtherFields}; use bytes::BufMut; use foundry_evm::traces::CallTraceNode; +use op_alloy_consensus::TxDeposit; use revm::{ interpreter::InstructionResult, primitives::{OptimismFields, TxEnv}, @@ -59,14 +60,16 @@ pub fn transaction_request_to_typed( // Special case: OP-stack deposit tx if transaction_type == Some(0x7E) || has_optimism_fields(&other) { - return Some(TypedTransactionRequest::Deposit(DepositTransactionRequest { + let mint = other.get_deserialized::("mint")?.map(|m| m.to::()).ok()?; + + return Some(TypedTransactionRequest::Deposit(TxDeposit { from: from.unwrap_or_default(), source_hash: other.get_deserialized::("sourceHash")?.ok()?, - kind: to.unwrap_or_default(), - mint: other.get_deserialized::("mint")?.ok()?, + to: to.unwrap_or_default(), + mint: Some(mint), value: value.unwrap_or_default(), gas_limit: gas.unwrap_or_default(), - is_system_tx: other.get_deserialized::("isSystemTx")?.ok()?, + is_system_transaction: other.get_deserialized::("isSystemTx")?.ok()?, input: input.into_input().unwrap_or_default(), })); } @@ -128,7 +131,7 @@ pub fn transaction_request_to_typed( })) } // EIP4844 - (Some(3), None, _, _, _, Some(_), Some(_), Some(sidecar), to) => { + (Some(3), None, _, _, _, _, Some(_), Some(sidecar), to) => { let tx = TxEip4844 { nonce: nonce.unwrap_or_default(), max_fee_per_gas: max_fee_per_gas.unwrap_or_default(), @@ -165,7 +168,7 @@ pub enum TypedTransactionRequest { EIP2930(TxEip2930), EIP1559(TxEip1559), EIP4844(TxEip4844Variant), - Deposit(DepositTransactionRequest), + Deposit(TxDeposit), } /// A wrapper for [TypedTransaction] that allows impersonating accounts. @@ -498,7 +501,7 @@ impl PendingTransaction { value: (*value), gas_price: U256::from(*gas_price), gas_priority_fee: None, - gas_limit: *gas_limit as u64, + gas_limit: *gas_limit, access_list: vec![], ..Default::default() } @@ -524,7 +527,7 @@ impl PendingTransaction { value: *value, gas_price: U256::from(*gas_price), gas_priority_fee: None, - gas_limit: *gas_limit as u64, + gas_limit: *gas_limit, access_list: access_list.clone().into(), ..Default::default() } @@ -551,7 +554,7 @@ impl PendingTransaction { value: *value, gas_price: U256::from(*max_fee_per_gas), gas_priority_fee: Some(U256::from(*max_priority_fee_per_gas)), - gas_limit: *gas_limit as u64, + gas_limit: *gas_limit, access_list: access_list.clone().into(), ..Default::default() } @@ -582,7 +585,7 @@ impl PendingTransaction { gas_priority_fee: Some(U256::from(*max_priority_fee_per_gas)), max_fee_per_blob_gas: Some(U256::from(*max_fee_per_blob_gas)), blob_hashes: blob_versioned_hashes.clone(), - gas_limit: *gas_limit as u64, + gas_limit: *gas_limit, access_list: access_list.clone().into(), ..Default::default() } @@ -609,7 +612,7 @@ impl PendingTransaction { value: *value, gas_price: U256::from(*max_fee_per_gas), gas_priority_fee: Some(U256::from(*max_priority_fee_per_gas)), - gas_limit: *gas_limit as u64, + gas_limit: *gas_limit, access_list: access_list.clone().into(), authorization_list: Some(authorization_list.clone().into()), ..Default::default() @@ -637,7 +640,7 @@ impl PendingTransaction { value: *value, gas_price: U256::ZERO, gas_priority_fee: None, - gas_limit: *gas_limit as u64, + gas_limit: { *gas_limit }, access_list: vec![], optimism: OptimismFields { source_hash: Some(*source_hash), @@ -672,7 +675,7 @@ pub enum TypedTransaction { /// This is a function that demotes TypedTransaction to TransactionRequest for greater flexibility /// over the type. /// -/// This function is purely for convience and specific use cases, e.g. RLP encoded transactions +/// This function is purely for convenience and specific use cases, e.g. RLP encoded transactions /// decode to TypedTransactions where the API over TypedTransctions is quite strict. impl TryFrom for TransactionRequest { type Error = ConversionError; @@ -716,7 +719,7 @@ impl TypedTransaction { } } - pub fn gas_limit(&self) -> u128 { + pub fn gas_limit(&self) -> u64 { match self { Self::Legacy(tx) => tx.tx().gas_limit, Self::EIP2930(tx) => tx.tx().gas_limit, @@ -766,20 +769,23 @@ impl TypedTransaction { /// and if the transaction is EIP-4844, the result of (total blob gas cost * max fee per blob /// gas) is also added pub fn max_cost(&self) -> u128 { - let mut max_cost = self.gas_limit().saturating_mul(self.gas_price()); + let mut max_cost = (self.gas_limit() as u128).saturating_mul(self.gas_price()); if self.is_eip4844() { max_cost = max_cost.saturating_add( - self.blob_gas().unwrap_or(0).mul(self.max_fee_per_blob_gas().unwrap_or(0)), + self.blob_gas() + .map(|g| g as u128) + .unwrap_or(0) + .mul(self.max_fee_per_blob_gas().unwrap_or(0)), ) } max_cost } - pub fn blob_gas(&self) -> Option { + pub fn blob_gas(&self) -> Option { match self { - Self::EIP4844(tx) => Some(tx.tx().tx().blob_gas() as u128), + Self::EIP4844(tx) => Some(tx.tx().tx().blob_gas()), _ => None, } } @@ -1003,11 +1009,41 @@ impl TypedTransaction { } } -impl TryFrom for TypedTransaction { +impl TryFrom> for TypedTransaction { type Error = ConversionError; - fn try_from(tx: RpcTransaction) -> Result { - // TODO(sergerad): Handle Arbitrum system transactions? + fn try_from(tx: WithOtherFields) -> Result { + if tx.transaction_type.is_some_and(|t| t == 0x7E) { + let mint = tx + .other + .get_deserialized::("mint") + .ok_or(ConversionError::Custom("MissingMint".to_string()))? + .map_err(|_| ConversionError::Custom("Cannot deserialize mint".to_string()))?; + + let source_hash = tx + .other + .get_deserialized::("sourceHash") + .ok_or(ConversionError::Custom("MissingSourceHash".to_string()))? + .map_err(|_| { + ConversionError::Custom("Cannot deserialize source hash".to_string()) + })?; + + let deposit = DepositTransaction { + nonce: tx.nonce, + is_system_tx: true, + from: tx.from, + kind: tx.to.map(TxKind::Call).unwrap_or(TxKind::Create), + value: tx.value, + gas_limit: tx.gas, + input: tx.input.clone(), + mint, + source_hash, + }; + + return Ok(Self::Deposit(deposit)); + } + + let tx = tx.inner; match tx.transaction_type.unwrap_or_default().try_into()? { TxType::Legacy => { let legacy = TxLegacy { @@ -1226,7 +1262,7 @@ pub struct TransactionEssentials { pub kind: TxKind, pub input: Bytes, pub nonce: u64, - pub gas_limit: u128, + pub gas_limit: u64, pub gas_price: Option, pub max_fee_per_gas: Option, pub max_priority_fee_per_gas: Option, @@ -1249,7 +1285,7 @@ pub struct TransactionInfo { pub exit: InstructionResult, pub out: Option, pub nonce: u64, - pub gas_used: u128, + pub gas_used: u64, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -1257,9 +1293,9 @@ pub struct TransactionInfo { pub struct DepositReceipt { #[serde(flatten)] pub inner: ReceiptWithBloom, - #[serde(default, with = "alloy_serde::num::u64_opt_via_ruint")] + #[serde(default, with = "alloy_serde::quantity::opt")] pub deposit_nonce: Option, - #[serde(default, with = "alloy_serde::num::u64_opt_via_ruint")] + #[serde(default, with = "alloy_serde::quantity::opt")] pub deposit_receipt_version: Option, } @@ -1660,7 +1696,7 @@ mod tests { let tx = TxLegacy { nonce: 2u64, gas_price: 1000000000u128, - gas_limit: 100000u128, + gas_limit: 100000, to: TxKind::Call(Address::from_slice( &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..], )), diff --git a/crates/anvil/core/src/eth/transaction/optimism.rs b/crates/anvil/core/src/eth/transaction/optimism.rs index 6cc7bfa5a..6bb4b2abb 100644 --- a/crates/anvil/core/src/eth/transaction/optimism.rs +++ b/crates/anvil/core/src/eth/transaction/optimism.rs @@ -1,276 +1,25 @@ -use alloy_consensus::{SignableTransaction, Signed, Transaction}; -use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, TxKind, B256, U256}; -use alloy_rlp::{ - length_of_length, Decodable, Encodable, Error as DecodeError, Header as RlpHeader, -}; -use bytes::BufMut; +use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; +use alloy_rlp::{Decodable, Encodable, Error as DecodeError, Header as RlpHeader}; +use op_alloy_consensus::TxDeposit; use serde::{Deserialize, Serialize}; -use std::mem; pub const DEPOSIT_TX_TYPE_ID: u8 = 0x7E; -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DepositTransactionRequest { - pub source_hash: B256, - pub from: Address, - pub kind: TxKind, - pub mint: U256, - pub value: U256, - pub gas_limit: u128, - pub is_system_tx: bool, - pub input: Bytes, -} - -impl DepositTransactionRequest { - pub fn hash(&self) -> B256 { - let mut encoded = Vec::new(); - encoded.put_u8(DEPOSIT_TX_TYPE_ID); - self.encode(&mut encoded); - - B256::from_slice(alloy_primitives::keccak256(encoded).as_slice()) - } - - /// Encodes only the transaction's fields into the desired buffer, without a RLP header. - pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { - self.source_hash.encode(out); - self.from.encode(out); - self.kind.encode(out); - self.mint.encode(out); - self.value.encode(out); - self.gas_limit.encode(out); - self.is_system_tx.encode(out); - self.input.encode(out); - } - - /// Calculates the length of the RLP-encoded transaction's fields. - pub(crate) fn fields_len(&self) -> usize { - let mut len = 0; - len += self.source_hash.length(); - len += self.from.length(); - len += self.kind.length(); - len += self.mint.length(); - len += self.value.length(); - len += self.gas_limit.length(); - len += self.is_system_tx.length(); - len += self.input.length(); - len - } - - /// Decodes the inner [DepositTransactionRequest] fields from RLP bytes. - /// - /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following - /// RLP fields in the following order: - /// - /// - `source_hash` - /// - `from` - /// - `kind` - /// - `mint` - /// - `value` - /// - `gas_limit` - /// - `is_system_tx` - /// - `input` - pub fn decode_inner(buf: &mut &[u8]) -> Result { - Ok(Self { - source_hash: Decodable::decode(buf)?, - from: Decodable::decode(buf)?, - kind: Decodable::decode(buf)?, - mint: Decodable::decode(buf)?, - value: Decodable::decode(buf)?, - gas_limit: Decodable::decode(buf)?, - is_system_tx: Decodable::decode(buf)?, - input: Decodable::decode(buf)?, - }) - } - - /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating - /// hash that for eip2718 does not require rlp header - pub(crate) fn encode_with_signature( - &self, - signature: &Signature, - out: &mut dyn alloy_rlp::BufMut, - ) { - let payload_length = self.fields_len() + signature.rlp_vrs_len(); - let header = alloy_rlp::Header { list: true, payload_length }; - header.encode(out); - self.encode_fields(out); - signature.write_rlp_vrs(out); - } - - /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. - pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { - let payload_length = self.fields_len() + signature.rlp_vrs_len(); - // 'transaction type byte length' + 'header length' + 'payload length' - 1 + length_of_length(payload_length) + payload_length - } - - /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. - pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { - let len = self.payload_len_with_signature_without_header(signature); - length_of_length(len) + len - } - - /// Get transaction type - pub(crate) const fn tx_type(&self) -> u8 { - DEPOSIT_TX_TYPE_ID - } - - /// Calculates a heuristic for the in-memory size of the [DepositTransaction] transaction. - pub fn size(&self) -> usize { - mem::size_of::() + // source_hash - mem::size_of::
() + // from - self.kind.size() + // to - mem::size_of::() + // mint - mem::size_of::() + // value - mem::size_of::() + // gas_limit - mem::size_of::() + // is_system_transaction - self.input.len() // input - } - - /// Encodes the legacy transaction in RLP for signing. - pub(crate) fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - out.put_u8(self.tx_type()); - alloy_rlp::Header { list: true, payload_length: self.fields_len() }.encode(out); - self.encode_fields(out); - } - - /// Outputs the length of the signature RLP encoding for the transaction. - pub(crate) fn payload_len_for_signature(&self) -> usize { - let payload_length = self.fields_len(); - // 'transaction type byte length' + 'header length' + 'payload length' - 1 + length_of_length(payload_length) + payload_length - } - - fn encoded_len_with_signature(&self, signature: &Signature) -> usize { - // this counts the tx fields and signature fields - let payload_length = self.fields_len() + signature.rlp_vrs_len(); - - // this counts: - // * tx type byte - // * inner header length - // * inner payload length - 1 + alloy_rlp::Header { list: true, payload_length }.length() + payload_length - } -} - -impl Transaction for DepositTransactionRequest { - fn input(&self) -> &[u8] { - &self.input - } - - /// Get `to`. - fn to(&self) -> TxKind { - self.kind - } - - /// Get `value`. - fn value(&self) -> U256 { - self.value - } - - /// Get `chain_id`. - fn chain_id(&self) -> Option { - None - } - - /// Get `nonce`. - fn nonce(&self) -> u64 { - u64::MAX - } - - /// Get `gas_limit`. - fn gas_limit(&self) -> u128 { - self.gas_limit - } - - /// Get `gas_price`. - fn gas_price(&self) -> Option { - None - } - - fn ty(&self) -> u8 { - 0x7E - } - - // Below fields are not found in a `DepositTransactionRequest` - - fn access_list(&self) -> Option<&alloy_rpc_types::AccessList> { - None - } - - fn authorization_list(&self) -> Option<&[revm::primitives::SignedAuthorization]> { - None - } - - fn blob_versioned_hashes(&self) -> Option<&[B256]> { - None - } - - fn effective_tip_per_gas(&self, _base_fee: u64) -> Option { - None - } - - fn max_fee_per_blob_gas(&self) -> Option { - None - } - - fn max_fee_per_gas(&self) -> u128 { - 0 - } - - fn max_priority_fee_per_gas(&self) -> Option { - None - } - - fn priority_fee_or_price(&self) -> u128 { - 0 - } -} - -impl SignableTransaction for DepositTransactionRequest { - fn set_chain_id(&mut self, _chain_id: ChainId) {} - - fn payload_len_for_signature(&self) -> usize { - self.payload_len_for_signature() - } - - fn into_signed(self, signature: Signature) -> Signed { - let mut buf = Vec::with_capacity(self.encoded_len_with_signature(&signature)); - self.encode_with_signature(&signature, &mut buf); - let hash = keccak256(&buf); - - // Drop any v chain id value to ensure the signature format is correct at the time of - // combination for an EIP-4844 transaction. V should indicate the y-parity of the - // signature. - Signed::new_unchecked(self, signature.with_parity_bool(), hash) - } - - fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - self.encode_for_signing(out); - } -} - -impl From for DepositTransactionRequest { +impl From for TxDeposit { fn from(tx: DepositTransaction) -> Self { Self { from: tx.from, source_hash: tx.source_hash, - kind: tx.kind, - mint: tx.mint, + to: tx.kind, + mint: Some(tx.mint.to::()), value: tx.value, gas_limit: tx.gas_limit, - is_system_tx: tx.is_system_tx, + is_system_transaction: tx.is_system_tx, input: tx.input, } } } -impl Encodable for DepositTransactionRequest { - fn encode(&self, out: &mut dyn bytes::BufMut) { - RlpHeader { list: true, payload_length: self.fields_len() }.encode(out); - self.encode_fields(out); - } -} - /// An op-stack deposit transaction. /// See #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -281,7 +30,7 @@ pub struct DepositTransaction { pub kind: TxKind, pub mint: U256, pub value: U256, - pub gas_limit: u128, + pub gas_limit: u64, pub is_system_tx: bool, pub input: Bytes, } @@ -443,23 +192,4 @@ mod tests { assert_eq!(tx, decoded_tx); } - - #[test] - fn test_tx_request_hash_equals_tx_hash() { - let tx = DepositTransaction { - nonce: 0, - source_hash: B256::default(), - from: Address::default(), - kind: TxKind::Call(Address::default()), - mint: U256::from(100), - value: U256::from(100), - gas_limit: 50000, - is_system_tx: false, - input: Bytes::default(), - }; - - let tx_request = DepositTransactionRequest::from(tx.clone()); - - assert_eq!(tx.hash(), tx_request.hash()); - } } diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index 3b7778a72..13182686a 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -254,6 +254,7 @@ impl NodeArgs { .fork_compute_units_per_second(compute_units_per_second) .with_eth_rpc_url(self.evm_opts.fork_url.map(|fork| fork.url)) .with_base_fee(self.evm_opts.block_base_fee_per_gas) + .disable_min_priority_fee(self.evm_opts.disable_min_priority_fee) .with_storage_caching(self.evm_opts.no_storage_caching) .with_server_config(self.server_config) .with_host(self.host) @@ -545,7 +546,11 @@ pub struct AnvilEvmArgs { value_name = "FEE", help_heading = "Environment config" )] - pub block_base_fee_per_gas: Option, + pub block_base_fee_per_gas: Option, + + /// Disable the enforcement of a minimum suggested priority fee. + #[arg(long, visible_alias = "no-priority-fee", help_heading = "Environment config")] + pub disable_min_priority_fee: bool, /// The chain ID. #[arg(long, alias = "chain", help_heading = "Environment config")] @@ -576,7 +581,7 @@ pub struct AnvilEvmArgs { pub memory_limit: Option, /// Enable Alphanet features - #[arg(long, visible_alias = "alphanet")] + #[arg(long, visible_alias = "odyssey")] pub alphanet: bool, } diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index cafdf1695..273dbad89 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -17,9 +17,10 @@ use crate::{ }; use alloy_genesis::Genesis; use alloy_network::AnyNetwork; -use alloy_primitives::{hex, utils::Unit, BlockNumber, TxHash, U256}; +use alloy_primitives::{hex, map::HashMap, utils::Unit, BlockNumber, TxHash, U256}; use alloy_provider::Provider; -use alloy_rpc_types::{BlockNumberOrTag, Transaction}; +use alloy_rpc_types::{Block, BlockNumberOrTag, Transaction}; +use alloy_serde::WithOtherFields; use alloy_signer::Signer; use alloy_signer_local::{ coins_bip39::{English, Mnemonic}, @@ -45,7 +46,6 @@ use rand::thread_rng; use revm::primitives::BlobExcessGasAndPrice; use serde_json::{json, to_writer, Value}; use std::{ - collections::HashMap, fmt::Write as FmtWrite, fs::File, net::{IpAddr, Ipv4Addr}, @@ -59,6 +59,8 @@ use yansi::Paint; pub const NODE_PORT: u16 = 8545; /// Default chain id of the node pub const CHAIN_ID: u64 = 31337; +/// The default gas limit for all transactions +pub const DEFAULT_GAS_LIMIT: u128 = 30_000_000; /// Default mnemonic for dev accounts pub const DEFAULT_MNEMONIC: &str = "test test test test test test test test test test test junk"; @@ -90,13 +92,15 @@ pub struct NodeConfig { /// Chain ID of the EVM chain pub chain_id: Option, /// Default gas limit for all txs - pub gas_limit: u128, + pub gas_limit: Option, /// If set to `true`, disables the block gas limit pub disable_block_gas_limit: bool, /// Default gas price for all txs pub gas_price: Option, /// Default base fee - pub base_fee: Option, + pub base_fee: Option, + /// If set to `true`, disables the enforcement of a minimum suggested priority fee + pub disable_min_priority_fee: bool, /// Default blob excess gas and price pub blob_excess_gas_and_price: Option, /// The hardfork to use @@ -303,7 +307,20 @@ Gas Limit {} "#, - self.gas_limit.green() + { + if self.disable_block_gas_limit { + "Disabled".to_string() + } else { + self.gas_limit.map(|l| l.to_string()).unwrap_or_else(|| { + if self.fork_choice.is_some() { + "Forked".to_string() + } else { + DEFAULT_GAS_LIMIT.to_string() + } + }) + } + } + .green() ); let _ = write!( @@ -338,6 +355,13 @@ Genesis Timestamp wallet_description.insert("mnemonic".to_string(), phrase); }; + let gas_limit = match self.gas_limit { + // if we have a disabled flag we should max out the limit + Some(_) | None if self.disable_block_gas_limit => Some(u64::MAX.to_string()), + Some(limit) => Some(limit.to_string()), + _ => None, + }; + if let Some(fork) = fork { json!({ "available_accounts": available_accounts, @@ -349,7 +373,7 @@ Genesis Timestamp "wallet": wallet_description, "base_fee": format!("{}", self.get_base_fee()), "gas_price": format!("{}", self.get_gas_price()), - "gas_limit": format!("{}", self.gas_limit), + "gas_limit": gas_limit, }) } else { json!({ @@ -358,7 +382,7 @@ Genesis Timestamp "wallet": wallet_description, "base_fee": format!("{}", self.get_base_fee()), "gas_price": format!("{}", self.get_gas_price()), - "gas_limit": format!("{}", self.gas_limit), + "gas_limit": gas_limit, "genesis_timestamp": format!("{}", self.get_genesis_timestamp()), }) } @@ -390,7 +414,7 @@ impl Default for NodeConfig { let genesis_accounts = AccountGenerator::new(10).phrase(DEFAULT_MNEMONIC).gen(); Self { chain_id: None, - gas_limit: 30_000_000, + gas_limit: None, disable_block_gas_limit: false, gas_price: None, hardfork: None, @@ -410,6 +434,7 @@ impl Default for NodeConfig { fork_choice: None, account_generator: None, base_fee: None, + disable_min_priority_fee: false, blob_excess_gas_and_price: None, enable_tracing: true, enable_steps_tracing: false, @@ -452,9 +477,9 @@ impl NodeConfig { self } /// Returns the base fee to use - pub fn get_base_fee(&self) -> u128 { + pub fn get_base_fee(&self) -> u64 { self.base_fee - .or_else(|| self.genesis.as_ref().and_then(|g| g.base_fee_per_gas)) + .or_else(|| self.genesis.as_ref().and_then(|g| g.base_fee_per_gas.map(|g| g as u64))) .unwrap_or(INITIAL_BASE_FEE) } @@ -470,7 +495,8 @@ impl NodeConfig { { BlobExcessGasAndPrice::new(excess_blob_gas as u64) } else { - BlobExcessGasAndPrice { blob_gasprice: 0, excess_blob_gas: 0 } + // If no excess blob gas is configured, default to 0 + BlobExcessGasAndPrice::new(0) } } @@ -546,9 +572,7 @@ impl NodeConfig { /// Sets the gas limit #[must_use] pub fn with_gas_limit(mut self, gas_limit: Option) -> Self { - if let Some(gas_limit) = gas_limit { - self.gas_limit = gas_limit; - } + self.gas_limit = gas_limit; self } @@ -597,11 +621,18 @@ impl NodeConfig { /// Sets the base fee #[must_use] - pub fn with_base_fee(mut self, base_fee: Option) -> Self { + pub fn with_base_fee(mut self, base_fee: Option) -> Self { self.base_fee = base_fee; self } + /// Disable the enforcement of a minimum suggested priority fee + #[must_use] + pub fn disable_min_priority_fee(mut self, disable_min_priority_fee: bool) -> Self { + self.disable_min_priority_fee = disable_min_priority_fee; + self + } + /// Sets the init genesis (genesis.json) #[must_use] pub fn with_genesis(mut self, genesis: Option) -> Self { @@ -962,7 +993,7 @@ impl NodeConfig { let env = revm::primitives::Env { cfg: cfg.cfg_env, block: BlockEnv { - gas_limit: U256::from(self.gas_limit), + gas_limit: U256::from(self.gas_limit()), basefee: U256::from(self.get_base_fee()), ..Default::default() }, @@ -973,6 +1004,7 @@ impl NodeConfig { let fees = FeeManager::new( cfg.handler_cfg.spec_id, self.get_base_fee(), + !self.disable_min_priority_fee, self.get_gas_price(), self.get_blob_excess_gas_and_price(), ); @@ -1140,15 +1172,7 @@ latest block number: {latest_block}" panic!("Failed to get block for block number: {fork_block_number}") }; - // we only use the gas limit value of the block if it is non-zero and the block gas - // limit is enabled, since there are networks where this is not used and is always - // `0x0` which would inevitably result in `OutOfGas` errors as soon as the evm is about to record gas, See also - let gas_limit = if self.disable_block_gas_limit || block.header.gas_limit == 0 { - u64::MAX as u128 - } else { - block.header.gas_limit - }; - + let gas_limit = self.fork_gas_limit(&block); env.block = BlockEnv { number: U256::from(fork_block_number), timestamp: U256::from(block.header.timestamp), @@ -1170,10 +1194,11 @@ latest block number: {latest_block}" // this is the base fee of the current block, but we need the base fee of // the next block let next_block_base_fee = fees.get_next_block_base_fee_per_gas( - block.header.gas_used, - block.header.gas_limit, + block.header.gas_used as u128, + gas_limit, block.header.base_fee_per_gas.unwrap_or_default(), ); + // update next base fee fees.set_base_fee(next_block_base_fee); } @@ -1181,9 +1206,9 @@ latest block number: {latest_block}" (block.header.excess_blob_gas, block.header.blob_gas_used) { env.block.blob_excess_gas_and_price = - Some(BlobExcessGasAndPrice::new(blob_excess_gas as u64)); - let next_block_blob_excess_gas = - fees.get_next_block_blob_excess_gas(blob_excess_gas, blob_gas_used); + Some(BlobExcessGasAndPrice::new(blob_excess_gas)); + let next_block_blob_excess_gas = fees + .get_next_block_blob_excess_gas(blob_excess_gas as u128, blob_gas_used as u128); fees.set_blob_excess_gas_and_price(BlobExcessGasAndPrice::new( next_block_blob_excess_gas, )); @@ -1243,13 +1268,13 @@ latest block number: {latest_block}" chain_id, override_chain_id, timestamp: block.header.timestamp, - base_fee: block.header.base_fee_per_gas, + base_fee: block.header.base_fee_per_gas.map(|g| g as u128), timeout: self.fork_request_timeout, retries: self.fork_request_retries, backoff: self.fork_retry_backoff, compute_units_per_second: self.compute_units_per_second, total_difficulty: block.header.total_difficulty.unwrap_or_default(), - blob_gas_used: block.header.blob_gas_used, + blob_gas_used: block.header.blob_gas_used.map(|g| g as u128), blob_excess_gas_and_price: env.block.blob_excess_gas_and_price.clone(), force_transactions, }; @@ -1261,6 +1286,32 @@ latest block number: {latest_block}" (db, config) } + + /// we only use the gas limit value of the block if it is non-zero and the block gas + /// limit is enabled, since there are networks where this is not used and is always + /// `0x0` which would inevitably result in `OutOfGas` errors as soon as the evm is about to record gas, See also + pub(crate) fn fork_gas_limit(&self, block: &Block>) -> u128 { + if !self.disable_block_gas_limit { + if let Some(gas_limit) = self.gas_limit { + return gas_limit; + } else if block.header.gas_limit > 0 { + return block.header.gas_limit as u128; + } + } + + u64::MAX as u128 + } + + /// Returns the gas limit for a non forked anvil instance + /// + /// Checks the config for the `disable_block_gas_limit` flag + pub(crate) fn gas_limit(&self) -> u128 { + if self.disable_block_gas_limit { + return u64::MAX as u128; + } + + self.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT) + } } /// If the fork choice is a block number, simply return it with an empty list of transactions. @@ -1300,8 +1351,9 @@ async fn derive_block_and_transactions( // Convert the transactions to PoolTransactions let force_transactions = filtered_transactions .iter() - .map(|&transaction| PoolTransaction::try_from(transaction.clone().inner)) - .collect::, _>>()?; + .map(|&transaction| PoolTransaction::try_from(transaction.clone())) + .collect::, _>>() + .map_err(|e| eyre::eyre!("Err converting to pool transactions {e}"))?; Ok((transaction_block_number.saturating_sub(1), Some(force_transactions))) } } @@ -1360,7 +1412,7 @@ impl PruneStateHistoryConfig { !self.enabled || self.max_memory_history.is_some() } - /// Returns tru if this setting was enabled. + /// Returns true if this setting was enabled. pub fn is_config_enabled(&self) -> bool { self.enabled } diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index cbb5b2d70..a85862664 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -592,7 +592,11 @@ impl EthApi { /// Returns the current gas price pub fn gas_price(&self) -> u128 { if self.backend.is_eip1559() { - self.backend.base_fee().saturating_add(self.lowest_suggestion_tip()) + if self.backend.is_min_priority_fee_enforced() { + (self.backend.base_fee() as u128).saturating_add(self.lowest_suggestion_tip()) + } else { + self.backend.base_fee() as u128 + } } else { self.backend.fees().raw_gas_price() } @@ -1405,7 +1409,7 @@ impl EthApi { // The spec states that `base_fee_per_gas` "[..] includes the next block after the // newest of the returned range, because this value can be derived from the // newest block" - response.base_fee_per_gas.push(self.backend.fees().base_fee()); + response.base_fee_per_gas.push(self.backend.fees().base_fee() as u128); // Same goes for the `base_fee_per_blob_gas`: // > [..] includes the next block after the newest of the returned range, because this @@ -1911,7 +1915,6 @@ impl EthApi { pub async fn anvil_metadata(&self) -> Result { node_info!("anvil_metadata"); let fork_config = self.backend.get_fork(); - let snapshots = self.backend.list_snapshots(); Ok(Metadata { client_version: CLIENT_VERSION.to_string(), @@ -1924,7 +1927,7 @@ impl EthApi { fork_block_number: cfg.block_number(), fork_block_hash: cfg.block_hash(), }), - snapshots, + snapshots: self.backend.list_state_snapshots(), }) } @@ -1934,7 +1937,7 @@ impl EthApi { Ok(()) } - /// Reorg the chain to a specific depth and mine new blocks back to the cannonical height. + /// Reorg the chain to a specific depth and mine new blocks back to the canonical height. /// /// e.g depth = 3 /// A -> B -> C -> D -> E @@ -2057,7 +2060,7 @@ impl EthApi { /// Handler for RPC call: `evm_snapshot` pub async fn evm_snapshot(&self) -> Result { node_info!("evm_snapshot"); - Ok(self.backend.create_snapshot().await) + Ok(self.backend.create_state_snapshot().await) } /// Revert the state of the blockchain to a previous snapshot. @@ -2066,7 +2069,7 @@ impl EthApi { /// Handler for RPC call: `evm_revert` pub async fn evm_revert(&self, id: U256) -> Result { node_info!("evm_revert"); - self.backend.revert_snapshot(id).await + self.backend.revert_state_snapshot(id).await } /// Jump forward in time by the given amount of time, in seconds. @@ -2303,7 +2306,7 @@ impl EthApi { let to = tx.to(); let gas_price = tx.gas_price(); let value = tx.value(); - let gas = tx.gas_limit(); + let gas = tx.gas_limit() as u128; TxpoolInspectSummary { to, value, gas, gas_price } } @@ -2443,7 +2446,7 @@ impl EthApi { state, )?); } - self.do_estimate_gas_with_state(request, state, block) + self.do_estimate_gas_with_state(request, &state, block) }) .await? } @@ -2451,15 +2454,12 @@ impl EthApi { /// Estimates the gas usage of the `request` with the state. /// /// This will execute the transaction request and find the best gas limit via binary search. - fn do_estimate_gas_with_state( + fn do_estimate_gas_with_state( &self, mut request: WithOtherFields, - state: D, + state: &dyn DatabaseRef, block_env: BlockEnv, - ) -> Result - where - D: DatabaseRef, - { + ) -> Result { // If the request is a simple native token transfer we can optimize // We assume it's a transfer if we have no input data. let to = request.to.as_ref().and_then(TxKind::to); @@ -2489,13 +2489,14 @@ impl EthApi { // get the highest possible gas limit, either the request's set value or the currently // configured gas limit - let mut highest_gas_limit = request.gas.unwrap_or(block_env.gas_limit.to()); + let mut highest_gas_limit = + request.gas.map_or(block_env.gas_limit.to::(), |g| g as u128); let gas_price = fees.gas_price.unwrap_or_default(); // If we have non-zero gas price, cap gas limit by sender balance if gas_price > 0 { if let Some(from) = request.from { - let mut available_funds = self.backend.get_balance_with_state(&state, from)?; + let mut available_funds = self.backend.get_balance_with_state(state, from)?; if let Some(value) = request.value { if value > available_funds { return Err(InvalidTransactionError::InsufficientFunds.into()); @@ -2511,7 +2512,7 @@ impl EthApi { } let mut call_to_estimate = request.clone(); - call_to_estimate.gas = Some(highest_gas_limit); + call_to_estimate.gas = Some(highest_gas_limit as u64); // execute the call without writing to db let ethres = @@ -2545,7 +2546,7 @@ impl EthApi { // Binary search for the ideal gas limit while (highest_gas_limit - lowest_gas_limit) > 1 { - request.gas = Some(mid_gas_limit); + request.gas = Some(mid_gas_limit as u64); let ethres = self.backend.call_with_state( &state, request.clone(), @@ -2567,7 +2568,7 @@ impl EthApi { // current midpoint, as spending any less gas would make no // sense (as the TX would still revert due to lack of gas). // - // We don't care about the reason here, as we known that trasaction is correct + // We don't care about the reason here, as we known that transaction is correct // as it succeeded earlier lowest_gas_limit = mid_gas_limit; } @@ -2692,7 +2693,7 @@ impl EthApi { let max_fee_per_blob_gas = request.max_fee_per_blob_gas; let gas_price = request.gas_price; - let gas_limit = request.gas.unwrap_or(self.backend.gas_limit()); + let gas_limit = request.gas.unwrap_or(self.backend.gas_limit() as u64); let request = match transaction_request_to_typed(request) { Some(TypedTransactionRequest::Legacy(mut m)) => { @@ -2902,7 +2903,7 @@ fn determine_base_gas_by_kind(request: &WithOtherFields) -> TxKind::Create => MIN_CREATE_GAS, }, TypedTransactionRequest::EIP4844(_) => MIN_TRANSACTION_GAS, - TypedTransactionRequest::Deposit(req) => match req.kind { + TypedTransactionRequest::Deposit(req) => match req.to { TxKind::Call(_) => MIN_TRANSACTION_GAS, TxKind::Create => MIN_CREATE_GAS, }, diff --git a/crates/anvil/src/eth/backend/cheats.rs b/crates/anvil/src/eth/backend/cheats.rs index 5b498f963..32115cf41 100644 --- a/crates/anvil/src/eth/backend/cheats.rs +++ b/crates/anvil/src/eth/backend/cheats.rs @@ -1,8 +1,8 @@ //! Support for "cheat codes" / bypass functions -use alloy_primitives::Address; +use alloy_primitives::{map::AddressHashSet, Address}; use parking_lot::RwLock; -use std::{collections::HashSet, sync::Arc}; +use std::sync::Arc; /// Manages user modifications that may affect the node's behavior /// @@ -24,7 +24,7 @@ impl CheatsManager { let mut state = self.state.write(); // When somebody **explicitly** impersonates an account we need to store it so we are able // to return it from `eth_accounts`. That's why we do not simply call `is_impersonated()` - // which does not check that list when auto impersonation is enabeld. + // which does not check that list when auto impersonation is enabled. if state.impersonated_accounts.contains(&addr) { // need to check if already impersonated, so we don't overwrite the code return true @@ -55,7 +55,7 @@ impl CheatsManager { } /// Returns all accounts that are currently being impersonated. - pub fn impersonated_accounts(&self) -> HashSet
{ + pub fn impersonated_accounts(&self) -> AddressHashSet { self.state.read().impersonated_accounts.clone() } } @@ -64,7 +64,7 @@ impl CheatsManager { #[derive(Clone, Debug, Default)] pub struct CheatsState { /// All accounts that are currently impersonated - pub impersonated_accounts: HashSet
, + pub impersonated_accounts: AddressHashSet, /// If set to true will make the `is_impersonated` function always return true pub auto_impersonate_accounts: bool, } diff --git a/crates/anvil/src/eth/backend/db.rs b/crates/anvil/src/eth/backend/db.rs index 4b6b2bed2..90eaaee05 100644 --- a/crates/anvil/src/eth/backend/db.rs +++ b/crates/anvil/src/eth/backend/db.rs @@ -11,7 +11,8 @@ use anvil_core::eth::{ use foundry_common::errors::FsPathError; use foundry_evm::{ backend::{ - BlockchainDb, DatabaseError, DatabaseResult, MemDb, RevertSnapshotAction, StateSnapshot, + BlockchainDb, DatabaseError, DatabaseResult, MemDb, RevertStateSnapshotAction, + StateSnapshot, }, revm::{ db::{CacheDB, DatabaseRef, DbAccount}, @@ -26,50 +27,56 @@ use serde::{ use std::{collections::BTreeMap, fmt, path::Path}; /// Helper trait get access to the full state data of the database -#[auto_impl::auto_impl(Box)] pub trait MaybeFullDatabase: DatabaseRef { + /// Returns a reference to the database as a `dyn DatabaseRef`. + // TODO: Required until trait upcasting is stabilized: + fn as_dyn(&self) -> &dyn DatabaseRef; + fn maybe_as_full_db(&self) -> Option<&HashMap> { None } - /// Clear the state and move it into a new `StateSnapshot` - fn clear_into_snapshot(&mut self) -> StateSnapshot; + /// Clear the state and move it into a new `StateSnapshot`. + fn clear_into_state_snapshot(&mut self) -> StateSnapshot; - /// Read the state snapshot + /// Read the state snapshot. /// - /// This clones all the states and returns a new `StateSnapshot` - fn read_as_snapshot(&self) -> StateSnapshot; + /// This clones all the states and returns a new `StateSnapshot`. + fn read_as_state_snapshot(&self) -> StateSnapshot; /// Clears the entire database fn clear(&mut self); - /// Reverses `clear_into_snapshot` by initializing the db's state with the snapshot - fn init_from_snapshot(&mut self, snapshot: StateSnapshot); + /// Reverses `clear_into_snapshot` by initializing the db's state with the state snapshot. + fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot); } impl<'a, T: 'a + MaybeFullDatabase + ?Sized> MaybeFullDatabase for &'a T where &'a T: DatabaseRef, { + fn as_dyn(&self) -> &dyn DatabaseRef { + T::as_dyn(self) + } + fn maybe_as_full_db(&self) -> Option<&HashMap> { T::maybe_as_full_db(self) } - fn clear_into_snapshot(&mut self) -> StateSnapshot { + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { unreachable!("never called for DatabaseRef") } - fn read_as_snapshot(&self) -> StateSnapshot { + fn read_as_state_snapshot(&self) -> StateSnapshot { unreachable!("never called for DatabaseRef") } fn clear(&mut self) {} - fn init_from_snapshot(&mut self, _snapshot: StateSnapshot) {} + fn init_from_state_snapshot(&mut self, _state_snapshot: StateSnapshot) {} } /// Helper trait to reset the DB if it's forked -#[auto_impl::auto_impl(Box)] pub trait MaybeForkedDatabase { fn maybe_reset(&mut self, _url: Option, block_number: BlockId) -> Result<(), String>; @@ -79,7 +86,6 @@ pub trait MaybeForkedDatabase { } /// This bundles all required revm traits -#[auto_impl::auto_impl(Box)] pub trait Db: DatabaseRef + Database @@ -171,13 +177,13 @@ pub trait Db: Ok(true) } - /// Creates a new snapshot - fn snapshot(&mut self) -> U256; + /// Creates a new state snapshot. + fn snapshot_state(&mut self) -> U256; - /// Reverts a snapshot + /// Reverts a state snapshot. /// - /// Returns `true` if the snapshot was reverted - fn revert(&mut self, snapshot: U256, action: RevertSnapshotAction) -> bool; + /// Returns `true` if the state snapshot was reverted. + fn revert_state(&mut self, state_snapshot: U256, action: RevertStateSnapshotAction) -> bool; /// Returns the state root if possible to compute fn maybe_state_root(&self) -> Option { @@ -188,6 +194,13 @@ pub trait Db: fn current_state(&self) -> StateDb; } +impl dyn Db { + // TODO: Required until trait upcasting is stabilized: + pub fn as_dbref(&self) -> &dyn DatabaseRef { + self.as_dyn() + } +} + /// Convenience impl only used to use any `Db` on the fly as the db layer for revm's CacheDB /// This is useful to create blocks without actually writing to the `Db`, but rather in the cache of /// the `CacheDB` see also @@ -216,11 +229,11 @@ impl + Send + Sync + Clone + fmt::Debug> D Ok(None) } - fn snapshot(&mut self) -> U256 { + fn snapshot_state(&mut self) -> U256 { U256::ZERO } - fn revert(&mut self, _snapshot: U256, _action: RevertSnapshotAction) -> bool { + fn revert_state(&mut self, _state_snapshot: U256, _action: RevertStateSnapshotAction) -> bool { false } @@ -230,11 +243,15 @@ impl + Send + Sync + Clone + fmt::Debug> D } impl> MaybeFullDatabase for CacheDB { + fn as_dyn(&self) -> &dyn DatabaseRef { + self + } + fn maybe_as_full_db(&self) -> Option<&HashMap> { Some(&self.accounts) } - fn clear_into_snapshot(&mut self) -> StateSnapshot { + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { let db_accounts = std::mem::take(&mut self.accounts); let mut accounts = HashMap::default(); let mut account_storage = HashMap::default(); @@ -249,7 +266,7 @@ impl> MaybeFullDatabase for CacheDB { StateSnapshot { accounts, storage: account_storage, block_hashes } } - fn read_as_snapshot(&self) -> StateSnapshot { + fn read_as_state_snapshot(&self) -> StateSnapshot { let db_accounts = self.accounts.clone(); let mut accounts = HashMap::default(); let mut account_storage = HashMap::default(); @@ -266,11 +283,11 @@ impl> MaybeFullDatabase for CacheDB { } fn clear(&mut self) { - self.clear_into_snapshot(); + self.clear_into_state_snapshot(); } - fn init_from_snapshot(&mut self, snapshot: StateSnapshot) { - let StateSnapshot { accounts, mut storage, block_hashes } = snapshot; + fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) { + let StateSnapshot { accounts, mut storage, block_hashes } = state_snapshot; for (addr, mut acc) in accounts { if let Some(code) = acc.code.take() { @@ -314,7 +331,7 @@ impl StateDb { pub fn serialize_state(&mut self) -> StateSnapshot { // Using read_as_snapshot makes sures we don't clear the historical state from the current // instance. - self.read_as_snapshot() + self.read_as_state_snapshot() } } @@ -338,24 +355,28 @@ impl DatabaseRef for StateDb { } impl MaybeFullDatabase for StateDb { + fn as_dyn(&self) -> &dyn DatabaseRef { + self.0.as_dyn() + } + fn maybe_as_full_db(&self) -> Option<&HashMap> { self.0.maybe_as_full_db() } - fn clear_into_snapshot(&mut self) -> StateSnapshot { - self.0.clear_into_snapshot() + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { + self.0.clear_into_state_snapshot() } - fn read_as_snapshot(&self) -> StateSnapshot { - self.0.read_as_snapshot() + fn read_as_state_snapshot(&self) -> StateSnapshot { + self.0.read_as_state_snapshot() } fn clear(&mut self) { self.0.clear() } - fn init_from_snapshot(&mut self, snapshot: StateSnapshot) { - self.0.init_from_snapshot(snapshot) + fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) { + self.0.init_from_state_snapshot(state_snapshot) } } diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index c84ad5200..3ceee8b04 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -28,8 +28,9 @@ use foundry_evm::{ }, }, traces::CallTraceNode, + utils::alphanet_handler_register, }; -use revm::primitives::MAX_BLOB_GAS_PER_BLOCK; +use revm::{db::WrapDatabaseRef, primitives::MAX_BLOB_GAS_PER_BLOCK}; use std::sync::Arc; /// Represents an executed transaction (transacted on the DB) @@ -38,7 +39,7 @@ pub struct ExecutedTransaction { transaction: Arc, exit_reason: InstructionResult, out: Option, - gas_used: u128, + gas_used: u64, logs: Vec, traces: Vec, nonce: u64, @@ -48,7 +49,7 @@ pub struct ExecutedTransaction { impl ExecutedTransaction { /// Creates the receipt for the transaction - fn create_receipt(&self, cumulative_gas_used: &mut u128) -> TypedReceipt { + fn create_receipt(&self, cumulative_gas_used: &mut u64) -> TypedReceipt { let logs = self.logs.clone(); *cumulative_gas_used = cumulative_gas_used.saturating_add(self.gas_used); @@ -56,7 +57,7 @@ impl ExecutedTransaction { let status_code = u8::from(self.exit_reason as u8 <= InstructionResult::SelfDestruct as u8); let receipt_with_bloom: ReceiptWithBloom = Receipt { status: (status_code == 1).into(), - cumulative_gas_used: *cumulative_gas_used, + cumulative_gas_used: *cumulative_gas_used as u128, logs, } .into(); @@ -89,11 +90,11 @@ pub struct ExecutedTransactions { } /// An executor for a series of transactions -pub struct TransactionExecutor<'a, Db: ?Sized, Validator: TransactionValidator> { +pub struct TransactionExecutor<'a, Db: ?Sized, V: TransactionValidator> { /// where to insert the transactions pub db: &'a mut Db, /// type used to validate before inclusion - pub validator: Validator, + pub validator: &'a V, /// all pending transactions pub pending: std::vec::IntoIter>, pub block_env: BlockEnv, @@ -101,9 +102,9 @@ pub struct TransactionExecutor<'a, Db: ?Sized, Validator: TransactionValidator> pub cfg_env: CfgEnvWithHandlerCfg, pub parent_hash: B256, /// Cumulative gas used by all executed transactions - pub gas_used: u128, + pub gas_used: u64, /// Cumulative blob gas used by all executed transactions - pub blob_gas_used: u128, + pub blob_gas_used: u64, pub enable_steps_tracing: bool, pub alphanet: bool, pub print_logs: bool, @@ -111,31 +112,31 @@ pub struct TransactionExecutor<'a, Db: ?Sized, Validator: TransactionValidator> pub precompile_factory: Option>, } -impl<'a, DB: Db + ?Sized, Validator: TransactionValidator> TransactionExecutor<'a, DB, Validator> { +impl TransactionExecutor<'_, DB, V> { /// Executes all transactions and puts them in a new block with the provided `timestamp` pub fn execute(mut self) -> ExecutedTransactions { let mut transactions = Vec::new(); let mut transaction_infos = Vec::new(); let mut receipts = Vec::new(); let mut bloom = Bloom::default(); - let mut cumulative_gas_used: u128 = 0; + let mut cumulative_gas_used = 0u64; let mut invalid = Vec::new(); let mut included = Vec::new(); - let gas_limit = self.block_env.gas_limit.to::(); + let gas_limit = self.block_env.gas_limit.to::(); let parent_hash = self.parent_hash; let block_number = self.block_env.number.to::(); let difficulty = self.block_env.difficulty; let beneficiary = self.block_env.coinbase; let timestamp = self.block_env.timestamp.to::(); let base_fee = if self.cfg_env.handler_cfg.spec_id.is_enabled_in(SpecId::LONDON) { - Some(self.block_env.basefee.to::()) + Some(self.block_env.basefee.to::()) } else { None }; let is_cancun = self.cfg_env.handler_cfg.spec_id >= SpecId::CANCUN; let excess_blob_gas = if is_cancun { self.block_env.get_blob_excess_gas() } else { None }; - let mut cumulative_blob_gas_used = if is_cancun { Some(0u128) } else { None }; + let mut cumulative_blob_gas_used = if is_cancun { Some(0u64) } else { None }; for tx in self.into_iter() { let tx = match tx { @@ -172,7 +173,7 @@ impl<'a, DB: Db + ?Sized, Validator: TransactionValidator> TransactionExecutor<' .blob_gas() .unwrap_or(0); cumulative_blob_gas_used = - Some(cumulative_blob_gas_used.unwrap_or(0u128).saturating_add(tx_blob_gas)); + Some(cumulative_blob_gas_used.unwrap_or(0u64).saturating_add(tx_blob_gas)); } let receipt = tx.create_receipt(&mut cumulative_gas_used); @@ -228,7 +229,7 @@ impl<'a, DB: Db + ?Sized, Validator: TransactionValidator> TransactionExecutor<' base_fee, parent_beacon_block_root: Default::default(), blob_gas_used: cumulative_blob_gas_used, - excess_blob_gas: excess_blob_gas.map(|g| g as u128), + excess_blob_gas, }; let block = Block::new(partial_header, transactions.clone(), ommers); @@ -262,9 +263,7 @@ pub enum TransactionExecutionOutcome { DatabaseError(Arc, DatabaseError), } -impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator - for &'b mut TransactionExecutor<'a, DB, Validator> -{ +impl Iterator for &mut TransactionExecutor<'_, DB, V> { type Item = TransactionExecutionOutcome; fn next(&mut self) -> Option { @@ -277,16 +276,16 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator let env = self.env_for(&transaction.pending_transaction); // check that we comply with the block's gas limit, if not disabled - let max_gas = self.gas_used.saturating_add(env.tx.gas_limit as u128); - if !env.cfg.disable_block_gas_limit && max_gas > env.block.gas_limit.to::() { + let max_gas = self.gas_used.saturating_add(env.tx.gas_limit); + if !env.cfg.disable_block_gas_limit && max_gas > env.block.gas_limit.to::() { return Some(TransactionExecutionOutcome::Exhausted(transaction)) } // check that we comply with the block's blob gas limit let max_blob_gas = self.blob_gas_used.saturating_add( - transaction.pending_transaction.transaction.transaction.blob_gas().unwrap_or(0u128), + transaction.pending_transaction.transaction.transaction.blob_gas().unwrap_or(0), ); - if max_blob_gas > MAX_BLOB_GAS_PER_BLOCK as u128 { + if max_blob_gas > MAX_BLOB_GAS_PER_BLOCK { return Some(TransactionExecutionOutcome::BlobGasExhausted(transaction)) } @@ -303,7 +302,7 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator let nonce = account.nonce; // records all call and step traces - let mut inspector = Inspector::default().with_tracing().with_alphanet(self.alphanet); + let mut inspector = Inspector::default().with_tracing(); if self.enable_steps_tracing { inspector = inspector.with_steps_tracing(); } @@ -312,8 +311,7 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator } let exec_result = { - let mut evm = - foundry_evm::utils::new_evm_with_inspector(&mut *self.db, env, &mut inspector); + let mut evm = new_evm_with_inspector(&mut *self.db, env, &mut inspector, self.alphanet); if let Some(factory) = &self.precompile_factory { inject_precompiles(&mut evm, factory.precompiles()); } @@ -364,7 +362,7 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator trace!(target: "backend", ?exit_reason, ?gas_used, "[{:?}] executed with out={:?}", transaction.hash(), out); // Track the total gas used for total gas per block checks - self.gas_used = self.gas_used.saturating_add(gas_used as u128); + self.gas_used = self.gas_used.saturating_add(gas_used); // Track the total blob gas used for total blob gas per blob checks if let Some(blob_gas) = transaction.pending_transaction.transaction.transaction.blob_gas() { @@ -377,7 +375,7 @@ impl<'a, 'b, DB: Db + ?Sized, Validator: TransactionValidator> Iterator transaction, exit_reason, out, - gas_used: gas_used as u128, + gas_used, logs: logs.unwrap_or_default(), traces: inspector.tracer.map(|t| t.into_traces().into_nodes()).unwrap_or_default(), nonce, @@ -396,3 +394,37 @@ fn build_logs_bloom(logs: Vec, bloom: &mut Bloom) { } } } + +/// Creates a database with given database and inspector, optionally enabling alphanet features. +pub fn new_evm_with_inspector( + db: DB, + env: EnvWithHandlerCfg, + inspector: &mut dyn revm::Inspector, + alphanet: bool, +) -> revm::Evm<'_, &mut dyn revm::Inspector, DB> { + let EnvWithHandlerCfg { env, handler_cfg } = env; + + let mut handler = revm::Handler::new(handler_cfg); + + handler.append_handler_register_plain(revm::inspector_handle_register); + if alphanet { + handler.append_handler_register_plain(alphanet_handler_register); + } + + let context = revm::Context::new(revm::EvmContext::new_with_env(db, env), inspector); + + revm::Evm::new(context, handler) +} + +/// Creates a new EVM with the given inspector and wraps the database in a `WrapDatabaseRef`. +pub fn new_evm_with_inspector_ref<'a, DB>( + db: DB, + env: EnvWithHandlerCfg, + inspector: &mut dyn revm::Inspector>, + alphanet: bool, +) -> revm::Evm<'a, &mut dyn revm::Inspector>, WrapDatabaseRef> +where + DB: revm::DatabaseRef, +{ + new_evm_with_inspector(WrapDatabaseRef(db), env, inspector, alphanet) +} diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index c661eeaa8..7d887f697 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -4,7 +4,10 @@ use crate::eth::{backend::db::Db, error::BlockchainError, pool::transactions::Po use alloy_consensus::Account; use alloy_eips::eip2930::AccessListResult; use alloy_network::BlockResponse; -use alloy_primitives::{Address, Bytes, StorageValue, B256, U256}; +use alloy_primitives::{ + map::{FbHashMap, HashMap}, + Address, Bytes, StorageValue, B256, U256, +}; use alloy_provider::{ ext::{DebugApi, TraceApi}, Provider, @@ -27,7 +30,7 @@ use parking_lot::{ RawRwLock, RwLock, }; use revm::primitives::BlobExcessGasAndPrice; -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; use tokio::sync::RwLock as AsyncRwLock; /// Represents a fork of a remote client @@ -89,7 +92,13 @@ impl ClientFork { let total_difficulty = block.header.total_difficulty.unwrap_or_default(); let number = block.header.number; - self.config.write().update_block(number, block_hash, timestamp, base_fee, total_difficulty); + self.config.write().update_block( + number, + block_hash, + timestamp, + base_fee.map(|g| g as u128), + total_difficulty, + ); self.clear_cached_storage(); @@ -199,7 +208,7 @@ impl ClientFork { let block = block.unwrap_or_default(); let res = self.provider().estimate_gas(request).block(block.into()).await?; - Ok(res) + Ok(res as u128) } /// Sends `eth_createAccessList` @@ -675,14 +684,14 @@ impl ClientForkConfig { /// This is used as a cache so repeated requests to the same data are not sent to the remote client #[derive(Clone, Debug, Default)] pub struct ForkedStorage { - pub uncles: HashMap>, - pub blocks: HashMap, + pub uncles: FbHashMap<32, Vec>, + pub blocks: FbHashMap<32, AnyNetworkBlock>, pub hashes: HashMap, - pub transactions: HashMap>, - pub transaction_receipts: HashMap, - pub transaction_traces: HashMap>, + pub transactions: FbHashMap<32, WithOtherFields>, + pub transaction_receipts: FbHashMap<32, ReceiptResponse>, + pub transaction_traces: FbHashMap<32, Vec>, pub logs: HashMap>, - pub geth_transaction_traces: HashMap, + pub geth_transaction_traces: FbHashMap<32, GethTrace>, pub block_traces: HashMap>, pub block_receipts: HashMap>, pub code_at: HashMap<(Address, u64), Bytes>, diff --git a/crates/anvil/src/eth/backend/mem/fork_db.rs b/crates/anvil/src/eth/backend/mem/fork_db.rs index 1329de724..a4528a8f0 100644 --- a/crates/anvil/src/eth/backend/mem/fork_db.rs +++ b/crates/anvil/src/eth/backend/mem/fork_db.rs @@ -8,15 +8,16 @@ use crate::{ use alloy_primitives::{Address, B256, U256, U64}; use alloy_rpc_types::BlockId; use foundry_evm::{ - backend::{BlockchainDb, DatabaseResult, RevertSnapshotAction, StateSnapshot}, - fork::database::ForkDbSnapshot, - revm::Database, + backend::{ + BlockchainDb, DatabaseError, DatabaseResult, RevertStateSnapshotAction, StateSnapshot, + }, + fork::database::ForkDbStateSnapshot, + revm::{primitives::BlockEnv, Database}, }; +use revm::DatabaseRef; pub use foundry_evm::fork::database::ForkedDatabase; -use foundry_evm::revm::primitives::BlockEnv; -/// Implement the helper for the fork database impl Db for ForkedDatabase { fn insert_account(&mut self, address: Address, account: AccountInfo) { self.database_mut().insert_account(address, account) @@ -73,21 +74,25 @@ impl Db for ForkedDatabase { })) } - fn snapshot(&mut self) -> U256 { - self.insert_snapshot() + fn snapshot_state(&mut self) -> U256 { + self.insert_state_snapshot() } - fn revert(&mut self, id: U256, action: RevertSnapshotAction) -> bool { - self.revert_snapshot(id, action) + fn revert_state(&mut self, id: U256, action: RevertStateSnapshotAction) -> bool { + self.revert_state_snapshot(id, action) } fn current_state(&self) -> StateDb { - StateDb::new(self.create_snapshot()) + StateDb::new(self.create_state_snapshot()) } } impl MaybeFullDatabase for ForkedDatabase { - fn clear_into_snapshot(&mut self) -> StateSnapshot { + fn as_dyn(&self) -> &dyn DatabaseRef { + self + } + + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { let db = self.inner().db(); let accounts = std::mem::take(&mut *db.accounts.write()); let storage = std::mem::take(&mut *db.storage.write()); @@ -95,7 +100,7 @@ impl MaybeFullDatabase for ForkedDatabase { StateSnapshot { accounts, storage, block_hashes } } - fn read_as_snapshot(&self) -> StateSnapshot { + fn read_as_state_snapshot(&self) -> StateSnapshot { let db = self.inner().db(); let accounts = db.accounts.read().clone(); let storage = db.storage.read().clone(); @@ -105,34 +110,38 @@ impl MaybeFullDatabase for ForkedDatabase { fn clear(&mut self) { self.flush_cache(); - self.clear_into_snapshot(); + self.clear_into_state_snapshot(); } - fn init_from_snapshot(&mut self, snapshot: StateSnapshot) { + fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) { let db = self.inner().db(); - let StateSnapshot { accounts, storage, block_hashes } = snapshot; + let StateSnapshot { accounts, storage, block_hashes } = state_snapshot; *db.accounts.write() = accounts; *db.storage.write() = storage; *db.block_hashes.write() = block_hashes; } } -impl MaybeFullDatabase for ForkDbSnapshot { - fn clear_into_snapshot(&mut self) -> StateSnapshot { - std::mem::take(&mut self.snapshot) +impl MaybeFullDatabase for ForkDbStateSnapshot { + fn as_dyn(&self) -> &dyn DatabaseRef { + self + } + + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { + std::mem::take(&mut self.state_snapshot) } - fn read_as_snapshot(&self) -> StateSnapshot { - self.snapshot.clone() + fn read_as_state_snapshot(&self) -> StateSnapshot { + self.state_snapshot.clone() } fn clear(&mut self) { - std::mem::take(&mut self.snapshot); + std::mem::take(&mut self.state_snapshot); self.local.clear() } - fn init_from_snapshot(&mut self, snapshot: StateSnapshot) { - self.snapshot = snapshot; + fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) { + self.state_snapshot = state_snapshot; } } diff --git a/crates/anvil/src/eth/backend/mem/in_memory_db.rs b/crates/anvil/src/eth/backend/mem/in_memory_db.rs index 56cd3815c..9e34448ad 100644 --- a/crates/anvil/src/eth/backend/mem/in_memory_db.rs +++ b/crates/anvil/src/eth/backend/mem/in_memory_db.rs @@ -8,16 +8,13 @@ use crate::{ mem::state::state_root, revm::{db::DbAccount, primitives::AccountInfo}, }; -use alloy_primitives::{Address, B256, U256, U64}; +use alloy_primitives::{map::HashMap, Address, B256, U256, U64}; use alloy_rpc_types::BlockId; -use foundry_evm::{ - backend::{BlockchainDb, DatabaseResult, StateSnapshot}, - hashbrown::HashMap, -}; +use foundry_evm::backend::{BlockchainDb, DatabaseResult, StateSnapshot}; // reexport for convenience pub use foundry_evm::{backend::MemDb, revm::db::DatabaseRef}; -use foundry_evm::{backend::RevertSnapshotAction, revm::primitives::BlockEnv}; +use foundry_evm::{backend::RevertStateSnapshotAction, revm::primitives::BlockEnv}; impl Db for MemDb { fn insert_account(&mut self, address: Address, account: AccountInfo) { @@ -74,22 +71,22 @@ impl Db for MemDb { } /// Creates a new snapshot - fn snapshot(&mut self) -> U256 { - let id = self.snapshots.insert(self.inner.clone()); - trace!(target: "backend::memdb", "Created new snapshot {}", id); + fn snapshot_state(&mut self) -> U256 { + let id = self.state_snapshots.insert(self.inner.clone()); + trace!(target: "backend::memdb", "Created new state snapshot {}", id); id } - fn revert(&mut self, id: U256, action: RevertSnapshotAction) -> bool { - if let Some(snapshot) = self.snapshots.remove(id) { + fn revert_state(&mut self, id: U256, action: RevertStateSnapshotAction) -> bool { + if let Some(state_snapshot) = self.state_snapshots.remove(id) { if action.is_keep() { - self.snapshots.insert_at(snapshot.clone(), id); + self.state_snapshots.insert_at(state_snapshot.clone(), id); } - self.inner = snapshot; - trace!(target: "backend::memdb", "Reverted snapshot {}", id); + self.inner = state_snapshot; + trace!(target: "backend::memdb", "Reverted state snapshot {}", id); true } else { - warn!(target: "backend::memdb", "No snapshot to revert for {}", id); + warn!(target: "backend::memdb", "No state snapshot to revert for {}", id); false } } @@ -104,24 +101,28 @@ impl Db for MemDb { } impl MaybeFullDatabase for MemDb { + fn as_dyn(&self) -> &dyn DatabaseRef { + self + } + fn maybe_as_full_db(&self) -> Option<&HashMap> { Some(&self.inner.accounts) } - fn clear_into_snapshot(&mut self) -> StateSnapshot { - self.inner.clear_into_snapshot() + fn clear_into_state_snapshot(&mut self) -> StateSnapshot { + self.inner.clear_into_state_snapshot() } - fn read_as_snapshot(&self) -> StateSnapshot { - self.inner.read_as_snapshot() + fn read_as_state_snapshot(&self) -> StateSnapshot { + self.inner.read_as_state_snapshot() } fn clear(&mut self) { self.inner.clear(); } - fn init_from_snapshot(&mut self, snapshot: StateSnapshot) { - self.inner.init_from_snapshot(snapshot) + fn init_from_state_snapshot(&mut self, snapshot: StateSnapshot) { + self.inner.init_from_state_snapshot(snapshot) } } diff --git a/crates/anvil/src/eth/backend/mem/inspector.rs b/crates/anvil/src/eth/backend/mem/inspector.rs index b354a9a5c..e590d57e3 100644 --- a/crates/anvil/src/eth/backend/mem/inspector.rs +++ b/crates/anvil/src/eth/backend/mem/inspector.rs @@ -14,7 +14,6 @@ use foundry_evm::{ EvmContext, }, traces::TracingInspectorConfig, - InspectorExt, }; /// The [`revm::Inspector`] used when transacting in the evm @@ -23,8 +22,6 @@ pub struct Inspector { pub tracer: Option, /// collects all `console.sol` logs pub log_collector: Option, - /// Whether to enable Alphanet support - pub alphanet: bool, } impl Inspector { @@ -59,12 +56,6 @@ impl Inspector { self.log_collector = Some(Default::default()); self } - - /// Enables Alphanet features - pub fn with_alphanet(mut self, yes: bool) -> Self { - self.alphanet = yes; - self - } } impl revm::Inspector for Inspector { @@ -176,12 +167,6 @@ impl revm::Inspector for Inspector { } } -impl InspectorExt for Inspector { - fn is_alphanet(&self) -> bool { - self.alphanet - } -} - /// Prints all the logs pub fn print_logs(logs: &[Log]) { for log in decode_console_logs(logs) { diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 0f28e28a5..0b7777f2d 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -1,6 +1,7 @@ //! In-memory blockchain backend. use self::state::trie_storage; +use super::executor::new_evm_with_inspector_ref; use crate::{ config::PruneStateHistoryConfig, eth::{ @@ -32,6 +33,7 @@ use crate::{ revm::{db::DatabaseRef, primitives::AccountInfo}, ForkChoice, NodeConfig, PrecompileFactory, }; +use alloy_chains::NamedChain; use alloy_consensus::{Account, Header, Receipt, ReceiptWithBloom}; use alloy_eips::eip4844::MAX_BLOBS_PER_BLOCK; use alloy_primitives::{keccak256, Address, Bytes, TxHash, TxKind, B256, U256, U64}; @@ -64,11 +66,10 @@ use anvil_core::eth::{ utils::meets_eip155, }; use anvil_rpc::error::RpcError; - -use alloy_chains::NamedChain; +use chrono::Datelike; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; use foundry_evm::{ - backend::{DatabaseError, DatabaseResult, RevertSnapshotAction}, + backend::{DatabaseError, DatabaseResult, RevertStateSnapshotAction}, constants::DEFAULT_CREATE2_DEPLOYER_RUNTIME_CODE, decode::RevertDecoder, inspectors::AccessListInspector, @@ -81,8 +82,6 @@ use foundry_evm::{ }, }, traces::TracingInspectorConfig, - utils::new_evm_with_inspector_ref, - InspectorExt, }; use futures::channel::mpsc::{unbounded, UnboundedSender}; use parking_lot::{Mutex, RwLock}; @@ -149,30 +148,30 @@ pub struct Backend { /// which the write-lock is active depends on whether the `ForkDb` can provide all requested /// data from memory or whether it has to retrieve it via RPC calls first. This means that it /// potentially blocks for some time, even taking into account the rate limits of RPC - /// endpoints. Therefor the `Db` is guarded by a `tokio::sync::RwLock` here so calls that + /// endpoints. Therefore the `Db` is guarded by a `tokio::sync::RwLock` here so calls that /// need to read from it, while it's currently written to, don't block. E.g. a new block is /// currently mined and a new [`Self::set_storage_at()`] request is being executed. db: Arc>>, - /// stores all block related data in memory + /// stores all block related data in memory. blockchain: Blockchain, - /// Historic states of previous blocks + /// Historic states of previous blocks. states: Arc>, - /// env data of the chain + /// Env data of the chain env: Arc>, - /// this is set if this is currently forked off another client + /// This is set if this is currently forked off another client. fork: Arc>>, - /// provides time related info, like timestamp + /// Provides time related info, like timestamp. time: TimeManager, - /// Contains state of custom overrides + /// Contains state of custom overrides. cheats: CheatsManager, - /// contains fee data + /// Contains fee data. fees: FeeManager, - /// initialised genesis + /// Initialised genesis. genesis: GenesisConfig, - /// listeners for new blocks that get notified when a new block was imported + /// Listeners for new blocks that get notified when a new block was imported. new_block_listeners: Arc>>>, - /// keeps track of active snapshots at a specific block - active_snapshots: Arc>>, + /// Keeps track of active state snapshots at a specific block. + active_state_snapshots: Arc>>, enable_steps_tracing: bool, print_logs: bool, alphanet: bool, @@ -256,7 +255,7 @@ impl Backend { new_block_listeners: Default::default(), fees, genesis, - active_snapshots: Arc::new(Mutex::new(Default::default())), + active_state_snapshots: Arc::new(Mutex::new(Default::default())), enable_steps_tracing, print_logs, alphanet, @@ -441,12 +440,14 @@ impl Backend { *self.fork.write() = Some(fork); *self.env.write() = env; } else { + let gas_limit = self.node_config.read().await.fork_gas_limit(&fork_block); let mut env = self.env.write(); + env.cfg.chain_id = fork.chain_id(); env.block = BlockEnv { number: U256::from(fork_block_number), timestamp: U256::from(fork_block.header.timestamp), - gas_limit: U256::from(fork_block.header.gas_limit), + gas_limit: U256::from(gas_limit), difficulty: fork_block.header.difficulty, prevrandao: Some(fork_block.header.mix_hash.unwrap_or_default()), // Keep previous `coinbase` and `basefee` value @@ -458,8 +459,8 @@ impl Backend { // this is the base fee of the current block, but we need the base fee of // the next block let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas( - fork_block.header.gas_used, - fork_block.header.gas_limit, + fork_block.header.gas_used as u128, + gas_limit, fork_block.header.base_fee_per_gas.unwrap_or_default(), ); @@ -663,16 +664,21 @@ impl Backend { } /// Returns the current base fee - pub fn base_fee(&self) -> u128 { + pub fn base_fee(&self) -> u64 { self.fees.base_fee() } + /// Returns whether the minimum suggested priority fee is enforced + pub fn is_min_priority_fee_enforced(&self) -> bool { + self.fees.is_min_priority_fee_enforced() + } + pub fn excess_blob_gas_and_price(&self) -> Option { self.fees.excess_blob_gas_and_price() } /// Sets the current basefee - pub fn set_base_fee(&self, basefee: u128) { + pub fn set_base_fee(&self, basefee: u64) { self.fees.set_base_fee(basefee) } @@ -693,21 +699,21 @@ impl Backend { self.blockchain.storage.read().total_difficulty } - /// Creates a new `evm_snapshot` at the current height + /// Creates a new `evm_snapshot` at the current height. /// - /// Returns the id of the snapshot created - pub async fn create_snapshot(&self) -> U256 { + /// Returns the id of the snapshot created. + pub async fn create_state_snapshot(&self) -> U256 { let num = self.best_number(); let hash = self.best_hash(); - let id = self.db.write().await.snapshot(); + let id = self.db.write().await.snapshot_state(); trace!(target: "backend", "creating snapshot {} at {}", id, num); - self.active_snapshots.lock().insert(id, (num, hash)); + self.active_state_snapshots.lock().insert(id, (num, hash)); id } - /// Reverts the state to the snapshot identified by the given `id`. - pub async fn revert_snapshot(&self, id: U256) -> Result { - let block = { self.active_snapshots.lock().remove(&id) }; + /// Reverts the state to the state snapshot identified by the given `id`. + pub async fn revert_state_snapshot(&self, id: U256) -> Result { + let block = { self.active_state_snapshots.lock().remove(&id) }; if let Some((num, hash)) = block { let best_block_hash = { // revert the storage that's newer than the snapshot @@ -750,11 +756,11 @@ impl Backend { ..Default::default() }; } - Ok(self.db.write().await.revert(id, RevertSnapshotAction::RevertRemove)) + Ok(self.db.write().await.revert_state(id, RevertStateSnapshotAction::RevertRemove)) } - pub fn list_snapshots(&self) -> BTreeMap { - self.active_snapshots.lock().clone().into_iter().collect() + pub fn list_state_snapshots(&self) -> BTreeMap { + self.active_state_snapshots.lock().clone().into_iter().collect() } /// Get the current state. @@ -800,14 +806,28 @@ impl Backend { /// Apply [SerializableState] data to the backend storage. pub async fn load_state(&self, state: SerializableState) -> Result { + // load the blocks and transactions into the storage + self.blockchain.storage.write().load_blocks(state.blocks.clone()); + self.blockchain.storage.write().load_transactions(state.transactions.clone()); // reset the block env if let Some(block) = state.block.clone() { self.env.write().block = block.clone(); // Set the current best block number. // Defaults to block number for compatibility with existing state files. - self.blockchain.storage.write().best_number = - state.best_block_number.unwrap_or(block.number.to::()); + + let best_number = state.best_block_number.unwrap_or(block.number.to::()); + self.blockchain.storage.write().best_number = best_number; + + // Set the current best block hash; + let best_hash = + self.blockchain.storage.read().hash(best_number.into()).ok_or_else(|| { + BlockchainError::RpcError(RpcError::internal_error_with(format!( + "Best hash not found for best number {best_number}", + ))) + })?; + + self.blockchain.storage.write().best_hash = best_hash; } if !self.db.write().await.load_state(state.clone())? { @@ -817,9 +837,6 @@ impl Backend { .into()); } - self.blockchain.storage.write().load_blocks(state.blocks.clone()); - self.blockchain.storage.write().load_transactions(state.transactions.clone()); - if let Some(historical_states) = state.historical_states { self.states.write().load_states(historical_states); } @@ -857,17 +874,20 @@ impl Backend { } /// Creates an EVM instance with optionally injected precompiles. - fn new_evm_with_inspector_ref( + #[allow(clippy::type_complexity)] + fn new_evm_with_inspector_ref<'i, 'db>( &self, - db: DB, + db: &'db dyn DatabaseRef, env: EnvWithHandlerCfg, - inspector: I, - ) -> revm::Evm<'_, I, WrapDatabaseRef> - where - DB: revm::DatabaseRef, - I: InspectorExt>, - { - let mut evm = new_evm_with_inspector_ref(db, env, inspector); + inspector: &'i mut dyn revm::Inspector< + WrapDatabaseRef<&'db dyn DatabaseRef>, + >, + ) -> revm::Evm< + '_, + &'i mut dyn revm::Inspector>>, + WrapDatabaseRef<&'db dyn DatabaseRef>, + > { + let mut evm = new_evm_with_inspector_ref(db, env, inspector, self.alphanet); if let Some(factory) = &self.precompile_factory { inject_precompiles(&mut evm, factory.precompiles()); } @@ -892,7 +912,7 @@ impl Backend { let db = self.db.read().await; let mut inspector = self.build_inspector(); - let mut evm = self.new_evm_with_inspector_ref(&**db, env, &mut inspector); + let mut evm = self.new_evm_with_inspector_ref(db.as_dyn(), env, &mut inspector); let ResultAndState { result, state } = evm.transact()?; let (exit_reason, gas_used, out, logs) = match result { ExecutionResult::Success { reason, gas_used, logs, output, .. } => { @@ -1011,7 +1031,7 @@ impl Backend { env.block.timestamp = U256::from(self.time.next_timestamp()); let executor = TransactionExecutor { - db: &mut *db, + db: &mut **db, validator: self, pending: pool_transactions.into_iter(), block_env: env.block.clone(), @@ -1068,7 +1088,7 @@ impl Backend { // log some tx info node_info!(" Transaction: {:?}", info.transaction_hash); if let Some(contract) = &info.contract_address { - node_info!(" Contract created: {contract:?}"); + node_info!(" Contract created: {contract}"); } node_info!(" Gas used: {}", receipt.cumulative_gas_used()); if !info.exit.is_ok() { @@ -1109,20 +1129,25 @@ impl Backend { node_info!(" Block Number: {}", block_number); node_info!(" Block Hash: {:?}", block_hash); - node_info!(" Block Time: {:?}\n", timestamp.to_rfc2822()); + if timestamp.year() > 9999 { + // rf2822 panics with more than 4 digits + node_info!(" Block Time: {:?}\n", timestamp.to_rfc3339()); + } else { + node_info!(" Block Time: {:?}\n", timestamp.to_rfc2822()); + } let outcome = MinedBlockOutcome { block_number, included, invalid }; (outcome, header, block_hash) }; let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas( - header.gas_used, - header.gas_limit, + header.gas_used as u128, + header.gas_limit as u128, header.base_fee_per_gas.unwrap_or_default(), ); let next_block_excess_blob_gas = self.fees.get_next_block_blob_excess_gas( - header.excess_blob_gas.unwrap_or_default(), - header.blob_gas_used.unwrap_or_default(), + header.excess_blob_gas.map(|g| g as u128).unwrap_or_default(), + header.blob_gas_used.map(|g| g as u128).unwrap_or_default(), ); // update next base fee @@ -1151,10 +1176,10 @@ impl Backend { self.with_database_at(block_request, |state, block| { let block_number = block.number.to::(); let (exit, out, gas, state) = match overrides { - None => self.call_with_state(state, request, fee_details, block), + None => self.call_with_state(state.as_dyn(), request, fee_details, block), Some(overrides) => { let state = state::apply_state_override(overrides.into_iter().collect(), state)?; - self.call_with_state(state, request, fee_details, block) + self.call_with_state(state.as_dyn(), request, fee_details, block) }, }?; trace!(target: "backend", "call return {:?} out: {:?} gas {} on block {}", exit, out, gas, block_number); @@ -1222,26 +1247,36 @@ impl Backend { }); let caller = from.unwrap_or_default(); let to = to.as_ref().and_then(TxKind::to); - env.tx = TxEnv { - caller, - gas_limit: gas_limit as u64, - gas_price: U256::from(gas_price), - gas_priority_fee: max_priority_fee_per_gas.map(U256::from), - max_fee_per_blob_gas: max_fee_per_blob_gas.map(U256::from), - transact_to: match to { - Some(addr) => TxKind::Call(*addr), - None => TxKind::Create, - }, - value: value.unwrap_or_default(), - data: input.into_input().unwrap_or_default(), - chain_id: None, - // set nonce to None so that the correct nonce is chosen by the EVM - nonce: None, - access_list: access_list.unwrap_or_default().into(), - blob_hashes: blob_versioned_hashes.unwrap_or_default(), - optimism: OptimismFields { enveloped_tx: Some(Bytes::new()), ..Default::default() }, - authorization_list: authorization_list.map(Into::into), - }; + let blob_hashes = blob_versioned_hashes.unwrap_or_default(); + env.tx = + TxEnv { + caller, + gas_limit, + gas_price: U256::from(gas_price), + gas_priority_fee: max_priority_fee_per_gas.map(U256::from), + max_fee_per_blob_gas: max_fee_per_blob_gas + .or_else(|| { + if !blob_hashes.is_empty() { + env.block.get_blob_gasprice() + } else { + None + } + }) + .map(U256::from), + transact_to: match to { + Some(addr) => TxKind::Call(*addr), + None => TxKind::Create, + }, + value: value.unwrap_or_default(), + data: input.into_input().unwrap_or_default(), + chain_id: None, + // set nonce to None so that the correct nonce is chosen by the EVM + nonce: None, + access_list: access_list.unwrap_or_default().into(), + blob_hashes, + optimism: OptimismFields { enveloped_tx: Some(Bytes::new()), ..Default::default() }, + authorization_list: authorization_list.map(Into::into), + }; if env.block.basefee.is_zero() { // this is an edge case because the evm fails if `tx.effective_gas_price < base_fee` @@ -1254,7 +1289,7 @@ impl Backend { /// Builds [`Inspector`] with the configured options fn build_inspector(&self) -> Inspector { - let mut inspector = Inspector::default().with_alphanet(self.alphanet); + let mut inspector = Inspector::default(); if self.print_logs { inspector = inspector.with_log_collector(); @@ -1263,16 +1298,13 @@ impl Backend { inspector } - pub fn call_with_state( + pub fn call_with_state( &self, - state: D, + state: &dyn DatabaseRef, request: WithOtherFields, fee_details: FeeDetails, block_env: BlockEnv, - ) -> Result<(InstructionResult, Option, u128, State), BlockchainError> - where - D: DatabaseRef, - { + ) -> Result<(InstructionResult, Option, u128, State), BlockchainError> { let mut inspector = self.build_inspector(); let env = self.build_call_env(request, fee_details, block_env); @@ -1319,8 +1351,11 @@ impl Backend { ); let env = self.build_call_env(request, fee_details, block); - let mut evm = - self.new_evm_with_inspector_ref(state, env, &mut inspector); + let mut evm = self.new_evm_with_inspector_ref( + state.as_dyn(), + env, + &mut inspector, + ); let ResultAndState { result, state: _ } = evm.transact()?; drop(evm); @@ -1352,7 +1387,7 @@ impl Backend { .with_tracing_config(TracingInspectorConfig::from_geth_config(&config)); let env = self.build_call_env(request, fee_details, block); - let mut evm = self.new_evm_with_inspector_ref(state, env, &mut inspector); + let mut evm = self.new_evm_with_inspector_ref(state.as_dyn(), env, &mut inspector); let ResultAndState { result, state: _ } = evm.transact()?; let (exit_reason, gas_used, out) = match result { @@ -1381,16 +1416,13 @@ impl Backend { .await? } - pub fn build_access_list_with_state( + pub fn build_access_list_with_state( &self, - state: D, + state: &dyn DatabaseRef, request: WithOtherFields, fee_details: FeeDetails, block_env: BlockEnv, - ) -> Result<(InstructionResult, Option, u64, AccessList), BlockchainError> - where - D: DatabaseRef, - { + ) -> Result<(InstructionResult, Option, u64, AccessList), BlockchainError> { let from = request.from.unwrap_or_default(); let to = if let Some(TxKind::Call(to)) = request.to { to @@ -1911,7 +1943,7 @@ impl Backend { let db = self.db.read().await; let block = self.env.read().block.clone(); - Ok(f(Box::new(&*db), block)) + Ok(f(Box::new(&**db), block)) } pub async fn storage_at( @@ -1937,17 +1969,14 @@ impl Backend { address: Address, block_request: Option, ) -> Result { - self.with_database_at(block_request, |db, _| self.get_code_with_state(db, address)).await? + self.with_database_at(block_request, |db, _| self.get_code_with_state(&db, address)).await? } - pub fn get_code_with_state( + pub fn get_code_with_state( &self, - state: D, + state: &dyn DatabaseRef, address: Address, - ) -> Result - where - D: DatabaseRef, - { + ) -> Result { trace!(target: "backend", "get code for {:?}", address); let account = state.basic_ref(address)?.unwrap_or_default(); if account.code_hash == KECCAK_EMPTY { @@ -2221,7 +2250,7 @@ impl Backend { // Cancun specific let excess_blob_gas = block.header.excess_blob_gas; - let blob_gas_price = calc_blob_gasprice(excess_blob_gas.map_or(0, |g| g as u64)); + let blob_gas_price = calc_blob_gasprice(excess_blob_gas.unwrap_or_default()); let blob_gas_used = transaction.blob_gas(); let effective_gas_price = match transaction.transaction { @@ -2230,17 +2259,17 @@ impl Backend { TypedTransaction::EIP1559(t) => block .header .base_fee_per_gas - .unwrap_or_else(|| self.base_fee()) + .map_or(self.base_fee() as u128, |g| g as u128) .saturating_add(t.tx().max_priority_fee_per_gas), TypedTransaction::EIP4844(t) => block .header .base_fee_per_gas - .unwrap_or_else(|| self.base_fee()) + .map_or(self.base_fee() as u128, |g| g as u128) .saturating_add(t.tx().tx().max_priority_fee_per_gas), TypedTransaction::EIP7702(t) => block .header .base_fee_per_gas - .unwrap_or_else(|| self.base_fee()) + .map_or(self.base_fee() as u128, |g| g as u128) .saturating_add(t.tx().max_priority_fee_per_gas), TypedTransaction::Deposit(_) => 0_u128, }; @@ -2289,7 +2318,7 @@ impl Backend { transaction_hash: info.transaction_hash, transaction_index: Some(info.transaction_index), block_number: Some(block.header.number), - gas_used: info.gas_used, + gas_used: info.gas_used as u128, contract_address: info.contract_address, effective_gas_price, block_hash: Some(block_hash), @@ -2297,7 +2326,7 @@ impl Backend { to: info.to, state_root: Some(block.header.state_root), blob_gas_price: Some(blob_gas_price), - blob_gas_used, + blob_gas_used: blob_gas_used.map(|g| g as u128), authorization_list: None, }; @@ -2447,7 +2476,12 @@ impl Backend { let _ = builder.root(); - let proof = builder.take_proofs().values().cloned().collect::>(); + let proof = builder + .take_proof_nodes() + .into_nodes_sorted() + .into_iter() + .map(|(_, v)| v) + .collect(); let storage_proofs = prove_storage(&account.storage, &keys); let account_proof = AccountProof { @@ -2620,7 +2654,7 @@ impl TransactionValidator for Backend { } } - if tx.gas_limit() < MIN_TRANSACTION_GAS { + if tx.gas_limit() < MIN_TRANSACTION_GAS as u64 { warn!(target: "backend", "[{:?}] gas too low", tx.hash()); return Err(InvalidTransactionError::GasTooLow); } @@ -2746,7 +2780,7 @@ pub fn transaction_build( eth_transaction: MaybeImpersonatedTransaction, block: Option<&Block>, info: Option, - base_fee: Option, + base_fee: Option, ) -> WithOtherFields { let mut transaction: Transaction = eth_transaction.clone().into(); if info.is_some() && transaction.transaction_type == Some(0x7E) { @@ -2760,7 +2794,7 @@ pub fn transaction_build( } else { // if transaction is already mined, gas price is considered base fee + priority fee: the // effective gas price. - let base_fee = base_fee.unwrap_or(0u128); + let base_fee = base_fee.map_or(0u128, |g| g as u128); let max_priority_fee_per_gas = transaction.max_priority_fee_per_gas.unwrap_or(0); transaction.gas_price = Some(base_fee.saturating_add(max_priority_fee_per_gas)); } @@ -2816,15 +2850,13 @@ pub fn prove_storage(storage: &HashMap, keys: &[B256]) -> Vec, + states: B256HashMap, /// states which data is moved to disk - on_disk_states: HashMap, + on_disk_states: B256HashMap, /// How many states to store at most in_memory_limit: usize, /// minimum amount of states we keep in memory @@ -147,8 +146,8 @@ impl InMemoryBlockStates { { // only write to disk if supported if !self.is_memory_only() { - let snapshot = state.0.clear_into_snapshot(); - self.disk_cache.write(hash, snapshot); + 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); } @@ -170,7 +169,7 @@ impl InMemoryBlockStates { self.states.get(hash).or_else(|| { if let Some(state) = self.on_disk_states.get_mut(hash) { if let Some(cached) = self.disk_cache.read(*hash) { - state.init_from_snapshot(cached); + state.init_from_state_snapshot(cached); return Some(state); } } @@ -204,8 +203,8 @@ impl InMemoryBlockStates { // Get on-disk state snapshots self.on_disk_states.iter().for_each(|(hash, _)| { - if let Some(snapshot) = self.disk_cache.read(*hash) { - states.push((*hash, snapshot)); + if let Some(state_snapshot) = self.disk_cache.read(*hash) { + states.push((*hash, state_snapshot)); } }); @@ -214,9 +213,9 @@ impl InMemoryBlockStates { /// Load states from serialized data pub fn load_states(&mut self, states: SerializableHistoricalStates) { - for (hash, snapshot) in states { + for (hash, state_snapshot) in states { let mut state_db = StateDb::new(MemDb::default()); - state_db.init_from_snapshot(snapshot); + state_db.init_from_state_snapshot(state_snapshot); self.insert(hash, state_db); } } @@ -245,7 +244,7 @@ impl Default for InMemoryBlockStates { #[derive(Clone)] pub struct BlockchainStorage { /// all stored blocks (block hash -> block) - pub blocks: HashMap, + pub blocks: B256HashMap, /// mapping from block number -> block hash pub hashes: HashMap, /// The current best hash @@ -256,23 +255,23 @@ pub struct BlockchainStorage { pub genesis_hash: B256, /// Mapping from the transaction hash to a tuple containing the transaction as well as the /// transaction receipt - pub transactions: HashMap, + pub transactions: B256HashMap, /// The total difficulty of the chain until this block pub total_difficulty: U256, } impl BlockchainStorage { /// Creates a new storage with a genesis block - pub fn new(env: &Env, base_fee: Option, timestamp: u64) -> Self { + pub fn new(env: &Env, base_fee: Option, timestamp: u64) -> Self { // create a dummy genesis block let partial_header = PartialHeader { timestamp, base_fee, - gas_limit: env.block.gas_limit.to::(), + gas_limit: env.block.gas_limit.to::(), beneficiary: env.block.coinbase, difficulty: env.block.difficulty, blob_gas_used: env.block.blob_excess_gas_and_price.as_ref().map(|_| 0), - excess_blob_gas: env.block.get_blob_excess_gas().map(|v| v as u128), + excess_blob_gas: env.block.get_blob_excess_gas(), ..Default::default() }; let block = Block::new::(partial_header, vec![], vec![]); @@ -280,9 +279,14 @@ impl BlockchainStorage { let best_hash = genesis_hash; let best_number: U64 = U64::from(0u64); + let mut blocks = B256HashMap::default(); + blocks.insert(genesis_hash, block); + + let mut hashes = HashMap::default(); + hashes.insert(best_number, genesis_hash); Self { - blocks: HashMap::from([(genesis_hash, block)]), - hashes: HashMap::from([(best_number, genesis_hash)]), + blocks, + hashes, best_hash, best_number, genesis_hash, @@ -292,9 +296,12 @@ impl BlockchainStorage { } pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self { + let mut hashes = HashMap::default(); + hashes.insert(U64::from(block_number), block_hash); + Self { - blocks: Default::default(), - hashes: HashMap::from([(U64::from(block_number), block_hash)]), + blocks: B256HashMap::default(), + hashes, best_hash: block_hash, best_number: U64::from(block_number), genesis_hash: Default::default(), @@ -416,7 +423,7 @@ pub struct Blockchain { impl Blockchain { /// Creates a new storage with a genesis block - pub fn new(env: &Env, base_fee: Option, timestamp: u64) -> Self { + pub fn new(env: &Env, base_fee: Option, timestamp: u64) -> Self { Self { storage: Arc::new(RwLock::new(BlockchainStorage::new(env, base_fee, timestamp))) } } @@ -569,11 +576,12 @@ impl MinedTransaction { pub struct MinedTransactionReceipt { /// The actual json rpc receipt object pub inner: ReceiptResponse, - /// Output data fo the transaction + /// Output data for the transaction pub out: Option, } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; use crate::eth::backend::db::Db; @@ -702,7 +710,7 @@ mod tests { load_storage.load_transactions(serialized_transactions); let loaded_block = load_storage.blocks.get(&block_hash).unwrap(); - assert_eq!(loaded_block.header.gas_limit, partial_header.gas_limit); + assert_eq!(loaded_block.header.gas_limit, { partial_header.gas_limit }); let loaded_tx = loaded_block.transactions.first().unwrap(); assert_eq!(loaded_tx, &tx); } diff --git a/crates/anvil/src/eth/backend/validate.rs b/crates/anvil/src/eth/backend/validate.rs index 650ce24a5..eca3fd9e3 100644 --- a/crates/anvil/src/eth/backend/validate.rs +++ b/crates/anvil/src/eth/backend/validate.rs @@ -6,7 +6,6 @@ use foundry_evm::revm::primitives::{AccountInfo, EnvWithHandlerCfg}; /// A trait for validating transactions #[async_trait::async_trait] -#[auto_impl::auto_impl(&, Box)] pub trait TransactionValidator { /// Validates the transaction's validity when it comes to nonce, payment /// diff --git a/crates/anvil/src/eth/error.rs b/crates/anvil/src/eth/error.rs index ada9e6c53..31d0521bb 100644 --- a/crates/anvil/src/eth/error.rs +++ b/crates/anvil/src/eth/error.rs @@ -388,7 +388,7 @@ impl ToRpcResponseResult for Result { match err { TransportError::ErrorResp(err) => RpcError { code: ErrorCode::from(err.code), - message: err.message.into(), + message: err.message, data: err.data.and_then(|data| serde_json::to_value(data).ok()), }, err => RpcError::internal_error_with(format!("Fork Error: {err:?}")), diff --git a/crates/anvil/src/eth/fees.rs b/crates/anvil/src/eth/fees.rs index 45b33ad0f..f41c51505 100644 --- a/crates/anvil/src/eth/fees.rs +++ b/crates/anvil/src/eth/fees.rs @@ -24,7 +24,7 @@ use std::{ pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64; /// Initial base fee for EIP-1559 blocks. -pub const INITIAL_BASE_FEE: u128 = 1_000_000_000; +pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; /// Initial default gas price for the first block pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000; @@ -47,7 +47,9 @@ pub struct FeeManager { /// Tracks the base fee for the next block post London /// /// This value will be updated after a new block was mined - base_fee: Arc>, + base_fee: Arc>, + /// Whether the minimum suggested priority fee is enforced + is_min_priority_fee_enforced: bool, /// Tracks the excess blob gas, and the base fee, for the next block post Cancun /// /// This value will be updated after a new block was mined @@ -62,13 +64,15 @@ pub struct FeeManager { impl FeeManager { pub fn new( spec_id: SpecId, - base_fee: u128, + base_fee: u64, + is_min_priority_fee_enforced: bool, gas_price: u128, blob_excess_gas_and_price: BlobExcessGasAndPrice, ) -> Self { Self { spec_id, base_fee: Arc::new(RwLock::new(base_fee)), + is_min_priority_fee_enforced, gas_price: Arc::new(RwLock::new(gas_price)), blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)), elasticity: Arc::new(RwLock::new(default_elasticity())), @@ -97,7 +101,7 @@ impl FeeManager { } } - pub fn base_fee(&self) -> u128 { + pub fn base_fee(&self) -> u64 { if self.is_eip1559() { *self.base_fee.read() } else { @@ -105,6 +109,10 @@ impl FeeManager { } } + pub fn is_min_priority_fee_enforced(&self) -> bool { + self.is_min_priority_fee_enforced + } + /// Raw base gas price pub fn raw_gas_price(&self) -> u128 { *self.gas_price.read() @@ -133,7 +141,7 @@ impl FeeManager { } /// Returns the current base fee - pub fn set_base_fee(&self, fee: u128) { + pub fn set_base_fee(&self, fee: u64) { trace!(target: "backend::fees", "updated base fee {:?}", fee); let mut base = self.base_fee.write(); *base = fee; @@ -151,8 +159,8 @@ impl FeeManager { &self, gas_used: u128, gas_limit: u128, - last_fee_per_gas: u128, - ) -> u128 { + last_fee_per_gas: u64, + ) -> u64 { // It's naturally impossible for base fee to be 0; // It means it was set by the user deliberately and therefore we treat it as a constant. // Therefore, we skip the base fee calculation altogether and we return 0. @@ -179,8 +187,8 @@ impl FeeManager { } /// Calculate base fee for next block. [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) spec -pub fn calculate_next_block_base_fee(gas_used: u128, gas_limit: u128, base_fee: u128) -> u128 { - calc_next_block_base_fee(gas_used, gas_limit, base_fee, BaseFeeParams::ethereum()) +pub fn calculate_next_block_base_fee(gas_used: u128, gas_limit: u128, base_fee: u64) -> u64 { + calc_next_block_base_fee(gas_used as u64, gas_limit as u64, base_fee, BaseFeeParams::ethereum()) } /// An async service that takes care of the `FeeHistory` cache @@ -235,9 +243,9 @@ impl FeeHistoryService { }; let mut block_number: Option = None; - let base_fee = header.base_fee_per_gas.unwrap_or_default(); - let excess_blob_gas = header.excess_blob_gas; - let blob_gas_used = header.blob_gas_used; + let base_fee = header.base_fee_per_gas.map(|g| g as u128).unwrap_or_default(); + let excess_blob_gas = header.excess_blob_gas.map(|g| g as u128); + let blob_gas_used = header.blob_gas_used.map(|g| g as u128); let base_fee_per_blob_gas = header.blob_fee(); let mut item = FeeHistoryCacheItem { base_fee, @@ -464,7 +472,7 @@ impl FeeDetails { impl fmt::Debug for FeeDetails { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "Fees {{ ")?; - write!(fmt, "gaPrice: {:?}, ", self.gas_price)?; + write!(fmt, "gas_price: {:?}, ", self.gas_price)?; write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?; write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?; write!(fmt, "}}")?; diff --git a/crates/anvil/src/eth/pool/mod.rs b/crates/anvil/src/eth/pool/mod.rs index 9ef45ace2..544d7eac9 100644 --- a/crates/anvil/src/eth/pool/mod.rs +++ b/crates/anvil/src/eth/pool/mod.rs @@ -20,7 +20,7 @@ //! used to determine whether it can be included in a block (transaction is ready) or whether it //! still _requires_ other transactions to be mined first (transaction is pending). //! A transaction is associated with the nonce of the account it's sent from. A unique identifying -//! marker for a transaction is therefor the pair `(nonce + account)`. An incoming transaction with +//! marker for a transaction is therefore the pair `(nonce + account)`. An incoming transaction with //! a `nonce > nonce on chain` will _require_ `(nonce -1, account)` first, before it is ready to be //! included in a block. //! diff --git a/crates/anvil/src/eth/pool/transactions.rs b/crates/anvil/src/eth/pool/transactions.rs index f0987572b..631064549 100644 --- a/crates/anvil/src/eth/pool/transactions.rs +++ b/crates/anvil/src/eth/pool/transactions.rs @@ -1,16 +1,13 @@ use crate::eth::{error::PoolError, util::hex_fmt_many}; -use alloy_primitives::{Address, TxHash}; +use alloy_primitives::{ + map::{HashMap, HashSet}, + Address, TxHash, +}; use alloy_rpc_types::Transaction as RpcTransaction; +use alloy_serde::WithOtherFields; use anvil_core::eth::transaction::{PendingTransaction, TypedTransaction}; use parking_lot::RwLock; -use std::{ - cmp::Ordering, - collections::{BTreeSet, HashMap, HashSet}, - fmt, - str::FromStr, - sync::Arc, - time::Instant, -}; +use std::{cmp::Ordering, collections::BTreeSet, fmt, str::FromStr, sync::Arc, time::Instant}; /// A unique identifying marker for a transaction pub type TxMarker = Vec; @@ -116,9 +113,9 @@ impl fmt::Debug for PoolTransaction { } } -impl TryFrom for PoolTransaction { +impl TryFrom> for PoolTransaction { type Error = eyre::Error; - fn try_from(transaction: RpcTransaction) -> Result { + fn try_from(transaction: WithOtherFields) -> Result { let typed_transaction = TypedTransaction::try_from(transaction)?; let pending_transaction = PendingTransaction::new(typed_transaction)?; Ok(Self { diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index d921b18d3..5f99ef9ca 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -2,14 +2,13 @@ use crate::eth::error::BlockchainError; use alloy_consensus::SignableTransaction; use alloy_dyn_abi::TypedData; use alloy_network::TxSignerSync; -use alloy_primitives::{Address, Signature, B256}; +use alloy_primitives::{map::AddressHashMap, Address, Signature, B256, U256}; use alloy_signer::Signer as AlloySigner; use alloy_signer_local::PrivateKeySigner; use anvil_core::eth::transaction::{ - optimism::{DepositTransaction, DepositTransactionRequest}, - TypedTransaction, TypedTransactionRequest, + optimism::DepositTransaction, TypedTransaction, TypedTransactionRequest, }; -use std::collections::HashMap; +use op_alloy_consensus::TxDeposit; /// A transaction signer #[async_trait::async_trait] @@ -47,7 +46,7 @@ pub trait Signer: Send + Sync { /// Maintains developer keys pub struct DevSigner { addresses: Vec
, - accounts: HashMap, + accounts: AddressHashMap, } impl DevSigner { @@ -106,7 +105,9 @@ impl Signer for DevSigner { TypedTransactionRequest::EIP2930(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?), TypedTransactionRequest::EIP1559(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?), TypedTransactionRequest::EIP4844(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?), - TypedTransactionRequest::Deposit(mut tx) => Ok(signer.sign_transaction_sync(&mut tx)?), + TypedTransactionRequest::Deposit(_) => { + unreachable!("op deposit txs should not be signed") + } } } } @@ -132,26 +133,26 @@ pub fn build_typed_transaction( TypedTransaction::EIP4844(tx.into_signed(signature)) } TypedTransactionRequest::Deposit(tx) => { - let DepositTransactionRequest { + let TxDeposit { from, gas_limit, - kind, + to, value, input, source_hash, mint, - is_system_tx, + is_system_transaction, .. } = tx; TypedTransaction::Deposit(DepositTransaction { from, gas_limit, - kind, + kind: to, value, input, source_hash, - mint, - is_system_tx, + mint: mint.map_or(U256::ZERO, U256::from), + is_system_tx: is_system_transaction, nonce: 0, }) } diff --git a/crates/anvil/src/eth/util.rs b/crates/anvil/src/eth/util.rs index 6bcde67d5..ca66f2ed3 100644 --- a/crates/anvil/src/eth/util.rs +++ b/crates/anvil/src/eth/util.rs @@ -29,7 +29,7 @@ impl<'a> HexDisplay<'a> { } } -impl<'a> fmt::Display for HexDisplay<'a> { +impl fmt::Display for HexDisplay<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.0.len() < 1027 { for byte in self.0 { @@ -48,7 +48,7 @@ impl<'a> fmt::Display for HexDisplay<'a> { } } -impl<'a> fmt::Debug for HexDisplay<'a> { +impl fmt::Debug for HexDisplay<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for byte in self.0 { f.write_fmt(format_args!("{byte:02x}"))?; diff --git a/crates/anvil/src/filter.rs b/crates/anvil/src/filter.rs index 2144ce27c..c0c8e7aef 100644 --- a/crates/anvil/src/filter.rs +++ b/crates/anvil/src/filter.rs @@ -4,13 +4,12 @@ use crate::{ pubsub::filter_logs, StorageInfo, }; -use alloy_primitives::TxHash; +use alloy_primitives::{map::HashMap, TxHash}; use alloy_rpc_types::{Filter, FilteredParams, Log}; use anvil_core::eth::subscription::SubscriptionId; use anvil_rpc::response::ResponseResult; use futures::{channel::mpsc::Receiver, Stream, StreamExt}; use std::{ - collections::HashMap, pin::Pin, sync::Arc, task::{Context, Poll}, diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index c800131d2..ee66caa67 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -45,7 +45,9 @@ use tokio::{ mod service; mod config; -pub use config::{AccountGenerator, ForkChoice, NodeConfig, CHAIN_ID, VERSION_MESSAGE}; +pub use config::{ + AccountGenerator, ForkChoice, NodeConfig, CHAIN_ID, DEFAULT_GAS_LIMIT, VERSION_MESSAGE, +}; mod hardfork; pub use hardfork::EthereumHardfork; @@ -253,32 +255,41 @@ pub async fn try_spawn(mut config: NodeConfig) -> io::Result<(EthApi, NodeHandle type IpcTask = JoinHandle<()>; -/// A handle to the spawned node and server tasks +/// A handle to the spawned node and server tasks. /// /// This future will resolve if either the node or server task resolve/fail. pub struct NodeHandle { config: NodeConfig, - /// The address of the running rpc server + /// The address of the running rpc server. addresses: Vec, - /// Join handle for the Node Service + /// Join handle for the Node Service. pub node_service: JoinHandle>, /// Join handles (one per socket) for the Anvil server. pub servers: Vec>>, - // The future that joins the ipc server, if any + /// The future that joins the ipc server, if any. ipc_task: Option, /// A signal that fires the shutdown, fired on drop. _signal: Option, - /// A task manager that can be used to spawn additional tasks + /// A task manager that can be used to spawn additional tasks. task_manager: TaskManager, } +impl Drop for NodeHandle { + fn drop(&mut self) { + // Fire shutdown signal to make sure anvil instance is terminated. + if let Some(signal) = self._signal.take() { + signal.fire().unwrap() + } + } +} + impl NodeHandle { - /// The [NodeConfig] the node was launched with + /// The [NodeConfig] the node was launched with. pub fn config(&self) -> &NodeConfig { &self.config } - /// Prints the launch info + /// Prints the launch info. pub(crate) fn print(&self, fork: Option<&ClientFork>) { self.config.print(fork); if !self.config.silent { @@ -296,25 +307,25 @@ impl NodeHandle { } } - /// The address of the launched server + /// The address of the launched server. /// /// **N.B.** this may not necessarily be the same `host + port` as configured in the - /// `NodeConfig`, if port was set to 0, then the OS auto picks an available port + /// `NodeConfig`, if port was set to 0, then the OS auto picks an available port. pub fn socket_address(&self) -> &SocketAddr { &self.addresses[0] } - /// Returns the http endpoint + /// Returns the http endpoint. pub fn http_endpoint(&self) -> String { format!("http://{}", self.socket_address()) } - /// Returns the websocket endpoint + /// Returns the websocket endpoint. pub fn ws_endpoint(&self) -> String { format!("ws://{}", self.socket_address()) } - /// Returns the path of the launched ipc server, if any + /// Returns the path of the launched ipc server, if any. pub fn ipc_path(&self) -> Option { self.config.get_ipc_path() } @@ -334,44 +345,44 @@ impl NodeHandle { ProviderBuilder::new(&self.config.get_ipc_path()?).build().ok() } - /// Signer accounts that can sign messages/transactions from the EVM node + /// Signer accounts that can sign messages/transactions from the EVM node. pub fn dev_accounts(&self) -> impl Iterator + '_ { self.config.signer_accounts.iter().map(|wallet| wallet.address()) } - /// Signer accounts that can sign messages/transactions from the EVM node + /// Signer accounts that can sign messages/transactions from the EVM node. pub fn dev_wallets(&self) -> impl Iterator + '_ { self.config.signer_accounts.iter().cloned() } - /// Accounts that will be initialised with `genesis_balance` in the genesis block + /// Accounts that will be initialised with `genesis_balance` in the genesis block. pub fn genesis_accounts(&self) -> impl Iterator + '_ { self.config.genesis_accounts.iter().map(|w| w.address()) } - /// Native token balance of every genesis account in the genesis block + /// Native token balance of every genesis account in the genesis block. pub fn genesis_balance(&self) -> U256 { self.config.genesis_balance } - /// Default gas price for all txs + /// Default gas price for all txs. pub fn gas_price(&self) -> u128 { self.config.get_gas_price() } - /// Returns the shutdown signal + /// Returns the shutdown signal. pub fn shutdown_signal(&self) -> &Option { &self._signal } - /// Returns mutable access to the shutdown signal + /// Returns mutable access to the shutdown signal. /// - /// This can be used to extract the Signal + /// This can be used to extract the Signal. pub fn shutdown_signal_mut(&mut self) -> &mut Option { &mut self._signal } - /// Returns the task manager that can be used to spawn new tasks + /// Returns the task manager that can be used to spawn new tasks. /// /// ``` /// use anvil::NodeHandle; diff --git a/crates/anvil/tests/it/anvil.rs b/crates/anvil/tests/it/anvil.rs index b8aed751d..50e27c57a 100644 --- a/crates/anvil/tests/it/anvil.rs +++ b/crates/anvil/tests/it/anvil.rs @@ -1,5 +1,6 @@ //! tests for anvil specific logic +use alloy_eips::BlockNumberOrTag; use alloy_primitives::Address; use alloy_provider::Provider; use anvil::{spawn, NodeConfig}; @@ -76,3 +77,14 @@ async fn test_can_use_default_genesis_timestamp() { provider.get_block(0.into(), false.into()).await.unwrap().unwrap().header.timestamp ); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_can_handle_large_timestamp() { + let (api, _handle) = spawn(NodeConfig::test()).await; + let num = 317071597274; + api.evm_set_next_block_timestamp(num).unwrap(); + api.mine_one().await; + + let block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); + assert_eq!(block.header.timestamp, num); +} diff --git a/crates/anvil/tests/it/anvil_api.rs b/crates/anvil/tests/it/anvil_api.rs index 0e8001853..74728f94b 100644 --- a/crates/anvil/tests/it/anvil_api.rs +++ b/crates/anvil/tests/it/anvil_api.rs @@ -48,7 +48,7 @@ async fn can_set_block_gas_limit() { // Mine a new block, and check the new block gas limit api.mine_one().await; let latest_block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); - assert_eq!(block_gas_limit.to::(), latest_block.header.gas_limit); + assert_eq!(block_gas_limit.to::(), latest_block.header.gas_limit); } // Ref @@ -557,7 +557,7 @@ async fn test_get_transaction_receipt() { // the block should have the new base fee let block = provider.get_block(BlockId::default(), false.into()).await.unwrap().unwrap(); - assert_eq!(block.header.base_fee_per_gas.unwrap(), new_base_fee.to::()); + assert_eq!(block.header.base_fee_per_gas.unwrap(), new_base_fee.to::()); // mine blocks api.evm_mine(None).await.unwrap(); @@ -591,9 +591,9 @@ async fn test_fork_revert_next_block_timestamp() { api.mine_one().await; let latest_block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); - let snapshot_id = api.evm_snapshot().await.unwrap(); + let state_snapshot = api.evm_snapshot().await.unwrap(); api.mine_one().await; - api.evm_revert(snapshot_id).await.unwrap(); + api.evm_revert(state_snapshot).await.unwrap(); let block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); assert_eq!(block, latest_block); @@ -613,9 +613,9 @@ async fn test_fork_revert_call_latest_block_timestamp() { api.mine_one().await; let latest_block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); - let snapshot_id = api.evm_snapshot().await.unwrap(); + let state_snapshot = api.evm_snapshot().await.unwrap(); api.mine_one().await; - api.evm_revert(snapshot_id).await.unwrap(); + api.evm_revert(state_snapshot).await.unwrap(); let multicall_contract = Multicall::new(address!("eefba1e63905ef1d7acba5a8513c70307c1ce441"), &provider); diff --git a/crates/anvil/tests/it/api.rs b/crates/anvil/tests/it/api.rs index f9aaf0dba..c4172b265 100644 --- a/crates/anvil/tests/it/api.rs +++ b/crates/anvil/tests/it/api.rs @@ -5,7 +5,10 @@ use crate::{ utils::{connect_pubsub_with_wallet, http_provider_with_signer}, }; use alloy_network::{EthereumWallet, TransactionBuilder}; -use alloy_primitives::{Address, ChainId, B256, U256}; +use alloy_primitives::{ + map::{AddressHashMap, B256HashMap, HashMap}, + Address, ChainId, B256, U256, +}; use alloy_provider::Provider; use alloy_rpc_types::{ request::TransactionRequest, state::AccountOverride, BlockId, BlockNumberOrTag, @@ -14,7 +17,7 @@ use alloy_rpc_types::{ use alloy_serde::WithOtherFields; use anvil::{eth::api::CLIENT_VERSION, spawn, NodeConfig, CHAIN_ID}; use futures::join; -use std::{collections::HashMap, time::Duration}; +use std::time::Duration; #[tokio::test(flavor = "multi_thread")] async fn can_get_block_number() { @@ -174,7 +177,7 @@ async fn can_estimate_gas_with_undersized_max_fee_per_gas() { let simple_storage_contract = SimpleStorage::deploy(&provider, init_value.clone()).await.unwrap(); - let undersized_max_fee_per_gas = 1_u128; + let undersized_max_fee_per_gas = 1; let latest_block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); let latest_block_base_fee_per_gas = latest_block.header.base_fee_per_gas.unwrap(); @@ -183,7 +186,7 @@ async fn can_estimate_gas_with_undersized_max_fee_per_gas() { let estimated_gas = simple_storage_contract .setValue("new_value".to_string()) - .max_fee_per_gas(undersized_max_fee_per_gas) + .max_fee_per_gas(undersized_max_fee_per_gas.into()) .from(wallet.address()) .estimate_gas() .await @@ -255,7 +258,7 @@ async fn can_call_on_pending_block() { .call() .await .unwrap(); - assert_eq!(block.header.gas_limit, ret_gas_limit.to::()); + assert_eq!(block.header.gas_limit, ret_gas_limit.to::()); let Multicall::getCurrentBlockCoinbaseReturn { coinbase: ret_coinbase, .. } = contract .getCurrentBlockCoinbase() @@ -284,13 +287,13 @@ async fn can_call_with_undersized_max_fee_per_gas() { let latest_block = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); let latest_block_base_fee_per_gas = latest_block.header.base_fee_per_gas.unwrap(); - let undersized_max_fee_per_gas = 1_u128; + let undersized_max_fee_per_gas = 1; assert!(undersized_max_fee_per_gas < latest_block_base_fee_per_gas); let last_sender = simple_storage_contract .lastSender() - .max_fee_per_gas(undersized_max_fee_per_gas) + .max_fee_per_gas(undersized_max_fee_per_gas.into()) .from(wallet.address()) .call() .await @@ -319,23 +322,24 @@ async fn can_call_with_state_override() { // Test the `balance` account override let balance = U256::from(42u64); - let overrides = HashMap::from([( - account, - AccountOverride { balance: Some(balance), ..Default::default() }, - )]); + let mut overrides = AddressHashMap::default(); + overrides.insert(account, AccountOverride { balance: Some(balance), ..Default::default() }); let result = multicall_contract.getEthBalance(account).state(overrides).call().await.unwrap().balance; assert_eq!(result, balance); // Test the `state_diff` account override - let overrides = HashMap::from([( + let mut state_diff = B256HashMap::default(); + state_diff.insert(B256::ZERO, account.into_word()); + let mut overrides = AddressHashMap::default(); + overrides.insert( *simple_storage_contract.address(), AccountOverride { // The `lastSender` is in the first storage slot - state_diff: Some(HashMap::from([(B256::ZERO, account.into_word())])), + state_diff: Some(state_diff), ..Default::default() }, - )]); + ); let last_sender = simple_storage_contract.lastSender().state(HashMap::default()).call().await.unwrap()._0; @@ -352,14 +356,17 @@ async fn can_call_with_state_override() { assert_eq!(value, init_value); // Test the `state` account override - let overrides = HashMap::from([( + let mut state = B256HashMap::default(); + state.insert(B256::ZERO, account.into_word()); + let mut overrides = AddressHashMap::default(); + overrides.insert( *simple_storage_contract.address(), AccountOverride { // The `lastSender` is in the first storage slot - state: Some(HashMap::from([(B256::ZERO, account.into_word())])), + state: Some(state), ..Default::default() }, - )]); + ); let last_sender = simple_storage_contract.lastSender().state(overrides.clone()).call().await.unwrap()._0; diff --git a/crates/anvil/tests/it/eip4844.rs b/crates/anvil/tests/it/eip4844.rs index 353083409..a4243ce15 100644 --- a/crates/anvil/tests/it/eip4844.rs +++ b/crates/anvil/tests/it/eip4844.rs @@ -1,7 +1,7 @@ -use crate::utils::http_provider; +use crate::utils::{http_provider, http_provider_with_signer}; use alloy_consensus::{SidecarBuilder, SimpleCoder}; -use alloy_eips::eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK}; -use alloy_network::{TransactionBuilder, TransactionBuilder4844}; +use alloy_eips::eip4844::{BLOB_TX_MIN_BLOB_GASPRICE, DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK}; +use alloy_network::{EthereumWallet, TransactionBuilder, TransactionBuilder4844}; use alloy_primitives::U256; use alloy_provider::Provider; use alloy_rpc_types::{BlockId, TransactionRequest}; @@ -138,7 +138,7 @@ async fn can_mine_blobs_when_exceeds_max_blobs() { let first_batch = vec![1u8; DATA_GAS_PER_BLOB as usize * 3]; let sidecar: SidecarBuilder = SidecarBuilder::from_slice(&first_batch); - let num_blobs_first = sidecar.clone().take().len(); + let num_blobs_first = sidecar.clone().take().len() as u64; let sidecar = sidecar.build().unwrap(); @@ -160,7 +160,7 @@ async fn can_mine_blobs_when_exceeds_max_blobs() { let sidecar: SidecarBuilder = SidecarBuilder::from_slice(&second_batch); - let num_blobs_second = sidecar.clone().take().len(); + let num_blobs_second = sidecar.clone().take().len() as u64; let sidecar = sidecar.build().unwrap(); tx.set_blob_sidecar(sidecar); @@ -181,12 +181,12 @@ async fn can_mine_blobs_when_exceeds_max_blobs() { ); assert_eq!( first_block.unwrap().unwrap().header.blob_gas_used, - Some(DATA_GAS_PER_BLOB as u128 * num_blobs_first as u128) + Some(DATA_GAS_PER_BLOB * num_blobs_first) ); assert_eq!( second_block.unwrap().unwrap().header.blob_gas_used, - Some(DATA_GAS_PER_BLOB as u128 * num_blobs_second as u128) + Some(DATA_GAS_PER_BLOB * num_blobs_second) ); // Mined in two different blocks assert_eq!(first_receipt.block_number.unwrap() + 1, second_receipt.block_number.unwrap()); @@ -204,3 +204,90 @@ async fn can_check_blob_fields_on_genesis() { assert_eq!(block.header.blob_gas_used, Some(0)); assert_eq!(block.header.excess_blob_gas, Some(0)); } + +#[tokio::test(flavor = "multi_thread")] +async fn can_correctly_estimate_blob_gas_with_recommended_fillers() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into())); + let (_api, handle) = spawn(node_config).await; + + let provider = http_provider(&handle.http_endpoint()); + + let accounts = provider.get_accounts().await.unwrap(); + let alice = accounts[0]; + let bob = accounts[1]; + + let sidecar: SidecarBuilder = SidecarBuilder::from_slice(b"Blobs are fun!"); + let sidecar = sidecar.build().unwrap(); + + let tx = TransactionRequest::default().with_to(bob).with_blob_sidecar(sidecar); + let tx = WithOtherFields::new(tx); + + // Send the transaction and wait for the broadcast. + let pending_tx = provider.send_transaction(tx).await.unwrap(); + + println!("Pending transaction... {}", pending_tx.tx_hash()); + + // Wait for the transaction to be included and get the receipt. + let receipt = pending_tx.get_receipt().await.unwrap(); + + // Grab the processed transaction. + let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap(); + + println!( + "Transaction included in block {}", + receipt.block_number.expect("Failed to get block number") + ); + + assert!(tx.max_fee_per_blob_gas.unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE); + assert_eq!(receipt.from, alice); + assert_eq!(receipt.to, Some(bob)); + assert_eq!( + receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"), + DATA_GAS_PER_BLOB as u128 + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_correctly_estimate_blob_gas_with_recommended_fillers_with_signer() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into())); + let (_api, handle) = spawn(node_config).await; + + let signer = handle.dev_wallets().next().unwrap(); + let wallet: EthereumWallet = signer.clone().into(); + + let provider = http_provider_with_signer(&handle.http_endpoint(), wallet); + + let accounts = provider.get_accounts().await.unwrap(); + let alice = accounts[0]; + let bob = accounts[1]; + + let sidecar: SidecarBuilder = SidecarBuilder::from_slice(b"Blobs are fun!"); + let sidecar = sidecar.build().unwrap(); + + let tx = TransactionRequest::default().with_to(bob).with_blob_sidecar(sidecar); + let tx = WithOtherFields::new(tx); + + // Send the transaction and wait for the broadcast. + let pending_tx = provider.send_transaction(tx).await.unwrap(); + + println!("Pending transaction... {}", pending_tx.tx_hash()); + + // Wait for the transaction to be included and get the receipt. + let receipt = pending_tx.get_receipt().await.unwrap(); + + // Grab the processed transaction. + let tx = provider.get_transaction_by_hash(receipt.transaction_hash).await.unwrap().unwrap(); + + println!( + "Transaction included in block {}", + receipt.block_number.expect("Failed to get block number") + ); + + assert!(tx.max_fee_per_blob_gas.unwrap() >= BLOB_TX_MIN_BLOB_GASPRICE); + assert_eq!(receipt.from, alice); + assert_eq!(receipt.to, Some(bob)); + assert_eq!( + receipt.blob_gas_used.expect("Expected to be EIP-4844 transaction"), + DATA_GAS_PER_BLOB as u128 + ); +} diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index ad2ed1e04..e6db8a063 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -6,12 +6,12 @@ use crate::{ }; use alloy_chains::NamedChain; use alloy_network::{EthereumWallet, ReceiptResponse, TransactionBuilder}; -use alloy_primitives::{address, b256, bytes, Address, Bytes, TxHash, TxKind, U256}; +use alloy_primitives::{address, b256, bytes, uint, Address, Bytes, TxHash, TxKind, U256, U64}; use alloy_provider::Provider; use alloy_rpc_types::{ anvil::Forking, request::{TransactionInput, TransactionRequest}, - BlockId, BlockNumberOrTag, + BlockId, BlockNumberOrTag, BlockTransactionsKind, }; use alloy_serde::WithOtherFields; use alloy_signer_local::PrivateKeySigner; @@ -60,6 +60,37 @@ pub fn fork_config() -> NodeConfig { .silent() } +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_gas_limit_applied_from_config() { + let (api, _handle) = spawn(fork_config().with_gas_limit(Some(10_000_000_u128))).await; + + assert_eq!(api.gas_limit(), uint!(10_000_000_U256)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_gas_limit_disabled_from_config() { + let (api, handle) = spawn(fork_config().disable_block_gas_limit(true)).await; + + // see https://github.com/foundry-rs/foundry/pull/8933 + assert_eq!(api.gas_limit(), U256::from(U64::MAX)); + + // try to mine a couple blocks + let provider = handle.http_provider(); + let tx = TransactionRequest::default() + .to(Address::random()) + .value(U256::from(1337u64)) + .from(handle.dev_wallets().next().unwrap().address()); + let tx = WithOtherFields::new(tx); + let _ = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + let tx = TransactionRequest::default() + .to(Address::random()) + .value(U256::from(1337u64)) + .from(handle.dev_wallets().next().unwrap().address()); + let tx = WithOtherFields::new(tx); + let _ = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); +} + #[tokio::test(flavor = "multi_thread")] async fn test_spawn_fork() { let (api, _handle) = spawn(fork_config()).await; @@ -166,6 +197,26 @@ async fn test_fork_eth_get_nonce() { assert_eq!(api_nonce, provider_nonce); } +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_optimism_with_transaction_hash() { + use std::str::FromStr; + + // Fork to a block with a specific transaction + let fork_tx_hash = + TxHash::from_str("fcb864b5a50f0f0b111dbbf9e9167b2cb6179dfd6270e1ad53aac6049c0ec038") + .unwrap(); + let (api, _handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(rpc::next_rpc_endpoint(NamedChain::Optimism))) + .with_fork_transaction_hash(Some(fork_tx_hash)), + ) + .await; + + // Make sure the fork starts from previous block + let block_number = api.block_number().unwrap().to::(); + assert_eq!(block_number, 125777954 - 1); +} + #[tokio::test(flavor = "multi_thread")] async fn test_fork_eth_fee_history() { let (api, handle) = spawn(fork_config()).await; @@ -251,10 +302,10 @@ async fn test_fork_reset_setup() { } #[tokio::test(flavor = "multi_thread")] -async fn test_fork_snapshotting() { +async fn test_fork_state_snapshotting() { let (api, handle) = spawn(fork_config()).await; let provider = handle.http_provider(); - let snapshot = api.evm_snapshot().await.unwrap(); + let state_snapshot = api.evm_snapshot().await.unwrap(); let accounts: Vec<_> = handle.dev_wallets().collect(); let from = accounts[0].address(); @@ -278,7 +329,7 @@ async fn test_fork_snapshotting() { let to_balance = provider.get_balance(to).await.unwrap(); assert_eq!(balance_before.saturating_add(amount), to_balance); - assert!(api.evm_revert(snapshot).await.unwrap()); + assert!(api.evm_revert(state_snapshot).await.unwrap()); let nonce = provider.get_transaction_count(from).await.unwrap(); assert_eq!(nonce, initial_nonce); @@ -290,11 +341,11 @@ async fn test_fork_snapshotting() { } #[tokio::test(flavor = "multi_thread")] -async fn test_fork_snapshotting_repeated() { +async fn test_fork_state_snapshotting_repeated() { let (api, handle) = spawn(fork_config()).await; let provider = handle.http_provider(); - let snapshot = api.evm_snapshot().await.unwrap(); + let state_snapshot = api.evm_snapshot().await.unwrap(); let accounts: Vec<_> = handle.dev_wallets().collect(); let from = accounts[0].address(); @@ -315,9 +366,9 @@ async fn test_fork_snapshotting_repeated() { let to_balance = provider.get_balance(to).await.unwrap(); assert_eq!(balance_before.saturating_add(amount), to_balance); - let _second_snapshot = api.evm_snapshot().await.unwrap(); + let _second_state_snapshot = api.evm_snapshot().await.unwrap(); - assert!(api.evm_revert(snapshot).await.unwrap()); + assert!(api.evm_revert(state_snapshot).await.unwrap()); let nonce = provider.get_transaction_count(from).await.unwrap(); assert_eq!(nonce, initial_nonce); @@ -332,17 +383,16 @@ async fn test_fork_snapshotting_repeated() { // assert!(!api.evm_revert(second_snapshot).await.unwrap()); // nothing is reverted, snapshot gone - assert!(!api.evm_revert(snapshot).await.unwrap()); + assert!(!api.evm_revert(state_snapshot).await.unwrap()); } // #[tokio::test(flavor = "multi_thread")] -async fn test_fork_snapshotting_blocks() { +async fn test_fork_state_snapshotting_blocks() { let (api, handle) = spawn(fork_config()).await; let provider = handle.http_provider(); - // create a snapshot - let snapshot = api.evm_snapshot().await.unwrap(); + let state_snapshot = api.evm_snapshot().await.unwrap(); let accounts: Vec<_> = handle.dev_wallets().collect(); let from = accounts[0].address(); @@ -366,8 +416,7 @@ async fn test_fork_snapshotting_blocks() { let to_balance = provider.get_balance(to).await.unwrap(); assert_eq!(balance_before.saturating_add(amount), to_balance); - // revert snapshot - assert!(api.evm_revert(snapshot).await.unwrap()); + assert!(api.evm_revert(state_snapshot).await.unwrap()); assert_eq!(initial_nonce, provider.get_transaction_count(from).await.unwrap()); let block_number_after = provider.get_block_number().await.unwrap(); @@ -378,8 +427,8 @@ async fn test_fork_snapshotting_blocks() { let nonce = provider.get_transaction_count(from).await.unwrap(); assert_eq!(nonce, initial_nonce + 1); - // revert again: nothing to revert since snapshot gone - assert!(!api.evm_revert(snapshot).await.unwrap()); + // revert again: nothing to revert since state snapshot gone + assert!(!api.evm_revert(state_snapshot).await.unwrap()); let nonce = provider.get_transaction_count(from).await.unwrap(); assert_eq!(nonce, initial_nonce + 1); let block_number_after = provider.get_block_number().await.unwrap(); @@ -732,7 +781,7 @@ async fn test_fork_can_send_opensea_tx() { .value(U256::from(20000000000000000u64)) .with_input(input) .with_gas_price(22180711707u128) - .with_gas_limit(150_000u128); + .with_gas_limit(150_000); let tx = WithOtherFields::new(tx); let tx = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); @@ -768,7 +817,7 @@ async fn test_fork_init_base_fee() { // assert_eq!(block.header.number, 13184859u64); let init_base_fee = block.header.base_fee_per_gas.unwrap(); - assert_eq!(init_base_fee, 63739886069u128); + assert_eq!(init_base_fee, 63739886069); api.mine_one().await; @@ -1136,7 +1185,7 @@ async fn test_fork_reset_basefee() { let latest = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); // basefee of +1 block: - assert_eq!(latest.header.base_fee_per_gas.unwrap(), 59455969592u128); + assert_eq!(latest.header.base_fee_per_gas.unwrap(), 59455969592u64); // now reset to block 18835000 -1 api.anvil_reset(Some(Forking { json_rpc_url: None, block_number: Some(18835000u64 - 1) })) @@ -1147,7 +1196,7 @@ async fn test_fork_reset_basefee() { let latest = api.block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); // basefee of the forked block: - assert_eq!(latest.header.base_fee_per_gas.unwrap(), 59017001138u128); + assert_eq!(latest.header.base_fee_per_gas.unwrap(), 59017001138); } // @@ -1193,7 +1242,7 @@ async fn test_arbitrum_fork_block_number() { assert_eq!(block_number, initial_block_number); // take snapshot at initial block number - let snapshot = api.evm_snapshot().await.unwrap(); + let snapshot_state = api.evm_snapshot().await.unwrap(); // mine new block and check block number returned by `eth_blockNumber` api.mine_one().await; @@ -1206,7 +1255,7 @@ async fn test_arbitrum_fork_block_number() { assert!(block_by_number.other.get("l1BlockNumber").is_some()); // revert to recorded snapshot and check block number - assert!(api.evm_revert(snapshot).await.unwrap()); + assert!(api.evm_revert(snapshot_state).await.unwrap()); let block_number = api.block_number().unwrap().to::(); assert_eq!(block_number, initial_block_number); @@ -1221,6 +1270,27 @@ async fn test_arbitrum_fork_block_number() { assert_eq!(block_number, initial_block_number - 2); } +#[tokio::test(flavor = "multi_thread")] +async fn test_base_fork_gas_limit() { + // fork to get initial block for test + let (api, handle) = spawn( + fork_config() + .with_fork_block_number(None::) + .with_eth_rpc_url(Some(next_rpc_endpoint(NamedChain::Base))), + ) + .await; + + let provider = handle.http_provider(); + let block = provider + .get_block(BlockId::Number(BlockNumberOrTag::Latest), BlockTransactionsKind::Hashes) + .await + .unwrap() + .unwrap(); + + assert!(api.gas_limit() >= uint!(132_000_000_U256)); + assert!(block.header.gas_limit >= 132_000_000_u64); +} + // #[tokio::test(flavor = "multi_thread")] async fn test_fork_execution_reverted() { @@ -1373,7 +1443,7 @@ async fn test_reset_dev_account_nonce() { .from(address) .to(address) .nonce(nonce_after) - .gas_limit(21000u128), + .gas_limit(21000), )) .await .unwrap() diff --git a/crates/anvil/tests/it/gas.rs b/crates/anvil/tests/it/gas.rs index ae9c9c201..55f832199 100644 --- a/crates/anvil/tests/it/gas.rs +++ b/crates/anvil/tests/it/gas.rs @@ -2,7 +2,7 @@ use crate::utils::http_provider_with_signer; use alloy_network::{EthereumWallet, TransactionBuilder}; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{uint, Address, U256, U64}; use alloy_provider::Provider; use alloy_rpc_types::{BlockId, TransactionRequest}; use alloy_serde::WithOtherFields; @@ -10,6 +10,21 @@ use anvil::{eth::fees::INITIAL_BASE_FEE, spawn, NodeConfig}; const GAS_TRANSFER: u128 = 21_000; +#[tokio::test(flavor = "multi_thread")] +async fn test_gas_limit_applied_from_config() { + let (api, _handle) = spawn(NodeConfig::test().with_gas_limit(Some(10_000_000))).await; + + assert_eq!(api.gas_limit(), uint!(10_000_000_U256)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_gas_limit_disabled_from_config() { + let (api, _handle) = spawn(NodeConfig::test().disable_block_gas_limit(true)).await; + + // see https://github.com/foundry-rs/foundry/pull/8933 + assert_eq!(api.gas_limit(), U256::from(U64::MAX)); +} + #[tokio::test(flavor = "multi_thread")] async fn test_basefee_full_block() { let (_api, handle) = spawn( @@ -87,7 +102,7 @@ async fn test_basefee_half_block() { .unwrap(); // unchanged, half block - assert_eq!(next_base_fee, INITIAL_BASE_FEE); + assert_eq!(next_base_fee, { INITIAL_BASE_FEE }); } #[tokio::test(flavor = "multi_thread")] @@ -132,7 +147,7 @@ async fn test_basefee_empty_block() { #[tokio::test(flavor = "multi_thread")] async fn test_respect_base_fee() { let base_fee = 50u128; - let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee))).await; + let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee as u64))).await; let provider = handle.http_provider(); @@ -153,7 +168,7 @@ async fn test_respect_base_fee() { #[tokio::test(flavor = "multi_thread")] async fn test_tip_above_fee_cap() { let base_fee = 50u128; - let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee))).await; + let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee as u64))).await; let provider = handle.http_provider(); @@ -175,17 +190,17 @@ async fn test_tip_above_fee_cap() { #[tokio::test(flavor = "multi_thread")] async fn test_can_use_fee_history() { let base_fee = 50u128; - let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee))).await; + let (_api, handle) = spawn(NodeConfig::test().with_base_fee(Some(base_fee as u64))).await; let provider = handle.http_provider(); for _ in 0..10 { let fee_history = provider.get_fee_history(1, Default::default(), &[]).await.unwrap(); - let next_base_fee = fee_history.base_fee_per_gas.last().unwrap(); + let next_base_fee = *fee_history.base_fee_per_gas.last().unwrap(); let tx = TransactionRequest::default() .with_to(Address::random()) .with_value(U256::from(100)) - .with_gas_price(*next_base_fee); + .with_gas_price(next_base_fee); let tx = WithOtherFields::new(tx); let receipt = @@ -193,11 +208,11 @@ async fn test_can_use_fee_history() { assert!(receipt.inner.inner.is_success()); let fee_history_after = provider.get_fee_history(1, Default::default(), &[]).await.unwrap(); - let latest_fee_history_fee = fee_history_after.base_fee_per_gas.first().unwrap(); + let latest_fee_history_fee = *fee_history_after.base_fee_per_gas.first().unwrap() as u64; let latest_block = provider.get_block(BlockId::latest(), false.into()).await.unwrap().unwrap(); - assert_eq!(latest_block.header.base_fee_per_gas.unwrap(), *latest_fee_history_fee); - assert_eq!(latest_fee_history_fee, next_base_fee); + assert_eq!(latest_block.header.base_fee_per_gas.unwrap(), latest_fee_history_fee); + assert_eq!(latest_fee_history_fee, next_base_fee as u64); } } diff --git a/crates/anvil/tests/it/logs.rs b/crates/anvil/tests/it/logs.rs index 1ce4ac64f..3bf09493d 100644 --- a/crates/anvil/tests/it/logs.rs +++ b/crates/anvil/tests/it/logs.rs @@ -5,7 +5,7 @@ use crate::{ utils::{http_provider_with_signer, ws_provider_with_signer}, }; use alloy_network::EthereumWallet; -use alloy_primitives::B256; +use alloy_primitives::{map::B256HashSet, B256}; use alloy_provider::Provider; use alloy_rpc_types::{BlockNumberOrTag, Filter}; use anvil::{spawn, NodeConfig}; @@ -120,7 +120,7 @@ async fn get_all_events() { // test that logs returned from get_logs and get_transaction_receipt have // the same log_index, block_number, and transaction_hash let mut tasks = vec![]; - let mut seen_tx_hashes = std::collections::HashSet::new(); + let mut seen_tx_hashes = B256HashSet::default(); for log in &logs { if seen_tx_hashes.contains(&log.transaction_hash.unwrap()) { continue; diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index f3f5eca15..256edb813 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::needless_return)] + mod abi; mod anvil; mod anvil_api; diff --git a/crates/anvil/tests/it/optimism.rs b/crates/anvil/tests/it/optimism.rs index 6446caf9c..8de4eab1d 100644 --- a/crates/anvil/tests/it/optimism.rs +++ b/crates/anvil/tests/it/optimism.rs @@ -25,19 +25,21 @@ async fn test_deposits_not_supported_if_optimism_disabled() { .with_to(to) .with_value(U256::from(1234)) .with_gas_limit(21000); - let tx = WithOtherFields { - inner: tx, - other: OptimismTransactionFields { - source_hash: Some(b256!( - "0000000000000000000000000000000000000000000000000000000000000000" - )), - mint: Some(0), - is_system_tx: Some(true), - deposit_receipt_version: None, - } - .into(), + + let op_fields = OptimismTransactionFields { + source_hash: Some(b256!( + "0000000000000000000000000000000000000000000000000000000000000000" + )), + mint: Some(0), + is_system_tx: Some(true), + deposit_receipt_version: None, }; + // TODO: Test this + let other = serde_json::to_value(op_fields).unwrap().try_into().unwrap(); + + let tx = WithOtherFields { inner: tx, other }; + let err = provider.send_transaction(tx).await.unwrap_err(); let s = err.to_string(); assert!(s.contains("op-stack deposit tx received but is not supported"), "{s:?}"); @@ -61,23 +63,22 @@ async fn test_send_value_deposit_transaction() { let send_value = U256::from(1234); let before_balance_to = provider.get_balance(to).await.unwrap(); + let op_fields = OptimismTransactionFields { + source_hash: Some(b256!( + "0000000000000000000000000000000000000000000000000000000000000000" + )), + mint: Some(0), + is_system_tx: Some(true), + deposit_receipt_version: None, + }; + + let other = serde_json::to_value(op_fields).unwrap().try_into().unwrap(); let tx = TransactionRequest::default() .with_from(from) .with_to(to) .with_value(send_value) .with_gas_limit(21000); - let tx: WithOtherFields = WithOtherFields { - inner: tx, - other: OptimismTransactionFields { - source_hash: Some(b256!( - "0000000000000000000000000000000000000000000000000000000000000000" - )), - mint: Some(0), - is_system_tx: Some(true), - deposit_receipt_version: None, - } - .into(), - }; + let tx: WithOtherFields = WithOtherFields { inner: tx, other }; let pending = provider.send_transaction(tx).await.unwrap().register().await.unwrap(); @@ -121,18 +122,17 @@ async fn test_send_value_raw_deposit_transaction() { .with_gas_limit(21_000) .with_max_fee_per_gas(20_000_000_000) .with_max_priority_fee_per_gas(1_000_000_000); - let tx = WithOtherFields { - inner: tx, - other: OptimismTransactionFields { - source_hash: Some(b256!( - "0000000000000000000000000000000000000000000000000000000000000000" - )), - mint: Some(0), - is_system_tx: Some(true), - deposit_receipt_version: None, - } - .into(), + + let op_fields = OptimismTransactionFields { + source_hash: Some(b256!( + "0000000000000000000000000000000000000000000000000000000000000000" + )), + mint: Some(0), + is_system_tx: Some(true), + deposit_receipt_version: None, }; + let other = serde_json::to_value(op_fields).unwrap().try_into().unwrap(); + let tx = WithOtherFields { inner: tx, other }; let tx_envelope = tx.build(&signer).await.unwrap(); let mut tx_buffer = Vec::with_capacity(tx_envelope.encode_2718_len()); tx_envelope.encode_2718(&mut tx_buffer); diff --git a/crates/anvil/tests/it/state.rs b/crates/anvil/tests/it/state.rs index 8227a89f1..cb8f3b9eb 100644 --- a/crates/anvil/tests/it/state.rs +++ b/crates/anvil/tests/it/state.rs @@ -1,7 +1,7 @@ //! general eth api tests use crate::abi::Greeter; -use alloy_primitives::{Bytes, Uint}; +use alloy_primitives::{Bytes, Uint, U256}; use alloy_provider::Provider; use alloy_rpc_types::BlockId; use anvil::{spawn, NodeConfig}; @@ -13,6 +13,7 @@ async fn can_load_state() { let (api, _handle) = spawn(NodeConfig::test()).await; + api.mine_one().await; api.mine_one().await; let num = api.block_number().unwrap(); @@ -23,7 +24,19 @@ async fn can_load_state() { let (api, _handle) = spawn(NodeConfig::test().with_init_state_path(state_file)).await; let num2 = api.block_number().unwrap(); + + // Ref: https://github.com/foundry-rs/foundry/issues/9017 + // Check responses of eth_blockNumber and eth_getBlockByNumber don't deviate after loading state + let num_from_tag = api + .block_by_number(alloy_eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .number; assert_eq!(num, num2); + + assert_eq!(num, U256::from(num_from_tag)); } #[tokio::test(flavor = "multi_thread")] diff --git a/crates/anvil/tests/it/transaction.rs b/crates/anvil/tests/it/transaction.rs index 0827bbac1..07c120d1c 100644 --- a/crates/anvil/tests/it/transaction.rs +++ b/crates/anvil/tests/it/transaction.rs @@ -3,7 +3,7 @@ use crate::{ utils::{connect_pubsub, http_provider_with_signer}, }; use alloy_network::{EthereumWallet, TransactionBuilder}; -use alloy_primitives::{Address, Bytes, FixedBytes, U256}; +use alloy_primitives::{map::B256HashSet, Address, Bytes, FixedBytes, U256}; use alloy_provider::Provider; use alloy_rpc_types::{ state::{AccountOverride, StateOverride}, @@ -13,7 +13,7 @@ use alloy_serde::WithOtherFields; use anvil::{spawn, EthereumHardfork, NodeConfig}; use eyre::Ok; use futures::{future::join_all, FutureExt, StreamExt}; -use std::{collections::HashSet, str::FromStr, time::Duration}; +use std::{str::FromStr, time::Duration}; use tokio::time::timeout; #[tokio::test(flavor = "multi_thread")] @@ -197,7 +197,7 @@ async fn can_reject_too_high_gas_limits() { let from = accounts[0].address(); let to = accounts[1].address(); - let gas_limit = api.gas_limit().to::(); + let gas_limit = api.gas_limit().to::(); let amount = handle.genesis_balance().checked_div(U256::from(3u64)).unwrap(); let tx = @@ -230,18 +230,18 @@ async fn can_reject_too_high_gas_limits() { // #[tokio::test(flavor = "multi_thread")] async fn can_mine_large_gas_limit() { - let (api, handle) = spawn(NodeConfig::test().disable_block_gas_limit(true)).await; + let (_, handle) = spawn(NodeConfig::test().disable_block_gas_limit(true)).await; let provider = handle.http_provider(); let accounts = handle.dev_wallets().collect::>(); let from = accounts[0].address(); let to = accounts[1].address(); - let gas_limit = api.gas_limit().to::(); + let gas_limit = anvil::DEFAULT_GAS_LIMIT as u64; let amount = handle.genesis_balance().checked_div(U256::from(3u64)).unwrap(); let tx = - TransactionRequest::default().to(to).value(amount).from(from).with_gas_limit(gas_limit * 3); + TransactionRequest::default().to(to).value(amount).from(from).with_gas_limit(gas_limit); // send transaction with higher gas limit let pending = provider.send_transaction(WithOtherFields::new(tx)).await.unwrap(); @@ -580,7 +580,7 @@ async fn can_handle_multiple_concurrent_transfers_with_same_nonce() { .value(U256::from(100)) .from(from) .nonce(nonce) - .with_gas_limit(21000u128); + .with_gas_limit(21000); let tx = WithOtherFields::new(tx); @@ -621,7 +621,7 @@ async fn can_handle_multiple_concurrent_deploys_with_same_nonce() { .from(from) .with_input(greeter_calldata.to_owned()) .nonce(nonce) - .with_gas_limit(300_000u128); + .with_gas_limit(300_000); let tx = WithOtherFields::new(tx); @@ -662,7 +662,7 @@ async fn can_handle_multiple_concurrent_transactions_with_same_nonce() { .from(from) .with_input(deploy_calldata.to_owned()) .nonce(nonce) - .with_gas_limit(300_000u128); + .with_gas_limit(300_000); let deploy_tx = WithOtherFields::new(deploy_tx); let set_greeting = greeter_contract.setGreeting("Hello".to_string()); @@ -672,7 +672,7 @@ async fn can_handle_multiple_concurrent_transactions_with_same_nonce() { .from(from) .with_input(set_greeting_calldata.to_owned()) .nonce(nonce) - .with_gas_limit(300_000u128); + .with_gas_limit(300_000); let set_greeting_tx = WithOtherFields::new(set_greeting_tx); for idx in 0..10 { @@ -950,7 +950,7 @@ async fn can_stream_pending_transactions() { if watch_received.len() == num_txs && sub_received.len() == num_txs { if let Some(sent) = &sent { assert_eq!(sent.len(), watch_received.len()); - let sent_txs = sent.iter().map(|tx| tx.transaction_hash).collect::>(); + let sent_txs = sent.iter().map(|tx| tx.transaction_hash).collect::(); assert_eq!(sent_txs, watch_received.iter().copied().collect()); assert_eq!(sent_txs, sub_received.iter().copied().collect()); break @@ -1127,7 +1127,7 @@ async fn test_estimate_gas() { let addr = recipient; let account_override = AccountOverride { balance: Some(alloy_primitives::U256::from(1e18)), ..Default::default() }; - let mut state_override = StateOverride::new(); + let mut state_override = StateOverride::default(); state_override.insert(addr, account_override); // Estimate gas with state override implying sufficient funds. @@ -1152,7 +1152,7 @@ async fn test_reject_gas_too_low() { .to(Address::random()) .value(U256::from(1337u64)) .from(account) - .with_gas_limit(gas as u128); + .with_gas_limit(gas); let tx = WithOtherFields::new(tx); let resp = provider.send_transaction(tx).await; @@ -1169,7 +1169,7 @@ async fn can_call_with_high_gas_limit() { let greeter_contract = Greeter::deploy(provider, "Hello World!".to_string()).await.unwrap(); - let greeting = greeter_contract.greet().gas(60_000_000u128).call().await.unwrap(); + let greeting = greeter_contract.greet().gas(60_000_000).call().await.unwrap(); assert_eq!("Hello World!", greeting._0); } @@ -1179,7 +1179,7 @@ async fn test_reject_eip1559_pre_london() { spawn(NodeConfig::test().with_hardfork(Some(EthereumHardfork::Berlin.into()))).await; let provider = handle.http_provider(); - let gas_limit = api.gas_limit().to::(); + let gas_limit = api.gas_limit().to::(); let gas_price = api.gas_price(); let unsupported_call_builder = diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index 154015538..034a1e148 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "foundry-cast" +name = "cast" description = "Command-line tool for performing Ethereum RPC calls" version.workspace = true @@ -56,7 +56,6 @@ alloy-sol-types.workspace = true alloy-transport.workspace = true chrono.workspace = true -evm-disassembler.workspace = true eyre.workspace = true futures.workspace = true rand.workspace = true @@ -92,8 +91,9 @@ tikv-jemallocator = { workspace = true, optional = true } [dev-dependencies] anvil.workspace = true foundry-test-utils.workspace = true + async-trait.workspace = true -criterion = "0.5" +divan.workspace = true [features] default = ["rustls", "jemalloc"] diff --git a/crates/cast/benches/vanity.rs b/crates/cast/benches/vanity.rs index 4311b5283..a39e4d8bc 100644 --- a/crates/cast/benches/vanity.rs +++ b/crates/cast/benches/vanity.rs @@ -1,51 +1,22 @@ -use criterion::{criterion_group, criterion_main, Criterion}; use rayon::prelude::*; -use std::{hint::black_box, time::Duration}; +use std::hint::black_box; #[path = "../bin/cmd/wallet/mod.rs"] #[allow(unused)] mod wallet; use wallet::vanity::*; -/// Benches `cast wallet vanity` -/// -/// Left or right matchers, with or without nonce do not change the outcome. -/// -/// Regex matchers get optimised away even with a black_box. -fn vanity(c: &mut Criterion) { - let mut g = c.benchmark_group("vanity"); - - g.sample_size(500); - g.noise_threshold(0.04); - g.measurement_time(Duration::from_secs(30)); - - g.bench_function("wallet generator", |b| b.iter(|| black_box(generate_wallet()))); - - // 1 - - g.sample_size(100); - g.noise_threshold(0.02); - - g.bench_function("match 1", |b| { - let m = LeftHexMatcher { left: vec![0] }; - let matcher = create_matcher(m); - b.iter(|| wallet_generator().find_any(|x| black_box(matcher(x)))) - }); - - // 2 - - g.sample_size(10); - g.noise_threshold(0.01); - g.measurement_time(Duration::from_secs(60)); - - g.bench_function("match 2", |b| { - let m = LeftHexMatcher { left: vec![0, 0] }; - let matcher = create_matcher(m); - b.iter(|| wallet_generator().find_any(|x| black_box(matcher(x)))) - }); +#[divan::bench] +fn vanity_wallet_generator() -> GeneratedWallet { + generate_wallet() +} - g.finish(); +#[divan::bench(args = [&[0][..]])] +fn vanity_match(bencher: divan::Bencher<'_, '_>, arg: &[u8]) { + let matcher = create_matcher(LeftHexMatcher { left: arg.to_vec() }); + bencher.bench_local(|| wallet_generator().find_any(|x| black_box(matcher(x)))); } -criterion_group!(vanity_benches, vanity); -criterion_main!(vanity_benches); +fn main() { + divan::main(); +} diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index 5b1e1ff65..fe0a244df 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -74,7 +74,7 @@ pub struct CallArgs { json: bool, /// Enable Alphanet features. - #[arg(long)] + #[arg(long, alias = "odyssey")] pub alphanet: bool, #[command(subcommand)] @@ -200,7 +200,7 @@ impl CallArgs { ), }; - handle_traces(trace, &config, chain, labels, debug, decode_internal).await?; + handle_traces(trace, &config, chain, labels, debug, decode_internal, false).await?; return Ok(()); } diff --git a/crates/cast/bin/cmd/run.rs b/crates/cast/bin/cmd/run.rs index b67c0ed11..9d04ed1e2 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -84,7 +84,7 @@ pub struct RunArgs { pub no_rate_limit: bool, /// Enables Alphanet features. - #[arg(long)] + #[arg(long, alias = "odyssey")] pub alphanet: bool, } @@ -242,7 +242,16 @@ impl RunArgs { } }; - handle_traces(result, &config, chain, self.label, self.debug, self.decode_internal).await?; + handle_traces( + result, + &config, + chain, + self.label, + self.debug, + self.decode_internal, + self.verbose, + ) + .await?; Ok(()) } diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 27a052fe6..63894d980 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -42,6 +42,7 @@ fn main() -> Result<()> { main_args(args) } +#[allow(clippy::needless_return)] #[tokio::main] async fn main_args(args: CastArgs) -> Result<()> { match args.cmd { @@ -306,7 +307,7 @@ async fn main_args(args: CastArgs) -> Result<()> { println!("Computed Address: {}", computed.to_checksum(None)); } CastSubcommand::Disassemble { bytecode } => { - println!("{}", SimpleCast::disassemble(&bytecode)?); + println!("{}", SimpleCast::disassemble(&hex::decode(bytecode)?)?); } CastSubcommand::Selectors { bytecode, resolve } => { let functions = SimpleCast::extract_functions(&bytecode)?; diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 8132f8e4f..69f86268b 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -21,7 +21,6 @@ use alloy_sol_types::sol; use alloy_transport::Transport; use base::{Base, NumberWithBase, ToBase}; use chrono::DateTime; -use evm_disassembler::{disassemble_bytes, disassemble_str, format_operations}; use eyre::{Context, ContextCompat, Result}; use foundry_block_explorers::Client; use foundry_common::{ @@ -37,6 +36,7 @@ use rayon::prelude::*; use revm::primitives::Eof; use std::{ borrow::Cow, + fmt::Write, io, marker::PhantomData, path::PathBuf, @@ -45,6 +45,7 @@ use std::{ time::Duration, }; use tokio::signal::ctrl_c; +use utils::decode_instructions; use foundry_common::abi::encode_function_args_packed; pub use foundry_evm::*; @@ -670,7 +671,7 @@ where if disassemble { let code = self.provider.get_code_at(who).block_id(block.unwrap_or_default()).await?.to_vec(); - Ok(format_operations(disassemble_bytes(code)?)?) + SimpleCast::disassemble(&code) } else { Ok(format!( "{}", @@ -1959,17 +1960,36 @@ impl SimpleCast { /// # Example /// /// ``` + /// use alloy_primitives::hex; /// use cast::SimpleCast as Cast; /// /// # async fn foo() -> eyre::Result<()> { /// let bytecode = "0x608060405260043610603f57600035"; - /// let opcodes = Cast::disassemble(bytecode)?; + /// let opcodes = Cast::disassemble(&hex::decode(bytecode)?)?; /// println!("{}", opcodes); /// # Ok(()) /// # } /// ``` - pub fn disassemble(bytecode: &str) -> Result { - format_operations(disassemble_str(bytecode)?) + pub fn disassemble(code: &[u8]) -> Result { + let mut output = String::new(); + + for step in decode_instructions(code) { + write!(output, "{:08x}: ", step.pc)?; + + if let Some(op) = step.op { + write!(output, "{op}")?; + } else { + write!(output, "INVALID")?; + } + + if !step.immediate.is_empty() { + write!(output, " {}", hex::encode_prefixed(step.immediate))?; + } + + writeln!(output)?; + } + + Ok(output) } /// Gets the selector for a given function signature diff --git a/crates/cast/src/rlp_converter.rs b/crates/cast/src/rlp_converter.rs index ad1787438..05e3462cb 100644 --- a/crates/cast/src/rlp_converter.rs +++ b/crates/cast/src/rlp_converter.rs @@ -47,7 +47,7 @@ impl Decodable for Item { impl Item { pub(crate) fn value_to_item(value: &Value) -> eyre::Result { - return match value { + match value { Value::Null => Ok(Self::Data(vec![])), Value::Bool(_) => { eyre::bail!("RLP input can not contain booleans") diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index b1eefa9a3..b85cdc1f0 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -59,6 +59,7 @@ mixHash [..] nonce [..] number [..] parentHash [..] +parentBeaconRoot [..] transactionsRoot [..] receiptsRoot [..] sha3Uncles [..] @@ -707,6 +708,8 @@ to 0x91da5bf3F8Eb72724E6f50Ec6C3D199C6355c59c "#]]); + let rpc = next_http_rpc_endpoint(); + // cmd.cast_fuse() .args([ @@ -884,6 +887,8 @@ casttest!(mktx_requires_to, |_prj, cmd| { "mktx", "--private-key", "0x0000000000000000000000000000000000000000000000000000000000000001", + "--chain", + "1", ]); cmd.assert_failure().stderr_eq(str![[r#" Error: @@ -972,6 +977,8 @@ casttest!(send_requires_to, |_prj, cmd| { "send", "--private-key", "0x0000000000000000000000000000000000000000000000000000000000000001", + "--chain", + "1", ]); cmd.assert_failure().stderr_eq(str![[r#" Error: @@ -1021,6 +1028,7 @@ casttest!(storage, |_prj, cmd| { "#]]); + let rpc = next_http_rpc_endpoint(); cmd.cast_fuse() .args(["storage", usdt, total_supply_slot, "--rpc-url", &rpc, "--block", block_after]) .assert_success() diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 9a65cf861..df3631ed3 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -28,6 +28,7 @@ foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm-core.workspace = true +foundry-evm-traces.workspace = true foundry-wallets.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compiler.workspace = true @@ -64,13 +65,11 @@ p256 = "0.13.2" ecdsa = "0.16" rand = "0.8" revm.workspace = true -rustc-hash.workspace = true +revm-inspectors.workspace = true semver.workspace = true serde_json.workspace = true thiserror.workspace = true toml = { workspace = true, features = ["preserve_order"] } tracing.workspace = true walkdir.workspace = true - -[dev-dependencies] proptest.workspace = true diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index aa809b9fb..34f0fadc2 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -488,6 +488,42 @@ "description": "The amount of gas remaining." } ] + }, + { + "name": "DebugStep", + "description": "The result of the `stopDebugTraceRecording` call", + "fields": [ + { + "name": "stack", + "ty": "uint256[]", + "description": "The stack before executing the step of the run.\n stack\\[0\\] represents the top of the stack.\n and only stack data relevant to the opcode execution is contained." + }, + { + "name": "memoryInput", + "ty": "bytes", + "description": "The memory input data before executing the step of the run.\n only input data relevant to the opcode execution is contained.\n e.g. for MLOAD, it will have memory\\[offset:offset+32\\] copied here.\n the offset value can be get by the stack data." + }, + { + "name": "opcode", + "ty": "uint8", + "description": "The opcode that was accessed." + }, + { + "name": "depth", + "ty": "uint64", + "description": "The call depth of the step." + }, + { + "name": "isOutOfGas", + "ty": "bool", + "description": "Whether the call end up with out of gas error." + }, + { + "name": "contractAddr", + "ty": "address", + "description": "The contract address where the opcode is running" + } + ] } ], "cheatcodes": [ @@ -3035,9 +3071,9 @@ "func": { "id": "breakpoint_0", "description": "Writes a breakpoint to jump to in the debugger.", - "declaration": "function breakpoint(string calldata char) external;", + "declaration": "function breakpoint(string calldata char) external pure;", "visibility": "external", - "mutability": "", + "mutability": "pure", "signature": "breakpoint(string)", "selector": "0xf0259e92", "selectorBytes": [ @@ -3055,9 +3091,9 @@ "func": { "id": "breakpoint_1", "description": "Writes a conditional breakpoint to jump to in the debugger.", - "declaration": "function breakpoint(string calldata char, bool value) external;", + "declaration": "function breakpoint(string calldata char, bool value) external pure;", "visibility": "external", - "mutability": "", + "mutability": "pure", "signature": "breakpoint(string,bool)", "selector": "0xf7d39a8d", "selectorBytes": [ @@ -3191,6 +3227,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "cloneAccount", + "description": "Clones a source account code, state, balance and nonce to a target account and updates in-memory EVM state.", + "declaration": "function cloneAccount(address source, address target) external;", + "visibility": "external", + "mutability": "", + "signature": "cloneAccount(address,address)", + "selector": "0x533d61c9", + "selectorBytes": [ + 83, + 61, + 97, + 201 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "closeFile", @@ -3291,6 +3347,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "contains", + "description": "Returns true if `search` is found in `subject`, false otherwise.", + "declaration": "function contains(string calldata subject, string calldata search) external returns (bool result);", + "visibility": "external", + "mutability": "", + "signature": "contains(string,string)", + "selector": "0x3fb18aec", + "selectorBytes": [ + 63, + 177, + 138, + 236 + ] + }, + "group": "string", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "cool", @@ -3574,7 +3650,7 @@ { "func": { "id": "deleteSnapshot", - "description": "Removes the snapshot with the given ID created by `snapshot`.\nTakes the snapshot ID to delete.\nReturns `true` if the snapshot was successfully deleted.\nReturns `false` if the snapshot does not exist.", + "description": "`deleteSnapshot` is being deprecated in favor of `deleteStateSnapshot`. It will be removed in future versions.", "declaration": "function deleteSnapshot(uint256 snapshotId) external returns (bool success);", "visibility": "external", "mutability": "", @@ -3588,13 +3664,15 @@ ] }, "group": "evm", - "status": "stable", + "status": { + "deprecated": "replaced by `deleteStateSnapshot`" + }, "safety": "unsafe" }, { "func": { "id": "deleteSnapshots", - "description": "Removes _all_ snapshots previously created by `snapshot`.", + "description": "`deleteSnapshots` is being deprecated in favor of `deleteStateSnapshots`. It will be removed in future versions.", "declaration": "function deleteSnapshots() external;", "visibility": "external", "mutability": "", @@ -3608,6 +3686,48 @@ ] }, "group": "evm", + "status": { + "deprecated": "replaced by `deleteStateSnapshots`" + }, + "safety": "unsafe" + }, + { + "func": { + "id": "deleteStateSnapshot", + "description": "Removes the snapshot with the given ID created by `snapshot`.\nTakes the snapshot ID to delete.\nReturns `true` if the snapshot was successfully deleted.\nReturns `false` if the snapshot does not exist.", + "declaration": "function deleteStateSnapshot(uint256 snapshotId) external returns (bool success);", + "visibility": "external", + "mutability": "", + "signature": "deleteStateSnapshot(uint256)", + "selector": "0x08d6b37a", + "selectorBytes": [ + 8, + 214, + 179, + 122 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "deleteStateSnapshots", + "description": "Removes _all_ snapshots previously created by `snapshot`.", + "declaration": "function deleteStateSnapshots() external;", + "visibility": "external", + "mutability": "", + "signature": "deleteStateSnapshots()", + "selector": "0xe0933c74", + "selectorBytes": [ + 224, + 147, + 60, + 116 + ] + }, + "group": "evm", "status": "stable", "safety": "unsafe" }, @@ -3634,7 +3754,7 @@ { "func": { "id": "deployCode_1", - "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nAdditionaly accepts abi-encoded constructor arguments.", + "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nAdditionally accepts abi-encoded constructor arguments.", "declaration": "function deployCode(string calldata artifactPath, bytes calldata constructorArgs) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -5011,6 +5131,46 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getArtifactPathByCode", + "description": "Gets the artifact path from code (aka. creation code).", + "declaration": "function getArtifactPathByCode(bytes calldata code) external view returns (string memory path);", + "visibility": "external", + "mutability": "view", + "signature": "getArtifactPathByCode(bytes)", + "selector": "0xeb74848c", + "selectorBytes": [ + 235, + 116, + 132, + 140 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getArtifactPathByDeployedCode", + "description": "Gets the artifact path from deployed code (aka. runtime code).", + "declaration": "function getArtifactPathByDeployedCode(bytes calldata deployedCode) external view returns (string memory path);", + "visibility": "external", + "mutability": "view", + "signature": "getArtifactPathByDeployedCode(bytes)", + "selector": "0x6d853ba5", + "selectorBytes": [ + 109, + 133, + 59, + 165 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getBlobBaseFee", @@ -5291,6 +5451,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getWallets", + "description": "Returns addresses of available unlocked wallets in the script environment.", + "declaration": "function getWallets() external returns (address[] memory wallets);", + "visibility": "external", + "mutability": "", + "signature": "getWallets()", + "selector": "0xdb7a4605", + "selectorBytes": [ + 219, + 122, + 70, + 5 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "indexOf", @@ -5408,7 +5588,9 @@ ] }, "group": "json", - "status": "deprecated", + "status": { + "deprecated": "replaced by `keyExistsJson`" + }, "safety": "safe" }, { @@ -5474,7 +5656,7 @@ { "func": { "id": "lastCallGas", - "description": "Gets the gas used in the last call.", + "description": "Gets the gas used in the last call from the callee perspective.", "declaration": "function lastCallGas() external view returns (Gas memory gas);", "visibility": "external", "mutability": "view", @@ -5514,7 +5696,7 @@ { "func": { "id": "loadAllocs", - "description": "Load a genesis JSON file's `allocs` into the in-memory revm state.", + "description": "Load a genesis JSON file's `allocs` into the in-memory EVM state.", "declaration": "function loadAllocs(string calldata pathToAllocsJson) external;", "visibility": "external", "mutability": "", @@ -5691,6 +5873,46 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "mockCalls_0", + "description": "Mocks multiple calls to an address, returning specified data for each call.", + "declaration": "function mockCalls(address callee, bytes calldata data, bytes[] calldata returnData) external;", + "visibility": "external", + "mutability": "", + "signature": "mockCalls(address,bytes,bytes[])", + "selector": "0x5c5c3de9", + "selectorBytes": [ + 92, + 92, + 61, + 233 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "mockCalls_1", + "description": "Mocks multiple calls to an address with a specific `msg.value`, returning specified data for each call.", + "declaration": "function mockCalls(address callee, uint256 msgValue, bytes calldata data, bytes[] calldata returnData) external;", + "visibility": "external", + "mutability": "", + "signature": "mockCalls(address,uint256,bytes,bytes[])", + "selector": "0x08bcbae1", + "selectorBytes": [ + 8, + 188, + 186, + 225 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "mockFunction", @@ -6471,6 +6693,66 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "parseTomlTypeArray", + "description": "Parses a string of TOML data at `key` and coerces it to type array corresponding to `typeDescription`.", + "declaration": "function parseTomlTypeArray(string calldata toml, string calldata key, string calldata typeDescription) external pure returns (bytes memory);", + "visibility": "external", + "mutability": "pure", + "signature": "parseTomlTypeArray(string,string,string)", + "selector": "0x49be3743", + "selectorBytes": [ + 73, + 190, + 55, + 67 + ] + }, + "group": "toml", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "parseTomlType_0", + "description": "Parses a string of TOML data and coerces it to type corresponding to `typeDescription`.", + "declaration": "function parseTomlType(string calldata toml, string calldata typeDescription) external pure returns (bytes memory);", + "visibility": "external", + "mutability": "pure", + "signature": "parseTomlType(string,string)", + "selector": "0x47fa5e11", + "selectorBytes": [ + 71, + 250, + 94, + 17 + ] + }, + "group": "toml", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "parseTomlType_1", + "description": "Parses a string of TOML data at `key` and coerces it to type corresponding to `typeDescription`.", + "declaration": "function parseTomlType(string calldata toml, string calldata key, string calldata typeDescription) external pure returns (bytes memory);", + "visibility": "external", + "mutability": "pure", + "signature": "parseTomlType(string,string,string)", + "selector": "0xf9fa5cdb", + "selectorBytes": [ + 249, + 250, + 92, + 219 + ] + }, + "group": "toml", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "parseTomlUint", @@ -6853,18 +7135,18 @@ }, { "func": { - "id": "randomUint_0", - "description": "Returns a random uint256 value.", - "declaration": "function randomUint() external returns (uint256);", + "id": "randomBool", + "description": "Returns a random `bool`.", + "declaration": "function randomBool() external view returns (bool);", "visibility": "external", - "mutability": "", - "signature": "randomUint()", - "selector": "0x25124730", + "mutability": "view", + "signature": "randomBool()", + "selector": "0xcdc126bd", "selectorBytes": [ - 37, - 18, - 71, - 48 + 205, + 193, + 38, + 189 ] }, "group": "utilities", @@ -6873,18 +7155,18 @@ }, { "func": { - "id": "randomUint_1", - "description": "Returns random uin256 value between the provided range (=min..=max).", - "declaration": "function randomUint(uint256 min, uint256 max) external returns (uint256);", + "id": "randomBytes", + "description": "Returns a random byte array value of the given length.", + "declaration": "function randomBytes(uint256 len) external view returns (bytes memory);", "visibility": "external", - "mutability": "", - "signature": "randomUint(uint256,uint256)", - "selector": "0xd61b051b", + "mutability": "view", + "signature": "randomBytes(uint256)", + "selector": "0x6c5d32a9", "selectorBytes": [ - 214, - 27, - 5, - 27 + 108, + 93, + 50, + 169 ] }, "group": "utilities", @@ -6893,27 +7175,167 @@ }, { "func": { - "id": "readCallers", - "description": "Reads the current `msg.sender` and `tx.origin` from state and reports if there is any active caller modification.", - "declaration": "function readCallers() external returns (CallerMode callerMode, address msgSender, address txOrigin);", + "id": "randomBytes4", + "description": "Returns a random fixed-size byte array of length 4.", + "declaration": "function randomBytes4() external view returns (bytes4);", "visibility": "external", - "mutability": "", - "signature": "readCallers()", - "selector": "0x4ad0bac9", + "mutability": "view", + "signature": "randomBytes4()", + "selector": "0x9b7cd579", "selectorBytes": [ - 74, - 208, - 186, - 201 + 155, + 124, + 213, + 121 ] }, - "group": "evm", + "group": "utilities", "status": "stable", - "safety": "unsafe" + "safety": "safe" }, { "func": { - "id": "readDir_0", + "id": "randomBytes8", + "description": "Returns a random fixed-size byte array of length 8.", + "declaration": "function randomBytes8() external view returns (bytes8);", + "visibility": "external", + "mutability": "view", + "signature": "randomBytes8()", + "selector": "0x0497b0a5", + "selectorBytes": [ + 4, + 151, + 176, + 165 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "randomInt_0", + "description": "Returns a random `int256` value.", + "declaration": "function randomInt() external view returns (int256);", + "visibility": "external", + "mutability": "view", + "signature": "randomInt()", + "selector": "0x111f1202", + "selectorBytes": [ + 17, + 31, + 18, + 2 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "randomInt_1", + "description": "Returns a random `int256` value of given bits.", + "declaration": "function randomInt(uint256 bits) external view returns (int256);", + "visibility": "external", + "mutability": "view", + "signature": "randomInt(uint256)", + "selector": "0x12845966", + "selectorBytes": [ + 18, + 132, + 89, + 102 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "randomUint_0", + "description": "Returns a random uint256 value.", + "declaration": "function randomUint() external returns (uint256);", + "visibility": "external", + "mutability": "", + "signature": "randomUint()", + "selector": "0x25124730", + "selectorBytes": [ + 37, + 18, + 71, + 48 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "randomUint_1", + "description": "Returns random uint256 value between the provided range (=min..=max).", + "declaration": "function randomUint(uint256 min, uint256 max) external returns (uint256);", + "visibility": "external", + "mutability": "", + "signature": "randomUint(uint256,uint256)", + "selector": "0xd61b051b", + "selectorBytes": [ + 214, + 27, + 5, + 27 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "randomUint_2", + "description": "Returns a random `uint256` value of given bits.", + "declaration": "function randomUint(uint256 bits) external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "randomUint(uint256)", + "selector": "0xcf81e69c", + "selectorBytes": [ + 207, + 129, + 230, + 156 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "readCallers", + "description": "Reads the current `msg.sender` and `tx.origin` from state and reports if there is any active caller modification.", + "declaration": "function readCallers() external returns (CallerMode callerMode, address msgSender, address txOrigin);", + "visibility": "external", + "mutability": "", + "signature": "readCallers()", + "selector": "0x4ad0bac9", + "selectorBytes": [ + 74, + 208, + 186, + 201 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "readDir_0", "description": "Reads the directory at the given path recursively, up to `maxDepth`.\n`maxDepth` defaults to 1, meaning only the direct children of the given directory will be returned.\nFollows symbolic links if `followLinks` is true.", "declaration": "function readDir(string calldata path) external view returns (DirEntry[] memory entries);", "visibility": "external", @@ -7111,6 +7533,46 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "rememberKeys_0", + "description": "Derive a set number of wallets from a mnemonic at the derivation path `m/44'/60'/0'/0/{0..count}`.\nThe respective private keys are saved to the local forge wallet for later use and their addresses are returned.", + "declaration": "function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs);", + "visibility": "external", + "mutability": "", + "signature": "rememberKeys(string,string,uint32)", + "selector": "0x97cb9189", + "selectorBytes": [ + 151, + 203, + 145, + 137 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "rememberKeys_1", + "description": "Derive a set number of wallets from a mnemonic in the specified language at the derivation path `m/44'/60'/0'/0/{0..count}`.\nThe respective private keys are saved to the local forge wallet for later use and their addresses are returned.", + "declaration": "function rememberKeys(string calldata mnemonic, string calldata derivationPath, string calldata language, uint32 count) external returns (address[] memory keyAddrs);", + "visibility": "external", + "mutability": "", + "signature": "rememberKeys(string,string,string,uint32)", + "selector": "0xf8d58eaf", + "selectorBytes": [ + 248, + 213, + 142, + 175 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "removeDir", @@ -7254,7 +7716,7 @@ { "func": { "id": "revertTo", - "description": "Revert the state of the EVM to a previous snapshot\nTakes the snapshot ID to revert to.\nReturns `true` if the snapshot was successfully reverted.\nReturns `false` if the snapshot does not exist.\n**Note:** This does not automatically delete the snapshot. To delete the snapshot use `deleteSnapshot`.", + "description": "`revertTo` is being deprecated in favor of `revertToState`. It will be removed in future versions.", "declaration": "function revertTo(uint256 snapshotId) external returns (bool success);", "visibility": "external", "mutability": "", @@ -7268,13 +7730,15 @@ ] }, "group": "evm", - "status": "stable", + "status": { + "deprecated": "replaced by `revertToState`" + }, "safety": "unsafe" }, { "func": { "id": "revertToAndDelete", - "description": "Revert the state of the EVM to a previous snapshot and automatically deletes the snapshots\nTakes the snapshot ID to revert to.\nReturns `true` if the snapshot was successfully reverted and deleted.\nReturns `false` if the snapshot does not exist.", + "description": "`revertToAndDelete` is being deprecated in favor of `revertToStateAndDelete`. It will be removed in future versions.", "declaration": "function revertToAndDelete(uint256 snapshotId) external returns (bool success);", "visibility": "external", "mutability": "", @@ -7288,6 +7752,48 @@ ] }, "group": "evm", + "status": { + "deprecated": "replaced by `revertToStateAndDelete`" + }, + "safety": "unsafe" + }, + { + "func": { + "id": "revertToState", + "description": "Revert the state of the EVM to a previous snapshot\nTakes the snapshot ID to revert to.\nReturns `true` if the snapshot was successfully reverted.\nReturns `false` if the snapshot does not exist.\n**Note:** This does not automatically delete the snapshot. To delete the snapshot use `deleteStateSnapshot`.", + "declaration": "function revertToState(uint256 snapshotId) external returns (bool success);", + "visibility": "external", + "mutability": "", + "signature": "revertToState(uint256)", + "selector": "0xc2527405", + "selectorBytes": [ + 194, + 82, + 116, + 5 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "revertToStateAndDelete", + "description": "Revert the state of the EVM to a previous snapshot and automatically deletes the snapshots\nTakes the snapshot ID to revert to.\nReturns `true` if the snapshot was successfully reverted and deleted.\nReturns `false` if the snapshot does not exist.", + "declaration": "function revertToStateAndDelete(uint256 snapshotId) external returns (bool success);", + "visibility": "external", + "mutability": "", + "signature": "revertToStateAndDelete(uint256)", + "selector": "0x3a1985dc", + "selectorBytes": [ + 58, + 25, + 133, + 220 + ] + }, + "group": "evm", "status": "stable", "safety": "unsafe" }, @@ -8254,7 +8760,7 @@ { "func": { "id": "snapshot", - "description": "Snapshot the current state of the evm.\nReturns the ID of the snapshot that was created.\nTo revert a snapshot use `revertTo`.", + "description": "`snapshot` is being deprecated in favor of `snapshotState`. It will be removed in future versions.", "declaration": "function snapshot() external returns (uint256 snapshotId);", "visibility": "external", "mutability": "", @@ -8268,6 +8774,108 @@ ] }, "group": "evm", + "status": { + "deprecated": "replaced by `snapshotState`" + }, + "safety": "unsafe" + }, + { + "func": { + "id": "snapshotGasLastCall_0", + "description": "Snapshot capture the gas usage of the last call by name from the callee perspective.", + "declaration": "function snapshotGasLastCall(string calldata name) external returns (uint256 gasUsed);", + "visibility": "external", + "mutability": "", + "signature": "snapshotGasLastCall(string)", + "selector": "0xdd9fca12", + "selectorBytes": [ + 221, + 159, + 202, + 18 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "snapshotGasLastCall_1", + "description": "Snapshot capture the gas usage of the last call by name in a group from the callee perspective.", + "declaration": "function snapshotGasLastCall(string calldata group, string calldata name) external returns (uint256 gasUsed);", + "visibility": "external", + "mutability": "", + "signature": "snapshotGasLastCall(string,string)", + "selector": "0x200c6772", + "selectorBytes": [ + 32, + 12, + 103, + 114 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "snapshotState", + "description": "Snapshot the current state of the evm.\nReturns the ID of the snapshot that was created.\nTo revert a snapshot use `revertToState`.", + "declaration": "function snapshotState() external returns (uint256 snapshotId);", + "visibility": "external", + "mutability": "", + "signature": "snapshotState()", + "selector": "0x9cd23835", + "selectorBytes": [ + 156, + 210, + 56, + 53 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "snapshotValue_0", + "description": "Snapshot capture an arbitrary numerical value by name.\nThe group name is derived from the contract name.", + "declaration": "function snapshotValue(string calldata name, uint256 value) external;", + "visibility": "external", + "mutability": "", + "signature": "snapshotValue(string,uint256)", + "selector": "0x51db805a", + "selectorBytes": [ + 81, + 219, + 128, + 90 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "snapshotValue_1", + "description": "Snapshot capture an arbitrary numerical value by name in a group.", + "declaration": "function snapshotValue(string calldata group, string calldata name, uint256 value) external;", + "visibility": "external", + "mutability": "", + "signature": "snapshotValue(string,string,uint256)", + "selector": "0x6d2b27d8", + "selectorBytes": [ + 109, + 43, + 39, + 216 + ] + }, + "group": "evm", "status": "stable", "safety": "unsafe" }, @@ -8351,6 +8959,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "startDebugTraceRecording", + "description": "Records the debug trace during the run.", + "declaration": "function startDebugTraceRecording() external;", + "visibility": "external", + "mutability": "", + "signature": "startDebugTraceRecording()", + "selector": "0x419c8832", + "selectorBytes": [ + 65, + 156, + 136, + 50 + ] + }, + "group": "evm", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "startMappingRecording", @@ -8411,6 +9039,46 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "startSnapshotGas_0", + "description": "Start a snapshot capture of the current gas usage by name.\nThe group name is derived from the contract name.", + "declaration": "function startSnapshotGas(string calldata name) external;", + "visibility": "external", + "mutability": "", + "signature": "startSnapshotGas(string)", + "selector": "0x3cad9d7b", + "selectorBytes": [ + 60, + 173, + 157, + 123 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "startSnapshotGas_1", + "description": "Start a snapshot capture of the current gas usage by name in a group.", + "declaration": "function startSnapshotGas(string calldata group, string calldata name) external;", + "visibility": "external", + "mutability": "", + "signature": "startSnapshotGas(string,string)", + "selector": "0x6cd0cc53", + "selectorBytes": [ + 108, + 208, + 204, + 83 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "startStateDiffRecording", @@ -8431,6 +9099,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "stopAndReturnDebugTraceRecording", + "description": "Stop debug trace recording and returns the recorded debug trace.", + "declaration": "function stopAndReturnDebugTraceRecording() external returns (DebugStep[] memory step);", + "visibility": "external", + "mutability": "", + "signature": "stopAndReturnDebugTraceRecording()", + "selector": "0xced398a2", + "selectorBytes": [ + 206, + 211, + 152, + 162 + ] + }, + "group": "evm", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "stopAndReturnStateDiff", @@ -8531,6 +9219,66 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "stopSnapshotGas_0", + "description": "Stop the snapshot capture of the current gas by latest snapshot name, capturing the gas used since the start.", + "declaration": "function stopSnapshotGas() external returns (uint256 gasUsed);", + "visibility": "external", + "mutability": "", + "signature": "stopSnapshotGas()", + "selector": "0xf6402eda", + "selectorBytes": [ + 246, + 64, + 46, + 218 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "stopSnapshotGas_1", + "description": "Stop the snapshot capture of the current gas usage by name, capturing the gas used since the start.\nThe group name is derived from the contract name.", + "declaration": "function stopSnapshotGas(string calldata name) external returns (uint256 gasUsed);", + "visibility": "external", + "mutability": "", + "signature": "stopSnapshotGas(string)", + "selector": "0x773b2805", + "selectorBytes": [ + 119, + 59, + 40, + 5 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "stopSnapshotGas_2", + "description": "Stop the snapshot capture of the current gas usage by name in a group, capturing the gas used since the start.", + "declaration": "function stopSnapshotGas(string calldata group, string calldata name) external returns (uint256 gasUsed);", + "visibility": "external", + "mutability": "", + "signature": "stopSnapshotGas(string,string)", + "selector": "0x0c9db707", + "selectorBytes": [ + 12, + 157, + 183, + 7 + ] + }, + "group": "evm", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "store", diff --git a/crates/cheatcodes/assets/cheatcodes.schema.json b/crates/cheatcodes/assets/cheatcodes.schema.json index 9301196d9..9ffc13d61 100644 --- a/crates/cheatcodes/assets/cheatcodes.schema.json +++ b/crates/cheatcodes/assets/cheatcodes.schema.json @@ -384,11 +384,20 @@ ] }, { - "description": "The cheatcode has been deprecated, meaning it will be removed in a future release.\n\nUse of deprecated cheatcodes is discouraged and will result in a warning.", - "type": "string", - "enum": [ + "description": "The cheatcode has been deprecated, meaning it will be removed in a future release.\n\nContains the optional reason for deprecation.\n\nUse of deprecated cheatcodes is discouraged and will result in a warning.", + "type": "object", + "required": [ "deprecated" - ] + ], + "properties": { + "deprecated": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false }, { "description": "The cheatcode has been removed and is no longer available for use.\n\nUse of removed cheatcodes will result in a hard error.", diff --git a/crates/cheatcodes/spec/src/cheatcode.rs b/crates/cheatcodes/spec/src/cheatcode.rs index 207cac159..bce501d45 100644 --- a/crates/cheatcodes/spec/src/cheatcode.rs +++ b/crates/cheatcodes/spec/src/cheatcode.rs @@ -23,18 +23,18 @@ pub struct Cheatcode<'a> { /// The group that the cheatcode belongs to. pub group: Group, /// The current status of the cheatcode. E.g. whether it is stable or experimental, etc. - pub status: Status, + pub status: Status<'a>, /// Whether the cheatcode is safe to use inside of scripts. E.g. it does not change state in an /// unexpected way. pub safety: Safety, } /// The status of a cheatcode. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub enum Status { +pub enum Status<'a> { /// The cheatcode and its API is currently stable. Stable, /// The cheatcode is unstable, meaning it may contain bugs and may break its API on any @@ -44,8 +44,10 @@ pub enum Status { Experimental, /// The cheatcode has been deprecated, meaning it will be removed in a future release. /// + /// Contains the optional reason for deprecation. + /// /// Use of deprecated cheatcodes is discouraged and will result in a warning. - Deprecated, + Deprecated(Option<&'a str>), /// The cheatcode has been removed and is no longer available for use. /// /// Use of removed cheatcodes will result in a hard error. diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index fffc146a9..662853e9e 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -85,6 +85,7 @@ impl Cheatcodes<'static> { Vm::AccountAccess::STRUCT.clone(), Vm::StorageAccess::STRUCT.clone(), Vm::Gas::STRUCT.clone(), + Vm::DebugStep::STRUCT.clone(), ]), enums: Cow::Owned(vec![ Vm::CallerMode::ENUM.clone(), diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 64a50983b..7b86cfdf2 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -6,6 +6,7 @@ use super::*; use crate::Vm::ForgeContext; use alloy_sol_types::sol; use foundry_macros::Cheatcode; +use std::fmt; sol! { // Cheatcodes are marked as view/pure/none using the following rules: @@ -260,6 +261,28 @@ interface Vm { uint64 depth; } + /// The result of the `stopDebugTraceRecording` call + struct DebugStep { + /// The stack before executing the step of the run. + /// stack\[0\] represents the top of the stack. + /// and only stack data relevant to the opcode execution is contained. + uint256[] stack; + /// The memory input data before executing the step of the run. + /// only input data relevant to the opcode execution is contained. + /// + /// e.g. for MLOAD, it will have memory\[offset:offset+32\] copied here. + /// the offset value can be get by the stack data. + bytes memoryInput; + /// The opcode that was accessed. + uint8 opcode; + /// The call depth of the step. + uint64 depth; + /// Whether the call end up with out of gas error. + bool isOutOfGas; + /// The contract address where the opcode is running + address contractAddr; + } + // ======== EVM ======== /// Gets the address for a given private key. @@ -282,10 +305,25 @@ interface Vm { #[cheatcode(group = Evm, safety = Safe)] function load(address target, bytes32 slot) external view returns (bytes32 data); - /// Load a genesis JSON file's `allocs` into the in-memory revm state. + /// Load a genesis JSON file's `allocs` into the in-memory EVM state. #[cheatcode(group = Evm, safety = Unsafe)] function loadAllocs(string calldata pathToAllocsJson) external; + // -------- Record Debug Traces -------- + + /// Records the debug trace during the run. + #[cheatcode(group = Evm, safety = Safe)] + function startDebugTraceRecording() external; + + /// Stop debug trace recording and returns the recorded debug trace. + #[cheatcode(group = Evm, safety = Safe)] + function stopAndReturnDebugTraceRecording() external returns (DebugStep[] memory step); + + + /// Clones a source account code, state, balance and nonce to a target account and updates in-memory EVM state. + #[cheatcode(group = Evm, safety = Unsafe)] + function cloneAccount(address source, address target) external; + // -------- Record Storage -------- /// Records all storage reads and writes. @@ -464,6 +502,14 @@ interface Vm { #[cheatcode(group = Evm, safety = Unsafe)] function mockCall(address callee, uint256 msgValue, bytes calldata data, bytes calldata returnData) external; + /// Mocks multiple calls to an address, returning specified data for each call. + #[cheatcode(group = Evm, safety = Unsafe)] + function mockCalls(address callee, bytes calldata data, bytes[] calldata returnData) external; + + /// Mocks multiple calls to an address with a specific `msg.value`, returning specified data for each call. + #[cheatcode(group = Evm, safety = Unsafe)] + function mockCalls(address callee, uint256 msgValue, bytes calldata data, bytes[] calldata returnData) external; + /// Reverts a call to an address with specified revert data. #[cheatcode(group = Evm, safety = Unsafe)] function mockCallRevert(address callee, bytes calldata data, bytes calldata revertData) external; @@ -508,13 +554,64 @@ interface Vm { #[cheatcode(group = Evm, safety = Unsafe)] function readCallers() external returns (CallerMode callerMode, address msgSender, address txOrigin); + // ----- Arbitrary Snapshots ----- + + /// Snapshot capture an arbitrary numerical value by name. + /// The group name is derived from the contract name. + #[cheatcode(group = Evm, safety = Unsafe)] + function snapshotValue(string calldata name, uint256 value) external; + + /// Snapshot capture an arbitrary numerical value by name in a group. + #[cheatcode(group = Evm, safety = Unsafe)] + function snapshotValue(string calldata group, string calldata name, uint256 value) external; + + // -------- Gas Snapshots -------- + + /// Snapshot capture the gas usage of the last call by name from the callee perspective. + #[cheatcode(group = Evm, safety = Unsafe)] + function snapshotGasLastCall(string calldata name) external returns (uint256 gasUsed); + + /// Snapshot capture the gas usage of the last call by name in a group from the callee perspective. + #[cheatcode(group = Evm, safety = Unsafe)] + function snapshotGasLastCall(string calldata group, string calldata name) external returns (uint256 gasUsed); + + /// Start a snapshot capture of the current gas usage by name. + /// The group name is derived from the contract name. + #[cheatcode(group = Evm, safety = Unsafe)] + function startSnapshotGas(string calldata name) external; + + /// Start a snapshot capture of the current gas usage by name in a group. + #[cheatcode(group = Evm, safety = Unsafe)] + function startSnapshotGas(string calldata group, string calldata name) external; + + /// Stop the snapshot capture of the current gas by latest snapshot name, capturing the gas used since the start. + #[cheatcode(group = Evm, safety = Unsafe)] + function stopSnapshotGas() external returns (uint256 gasUsed); + + /// Stop the snapshot capture of the current gas usage by name, capturing the gas used since the start. + /// The group name is derived from the contract name. + #[cheatcode(group = Evm, safety = Unsafe)] + function stopSnapshotGas(string calldata name) external returns (uint256 gasUsed); + + /// Stop the snapshot capture of the current gas usage by name in a group, capturing the gas used since the start. + #[cheatcode(group = Evm, safety = Unsafe)] + function stopSnapshotGas(string calldata group, string calldata name) external returns (uint256 gasUsed); + // -------- State Snapshots -------- + /// `snapshot` is being deprecated in favor of `snapshotState`. It will be removed in future versions. + #[cheatcode(group = Evm, safety = Unsafe, status = Deprecated(Some("replaced by `snapshotState`")))] + function snapshot() external returns (uint256 snapshotId); + /// Snapshot the current state of the evm. /// Returns the ID of the snapshot that was created. - /// To revert a snapshot use `revertTo`. + /// To revert a snapshot use `revertToState`. #[cheatcode(group = Evm, safety = Unsafe)] - function snapshot() external returns (uint256 snapshotId); + function snapshotState() external returns (uint256 snapshotId); + + /// `revertTo` is being deprecated in favor of `revertToState`. It will be removed in future versions. + #[cheatcode(group = Evm, safety = Unsafe, status = Deprecated(Some("replaced by `revertToState`")))] + function revertTo(uint256 snapshotId) external returns (bool success); /// Revert the state of the EVM to a previous snapshot /// Takes the snapshot ID to revert to. @@ -522,9 +619,13 @@ interface Vm { /// Returns `true` if the snapshot was successfully reverted. /// Returns `false` if the snapshot does not exist. /// - /// **Note:** This does not automatically delete the snapshot. To delete the snapshot use `deleteSnapshot`. + /// **Note:** This does not automatically delete the snapshot. To delete the snapshot use `deleteStateSnapshot`. #[cheatcode(group = Evm, safety = Unsafe)] - function revertTo(uint256 snapshotId) external returns (bool success); + function revertToState(uint256 snapshotId) external returns (bool success); + + /// `revertToAndDelete` is being deprecated in favor of `revertToStateAndDelete`. It will be removed in future versions. + #[cheatcode(group = Evm, safety = Unsafe, status = Deprecated(Some("replaced by `revertToStateAndDelete`")))] + function revertToAndDelete(uint256 snapshotId) external returns (bool success); /// Revert the state of the EVM to a previous snapshot and automatically deletes the snapshots /// Takes the snapshot ID to revert to. @@ -532,7 +633,11 @@ interface Vm { /// Returns `true` if the snapshot was successfully reverted and deleted. /// Returns `false` if the snapshot does not exist. #[cheatcode(group = Evm, safety = Unsafe)] - function revertToAndDelete(uint256 snapshotId) external returns (bool success); + function revertToStateAndDelete(uint256 snapshotId) external returns (bool success); + + /// `deleteSnapshot` is being deprecated in favor of `deleteStateSnapshot`. It will be removed in future versions. + #[cheatcode(group = Evm, safety = Unsafe, status = Deprecated(Some("replaced by `deleteStateSnapshot`")))] + function deleteSnapshot(uint256 snapshotId) external returns (bool success); /// Removes the snapshot with the given ID created by `snapshot`. /// Takes the snapshot ID to delete. @@ -540,11 +645,15 @@ interface Vm { /// Returns `true` if the snapshot was successfully deleted. /// Returns `false` if the snapshot does not exist. #[cheatcode(group = Evm, safety = Unsafe)] - function deleteSnapshot(uint256 snapshotId) external returns (bool success); + function deleteStateSnapshot(uint256 snapshotId) external returns (bool success); + + /// `deleteSnapshots` is being deprecated in favor of `deleteStateSnapshots`. It will be removed in future versions. + #[cheatcode(group = Evm, safety = Unsafe, status = Deprecated(Some("replaced by `deleteStateSnapshots`")))] + function deleteSnapshots() external; /// Removes _all_ snapshots previously created by `snapshot`. #[cheatcode(group = Evm, safety = Unsafe)] - function deleteSnapshots() external; + function deleteStateSnapshots() external; // -------- Forking -------- // --- Creation and Selection --- @@ -677,7 +786,7 @@ interface Vm { // -------- Gas Measurement -------- - /// Gets the gas used in the last call. + /// Gets the gas used in the last call from the callee perspective. #[cheatcode(group = Evm, safety = Safe)] function lastCallGas() external view returns (Gas memory gas); @@ -715,11 +824,11 @@ interface Vm { /// Writes a breakpoint to jump to in the debugger. #[cheatcode(group = Testing, safety = Safe)] - function breakpoint(string calldata char) external; + function breakpoint(string calldata char) external pure; /// Writes a conditional breakpoint to jump to in the debugger. #[cheatcode(group = Testing, safety = Safe)] - function breakpoint(string calldata char, bool value) external; + function breakpoint(string calldata char, bool value) external pure; /// Returns the Foundry version. /// Format: ++ @@ -1553,6 +1662,14 @@ interface Vm { #[cheatcode(group = Filesystem)] function writeLine(string calldata path, string calldata data) external; + /// Gets the artifact path from code (aka. creation code). + #[cheatcode(group = Filesystem)] + function getArtifactPathByCode(bytes calldata code) external view returns (string memory path); + + /// Gets the artifact path from deployed code (aka. runtime code). + #[cheatcode(group = Filesystem)] + function getArtifactPathByDeployedCode(bytes calldata deployedCode) external view returns (string memory path); + /// Gets the creation bytecode from an artifact file. Takes in the relative path to the json file or the path to the /// artifact in the form of :: where and parts are optional. #[cheatcode(group = Filesystem)] @@ -1566,7 +1683,7 @@ interface Vm { /// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the /// artifact in the form of :: where and parts are optional. /// - /// Additionaly accepts abi-encoded constructor arguments. + /// Additionally accepts abi-encoded constructor arguments. #[cheatcode(group = Filesystem)] function deployCode(string calldata artifactPath, bytes calldata constructorArgs) external returns (address deployedAddress); @@ -1815,6 +1932,10 @@ interface Vm { #[cheatcode(group = Scripting)] function broadcastRawTransaction(bytes calldata data) external; + /// Returns addresses of available unlocked wallets in the script environment. + #[cheatcode(group = Scripting)] + function getWallets() external returns (address[] memory wallets); + // ======== Utilities ======== // -------- Strings -------- @@ -1877,6 +1998,9 @@ interface Vm { /// Returns 0 in case of an empty `key`. #[cheatcode(group = String)] function indexOf(string calldata input, string calldata key) external pure returns (uint256); + /// Returns true if `search` is found in `subject`, false otherwise. + #[cheatcode(group = String)] + function contains(string calldata subject, string calldata search) external returns (bool result); // ======== JSON Parsing and Manipulation ======== @@ -1887,7 +2011,7 @@ interface Vm { /// Checks if `key` exists in a JSON object /// `keyExists` is being deprecated in favor of `keyExistsJson`. It will be removed in future versions. - #[cheatcode(group = Json, status = Deprecated)] + #[cheatcode(group = Json, status = Deprecated(Some("replaced by `keyExistsJson`")))] function keyExists(string calldata json, string calldata key) external view returns (bool); /// Checks if `key` exists in a JSON object. #[cheatcode(group = Json)] @@ -2156,6 +2280,19 @@ interface Vm { pure returns (bytes32[] memory); + /// Parses a string of TOML data and coerces it to type corresponding to `typeDescription`. + #[cheatcode(group = Toml)] + function parseTomlType(string calldata toml, string calldata typeDescription) external pure returns (bytes memory); + /// Parses a string of TOML data at `key` and coerces it to type corresponding to `typeDescription`. + #[cheatcode(group = Toml)] + function parseTomlType(string calldata toml, string calldata key, string calldata typeDescription) external pure returns (bytes memory); + /// Parses a string of TOML data at `key` and coerces it to type array corresponding to `typeDescription`. + #[cheatcode(group = Toml)] + function parseTomlTypeArray(string calldata toml, string calldata key, string calldata typeDescription) + external + pure + returns (bytes memory); + /// Returns an array of all the keys in a TOML table. #[cheatcode(group = Toml)] function parseTomlKeys(string calldata toml, string calldata key) external pure returns (string[] memory keys); @@ -2292,6 +2429,20 @@ interface Vm { #[cheatcode(group = Crypto)] function rememberKey(uint256 privateKey) external returns (address keyAddr); + /// Derive a set number of wallets from a mnemonic at the derivation path `m/44'/60'/0'/0/{0..count}`. + /// + /// The respective private keys are saved to the local forge wallet for later use and their addresses are returned. + #[cheatcode(group = Crypto)] + function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs); + + /// Derive a set number of wallets from a mnemonic in the specified language at the derivation path `m/44'/60'/0'/0/{0..count}`. + /// + /// The respective private keys are saved to the local forge wallet for later use and their addresses are returned. + #[cheatcode(group = Crypto)] + function rememberKeys(string calldata mnemonic, string calldata derivationPath, string calldata language, uint32 count) + external + returns (address[] memory keyAddrs); + // -------- Uncategorized Utilities -------- /// Labels an address in call traces. @@ -2338,14 +2489,42 @@ interface Vm { #[cheatcode(group = Utilities)] function randomUint() external returns (uint256); - /// Returns random uin256 value between the provided range (=min..=max). + /// Returns random uint256 value between the provided range (=min..=max). #[cheatcode(group = Utilities)] function randomUint(uint256 min, uint256 max) external returns (uint256); + /// Returns a random `uint256` value of given bits. + #[cheatcode(group = Utilities)] + function randomUint(uint256 bits) external view returns (uint256); + /// Returns a random `address`. #[cheatcode(group = Utilities)] function randomAddress() external returns (address); + /// Returns a random `int256` value. + #[cheatcode(group = Utilities)] + function randomInt() external view returns (int256); + + /// Returns a random `int256` value of given bits. + #[cheatcode(group = Utilities)] + function randomInt(uint256 bits) external view returns (int256); + + /// Returns a random `bool`. + #[cheatcode(group = Utilities)] + function randomBool() external view returns (bool); + + /// Returns a random byte array value of the given length. + #[cheatcode(group = Utilities)] + function randomBytes(uint256 len) external view returns (bytes memory); + + /// Returns a random fixed-size byte array of length 4. + #[cheatcode(group = Utilities)] + function randomBytes4() external view returns (bytes4); + + /// Returns a random fixed-size byte array of length 8. + #[cheatcode(group = Utilities)] + function randomBytes8() external view returns (bytes8); + /// Pauses collection of call traces. Useful in cases when you want to skip tracing of /// complex calls which are not useful for debugging. #[cheatcode(group = Utilities)] @@ -2387,3 +2566,22 @@ impl PartialEq for ForgeContext { } } } + +impl fmt::Display for Vm::CheatcodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.message.fmt(f) + } +} + +impl fmt::Display for Vm::VmErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CheatcodeError(err) => err.fmt(f), + } + } +} + +#[track_caller] +const fn panic_unknown_safety() -> ! { + panic!("cannot determine safety from the group, add a `#[cheatcode(safety = ...)]` attribute") +} diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 0dd0e9093..519ceda5e 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -1,6 +1,6 @@ use super::Result; -use crate::{script::ScriptWallets, Vm::Rpc}; -use alloy_primitives::{Address, U256}; +use crate::Vm::Rpc; +use alloy_primitives::{map::AddressHashMap, U256}; use foundry_common::{fs::normalize_path, ContractsByArtifact}; use foundry_compilers::{utils::canonicalize, ProjectPathsConfig}; use foundry_config::{ @@ -11,7 +11,6 @@ use foundry_evm_core::opts::EvmOpts; use foundry_zksync_compiler::DualCompiledContracts; use semver::Version; use std::{ - collections::HashMap, path::{Path, PathBuf}, time::Duration, }; @@ -44,13 +43,13 @@ pub struct CheatsConfig { /// How the evm was configured by the user pub evm_opts: EvmOpts, /// Address labels from config - pub labels: HashMap, - /// Script wallets - pub script_wallets: Option, + pub labels: AddressHashMap, /// Artifacts which are guaranteed to be fresh (either recompiled or cached). /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list. /// If None, no validation is performed. pub available_artifacts: Option, + /// Name of the script/test contract which is currently running. + pub running_contract: Option, /// Version of the script/test contract which is currently running. pub running_version: Option, /// ZKSolc -> Solc Contract codes @@ -69,7 +68,7 @@ impl CheatsConfig { config: &Config, evm_opts: EvmOpts, available_artifacts: Option, - script_wallets: Option, + running_contract: Option, running_version: Option, dual_compiled_contracts: DualCompiledContracts, use_zk: bool, @@ -98,8 +97,8 @@ impl CheatsConfig { allowed_paths, evm_opts, labels: config.labels.clone(), - script_wallets, available_artifacts, + running_contract, running_version, dual_compiled_contracts, use_zk, @@ -229,8 +228,8 @@ impl Default for CheatsConfig { allowed_paths: vec![], evm_opts: Default::default(), labels: Default::default(), - script_wallets: None, available_artifacts: Default::default(), + running_contract: Default::default(), running_version: Default::default(), dual_compiled_contracts: Default::default(), use_zk: false, diff --git a/crates/cheatcodes/src/crypto.rs b/crates/cheatcodes/src/crypto.rs index f080938ac..cdb07720c 100644 --- a/crates/cheatcodes/src/crypto.rs +++ b/crates/cheatcodes/src/crypto.rs @@ -8,7 +8,7 @@ use alloy_signer_local::{ ChineseSimplified, ChineseTraditional, Czech, English, French, Italian, Japanese, Korean, Portuguese, Spanish, Wordlist, }, - MnemonicBuilder, PrivateKeySigner, + LocalSigner, MnemonicBuilder, PrivateKeySigner, }; use alloy_sol_types::SolValue; use k256::{ @@ -89,14 +89,45 @@ impl Cheatcode for rememberKeyCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { privateKey } = self; let wallet = parse_wallet(privateKey)?; - let address = wallet.address(); - if let Some(script_wallets) = state.script_wallets() { - script_wallets.add_local_signer(wallet); - } + let address = inject_wallet(state, wallet); Ok(address.abi_encode()) } } +impl Cheatcode for rememberKeys_0Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { mnemonic, derivationPath, count } = self; + let wallets = derive_wallets::(mnemonic, derivationPath, *count)?; + let mut addresses = Vec::
::with_capacity(wallets.len()); + for wallet in wallets { + let addr = inject_wallet(state, wallet); + addresses.push(addr); + } + + Ok(addresses.abi_encode()) + } +} + +impl Cheatcode for rememberKeys_1Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { mnemonic, derivationPath, language, count } = self; + let wallets = derive_wallets_str(mnemonic, derivationPath, language, *count)?; + let mut addresses = Vec::
::with_capacity(wallets.len()); + for wallet in wallets { + let addr = inject_wallet(state, wallet); + addresses.push(addr); + } + + Ok(addresses.abi_encode()) + } +} + +fn inject_wallet(state: &mut Cheatcodes, wallet: LocalSigner) -> Address { + let address = wallet.address(); + state.wallets().add_local_signer(wallet); + address +} + impl Cheatcode for sign_1Call { fn apply(&self, _state: &mut Cheatcodes) -> Result { let Self { privateKey, digest } = self; @@ -213,13 +244,13 @@ fn sign_with_wallet( signer: Option
, digest: &B256, ) -> Result { - let Some(script_wallets) = state.script_wallets() else { - bail!("no wallets are available"); - }; + if state.wallets().is_empty() { + bail!("no wallets available"); + } - let mut script_wallets = script_wallets.inner.lock(); - let maybe_provided_sender = script_wallets.provided_sender; - let signers = script_wallets.multi_wallet.signers()?; + let mut wallets = state.wallets().inner.lock(); + let maybe_provided_sender = wallets.provided_sender; + let signers = wallets.multi_wallet.signers()?; let signer = if let Some(signer) = signer { signer @@ -228,7 +259,7 @@ fn sign_with_wallet( } else if signers.len() == 1 { *signers.keys().next().unwrap() } else { - bail!("could not determine signer"); + bail!("could not determine signer, there are multiple signers available use vm.sign(signer, digest) to specify one"); }; let wallet = signers @@ -309,6 +340,50 @@ fn derive_key(mnemonic: &str, path: &str, index: u32) -> Result { Ok(private_key.abi_encode()) } +fn derive_wallets_str( + mnemonic: &str, + path: &str, + language: &str, + count: u32, +) -> Result>> { + match language { + "chinese_simplified" => derive_wallets::(mnemonic, path, count), + "chinese_traditional" => derive_wallets::(mnemonic, path, count), + "czech" => derive_wallets::(mnemonic, path, count), + "english" => derive_wallets::(mnemonic, path, count), + "french" => derive_wallets::(mnemonic, path, count), + "italian" => derive_wallets::(mnemonic, path, count), + "japanese" => derive_wallets::(mnemonic, path, count), + "korean" => derive_wallets::(mnemonic, path, count), + "portuguese" => derive_wallets::(mnemonic, path, count), + "spanish" => derive_wallets::(mnemonic, path, count), + _ => Err(fmt_err!("unsupported mnemonic language: {language:?}")), + } +} + +fn derive_wallets( + mnemonic: &str, + path: &str, + count: u32, +) -> Result>> { + let mut out = path.to_string(); + + if !out.ends_with('/') { + out.push('/'); + } + + let mut wallets = Vec::with_capacity(count as usize); + for idx in 0..count { + let wallet = MnemonicBuilder::::default() + .phrase(mnemonic) + .derivation_path(format!("{out}{idx}"))? + .build()?; + wallets.push(wallet); + } + + Ok(wallets) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 97e293cf3..6d1da5ce6 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1,7 +1,9 @@ //! Implementations of [`Evm`](spec::Group::Evm) cheatcodes. use crate::{ - BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, + inspector::{InnerEcx, RecordDebugStepInfo}, + BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Error, Result, + Vm::*, }; use alloy_consensus::TxEnvelope; use alloy_genesis::{Genesis, GenesisAccount}; @@ -11,21 +13,34 @@ use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; use foundry_evm_core::{ abi::HARDHAT_CONSOLE_ADDRESS, - backend::{DatabaseExt, RevertSnapshotAction}, + backend::{DatabaseExt, RevertStateSnapshotAction}, constants::{CALLER, CHEATCODE_ADDRESS, TEST_CONTRACT_ADDRESS}, }; +use foundry_evm_traces::StackSnapshotType; use rand::Rng; -use revm::{ - primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY}, - InnerEvmContext, -}; +use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY}; use std::{collections::BTreeMap, path::Path}; +mod record_debug_step; +use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace}; mod fork; pub(crate) mod mapping; pub(crate) mod mock; pub(crate) mod prank; +/// Records the `snapshotGas*` cheatcodes. +#[derive(Clone, Debug)] +pub struct GasRecord { + /// The group name of the gas snapshot. + pub group: String, + /// The name of the gas snapshot. + pub name: String, + /// The total gas used in the gas snapshot. + pub gas_used: u64, + /// Depth at which the gas snapshot was taken. + pub depth: u64, +} + /// Records `deal` cheatcodes #[derive(Clone, Debug)] pub struct DealRecord { @@ -46,7 +61,7 @@ impl Cheatcode for addrCall { } impl Cheatcode for getNonce_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; if ccx.state.use_zk_vm { @@ -59,14 +74,14 @@ impl Cheatcode for getNonce_0Call { } impl Cheatcode for getNonce_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { wallet } = self; get_nonce(ccx, &wallet.addr) } } impl Cheatcode for loadCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target, slot } = *self; ensure_not_precompile!(&target, ccx); ccx.ecx.load_account(target)?; @@ -103,7 +118,7 @@ impl Cheatcode for loadCall { } impl Cheatcode for loadAllocsCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { pathToAllocsJson } = self; let path = Path::new(pathToAllocsJson); @@ -128,8 +143,24 @@ impl Cheatcode for loadAllocsCall { } } +impl Cheatcode for cloneAccountCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { source, target } = self; + + let account = ccx.ecx.journaled_state.load_account(*source, &mut ccx.ecx.db)?; + ccx.ecx.db.clone_account( + &genesis_account(account.data), + target, + &mut ccx.ecx.journaled_state, + )?; + // Cloned account should persist in forked envs. + ccx.ecx.db.add_persistent_account(*target); + Ok(Default::default()) + } +} + impl Cheatcode for dumpStateCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { pathToStateJson } = self; let path = Path::new(pathToStateJson); @@ -150,23 +181,7 @@ impl Cheatcode for dumpStateCall { .state() .iter_mut() .filter(|(key, val)| !skip(key, val)) - .map(|(key, val)| { - ( - key, - GenesisAccount { - nonce: Some(val.info.nonce), - balance: val.info.balance, - code: val.info.code.as_ref().map(|o| o.original_bytes()), - storage: Some( - val.storage - .iter() - .map(|(k, v)| (B256::from(*k), B256::from(v.present_value()))) - .collect(), - ), - private_key: None, - }, - ) - }) + .map(|(key, val)| (key, genesis_account(val))) .collect::>(); write_json_file(path, &alloc)?; @@ -249,7 +264,7 @@ impl Cheatcode for lastCallGasCall { } impl Cheatcode for chainIdCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newChainId } = self; ensure!(*newChainId <= U256::from(u64::MAX), "chain ID must be less than 2^64 - 1"); ccx.ecx.env.cfg.chain_id = newChainId.to(); @@ -258,7 +273,7 @@ impl Cheatcode for chainIdCall { } impl Cheatcode for coinbaseCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newCoinbase } = self; ccx.ecx.env.block.coinbase = *newCoinbase; Ok(Default::default()) @@ -266,7 +281,7 @@ impl Cheatcode for coinbaseCall { } impl Cheatcode for difficultyCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newDifficulty } = self; ensure!( ccx.ecx.spec_id() < SpecId::MERGE, @@ -279,7 +294,7 @@ impl Cheatcode for difficultyCall { } impl Cheatcode for feeCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newBasefee } = self; ccx.ecx.env.block.basefee = *newBasefee; Ok(Default::default()) @@ -287,7 +302,7 @@ impl Cheatcode for feeCall { } impl Cheatcode for prevrandao_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newPrevrandao } = self; ensure!( ccx.ecx.spec_id() >= SpecId::MERGE, @@ -300,7 +315,7 @@ impl Cheatcode for prevrandao_0Call { } impl Cheatcode for prevrandao_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newPrevrandao } = self; ensure!( ccx.ecx.spec_id() >= SpecId::MERGE, @@ -313,7 +328,7 @@ impl Cheatcode for prevrandao_1Call { } impl Cheatcode for blobhashesCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { hashes } = self; ensure!( ccx.ecx.spec_id() >= SpecId::CANCUN, @@ -326,7 +341,7 @@ impl Cheatcode for blobhashesCall { } impl Cheatcode for getBlobhashesCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; ensure!( ccx.ecx.spec_id() >= SpecId::CANCUN, @@ -338,7 +353,7 @@ impl Cheatcode for getBlobhashesCall { } impl Cheatcode for rollCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newHeight } = self; if ccx.state.use_zk_vm { foundry_zksync_core::cheatcodes::roll(*newHeight, ccx.ecx); @@ -351,14 +366,14 @@ impl Cheatcode for rollCall { } impl Cheatcode for getBlockNumberCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; Ok(ccx.ecx.env.block.number.abi_encode()) } } impl Cheatcode for txGasPriceCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newGasPrice } = self; ccx.ecx.env.tx.gas_price = *newGasPrice; Ok(Default::default()) @@ -366,7 +381,7 @@ impl Cheatcode for txGasPriceCall { } impl Cheatcode for warpCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newTimestamp } = self; if ccx.state.use_zk_vm { foundry_zksync_core::cheatcodes::warp(*newTimestamp, ccx.ecx); @@ -379,14 +394,14 @@ impl Cheatcode for warpCall { } impl Cheatcode for getBlockTimestampCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; Ok(ccx.ecx.env.block.timestamp.abi_encode()) } } impl Cheatcode for blobBaseFeeCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { newBlobBaseFee } = self; ensure!( ccx.ecx.spec_id() >= SpecId::CANCUN, @@ -399,14 +414,14 @@ impl Cheatcode for blobBaseFeeCall { } impl Cheatcode for getBlobBaseFeeCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; Ok(ccx.ecx.env.block.get_blob_excess_gas().unwrap_or(0).abi_encode()) } } impl Cheatcode for dealCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account: address, newBalance: new_balance } = *self; let old_balance = if ccx.state.use_zk_vm { foundry_zksync_core::cheatcodes::deal(address, new_balance, ccx.ecx) @@ -421,7 +436,7 @@ impl Cheatcode for dealCall { } impl Cheatcode for etchCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target, newRuntimeBytecode } = self; if ccx.state.use_zk_vm { foundry_zksync_core::cheatcodes::etch(*target, newRuntimeBytecode, ccx.ecx); @@ -438,7 +453,7 @@ impl Cheatcode for etchCall { } impl Cheatcode for resetNonceCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; if ccx.state.use_zk_vm { foundry_zksync_core::cheatcodes::set_nonce(*account, U256::ZERO, ccx.ecx); @@ -458,7 +473,7 @@ impl Cheatcode for resetNonceCall { } impl Cheatcode for setNonceCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account, newNonce } = *self; if ccx.state.use_zk_vm { @@ -480,7 +495,7 @@ impl Cheatcode for setNonceCall { } impl Cheatcode for setNonceUnsafeCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account, newNonce } = *self; if ccx.state.use_zk_vm { @@ -495,7 +510,7 @@ impl Cheatcode for setNonceUnsafeCall { } impl Cheatcode for storeCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target, slot, value } = *self; ensure_not_precompile!(&target, ccx); // ensure the account is touched @@ -506,7 +521,7 @@ impl Cheatcode for storeCall { } impl Cheatcode for coolCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target } = self; if let Some(account) = ccx.ecx.journaled_state.state.get_mut(target) { account.unmark_touch(); @@ -517,69 +532,158 @@ impl Cheatcode for coolCall { } impl Cheatcode for readCallersCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; read_callers(ccx.state, &ccx.ecx.env.tx.caller) } } +impl Cheatcode for snapshotValue_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { name, value } = self; + inner_value_snapshot(ccx, None, Some(name.clone()), value.to_string()) + } +} + +impl Cheatcode for snapshotValue_1Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { group, name, value } = self; + inner_value_snapshot(ccx, Some(group.clone()), Some(name.clone()), value.to_string()) + } +} + +impl Cheatcode for snapshotGasLastCall_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { name } = self; + let Some(last_call_gas) = &ccx.state.gas_metering.last_call_gas else { + bail!("no external call was made yet"); + }; + inner_last_gas_snapshot(ccx, None, Some(name.clone()), last_call_gas.gasTotalUsed) + } +} + +impl Cheatcode for snapshotGasLastCall_1Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { name, group } = self; + let Some(last_call_gas) = &ccx.state.gas_metering.last_call_gas else { + bail!("no external call was made yet"); + }; + inner_last_gas_snapshot( + ccx, + Some(group.clone()), + Some(name.clone()), + last_call_gas.gasTotalUsed, + ) + } +} + +impl Cheatcode for startSnapshotGas_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { name } = self; + inner_start_gas_snapshot(ccx, None, Some(name.clone())) + } +} + +impl Cheatcode for startSnapshotGas_1Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { group, name } = self; + inner_start_gas_snapshot(ccx, Some(group.clone()), Some(name.clone())) + } +} + +impl Cheatcode for stopSnapshotGas_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self {} = self; + inner_stop_gas_snapshot(ccx, None, None) + } +} + +impl Cheatcode for stopSnapshotGas_1Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { name } = self; + inner_stop_gas_snapshot(ccx, None, Some(name.clone())) + } +} + +impl Cheatcode for stopSnapshotGas_2Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { group, name } = self; + inner_stop_gas_snapshot(ccx, Some(group.clone()), Some(name.clone())) + } +} + +// Deprecated in favor of `snapshotStateCall` impl Cheatcode for snapshotCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self {} = self; + inner_snapshot_state(ccx) + } +} + +impl Cheatcode for snapshotStateCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; - Ok(ccx.ecx.db.snapshot(&ccx.ecx.journaled_state, &ccx.ecx.env).abi_encode()) + inner_snapshot_state(ccx) } } +// Deprecated in favor of `revertToStateCall` impl Cheatcode for revertToCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { snapshotId } = self; - let result = if let Some(journaled_state) = ccx.ecx.db.revert( - *snapshotId, - &ccx.ecx.journaled_state, - &mut ccx.ecx.env, - RevertSnapshotAction::RevertKeep, - ) { - // we reset the evm's journaled_state to the state of the snapshot previous state - ccx.ecx.journaled_state = journaled_state; - true - } else { - false - }; - Ok(result.abi_encode()) + inner_revert_to_state(ccx, *snapshotId) + } +} + +impl Cheatcode for revertToStateCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { snapshotId } = self; + inner_revert_to_state(ccx, *snapshotId) } } +// Deprecated in favor of `revertToStateAndDeleteCall` impl Cheatcode for revertToAndDeleteCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { snapshotId } = self; - let result = if let Some(journaled_state) = ccx.ecx.db.revert( - *snapshotId, - &ccx.ecx.journaled_state, - &mut ccx.ecx.env, - RevertSnapshotAction::RevertRemove, - ) { - // we reset the evm's journaled_state to the state of the snapshot previous state - ccx.ecx.journaled_state = journaled_state; - true - } else { - false - }; - Ok(result.abi_encode()) + inner_revert_to_state_and_delete(ccx, *snapshotId) + } +} + +impl Cheatcode for revertToStateAndDeleteCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { snapshotId } = self; + inner_revert_to_state_and_delete(ccx, *snapshotId) } } +// Deprecated in favor of `deleteStateSnapshotCall` impl Cheatcode for deleteSnapshotCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { snapshotId } = self; + inner_delete_state_snapshot(ccx, *snapshotId) + } +} + +impl Cheatcode for deleteStateSnapshotCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { snapshotId } = self; - let result = ccx.ecx.db.delete_snapshot(*snapshotId); - Ok(result.abi_encode()) + inner_delete_state_snapshot(ccx, *snapshotId) } } + +// Deprecated in favor of `deleteStateSnapshotsCall` impl Cheatcode for deleteSnapshotsCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; - ccx.ecx.db.delete_snapshots(); - Ok(Default::default()) + inner_delete_state_snapshots(ccx) + } +} + +impl Cheatcode for deleteStateSnapshotsCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self {} = self; + inner_delete_state_snapshots(ccx) } } @@ -599,20 +703,15 @@ impl Cheatcode for stopAndReturnStateDiffCall { } impl Cheatcode for broadcastRawTransactionCall { - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { - let mut data = self.data.as_ref(); - let tx = TxEnvelope::decode(&mut data) + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { + let tx = TxEnvelope::decode(&mut self.data.as_ref()) .map_err(|err| fmt_err!("failed to decode RLP-encoded transaction: {err}"))?; ccx.ecx.db.transact_from_tx( - tx.clone().into(), - &ccx.ecx.env, + &tx.clone().into(), + (*ccx.ecx.env).clone(), &mut ccx.ecx.journaled_state, - &mut executor.get_inspector(ccx.state), + &mut *executor.get_inspector(ccx.state), )?; if ccx.state.broadcast.is_some() { @@ -628,7 +727,7 @@ impl Cheatcode for broadcastRawTransactionCall { } impl Cheatcode for setBlockhashCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { blockNumber, blockHash } = *self; ensure!( blockNumber <= ccx.ecx.env.block.number, @@ -641,11 +740,231 @@ impl Cheatcode for setBlockhashCall { } } -pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result { +impl Cheatcode for startDebugTraceRecordingCall { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { + let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else { + return Err(Error::from("no tracer initiated, consider adding -vvv flag")) + }; + + let mut info = RecordDebugStepInfo { + // will be updated later + start_node_idx: 0, + // keep the original config to revert back later + original_tracer_config: *tracer.config(), + }; + + // turn on tracer configuration for recording + tracer.update_config(|config| { + config + .set_steps(true) + .set_memory_snapshots(true) + .set_stack_snapshots(StackSnapshotType::Full) + }); + + // track where the recording starts + if let Some(last_node) = tracer.traces().nodes().last() { + info.start_node_idx = last_node.idx; + } + + ccx.state.record_debug_steps_info = Some(info); + Ok(Default::default()) + } +} + +impl Cheatcode for stopAndReturnDebugTraceRecordingCall { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { + let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else { + return Err(Error::from("no tracer initiated, consider adding -vvv flag")) + }; + + let Some(record_info) = ccx.state.record_debug_steps_info else { + return Err(Error::from("nothing recorded")) + }; + + // Revert the tracer config to the one before recording + tracer.update_config(|_config| record_info.original_tracer_config); + + // Use the trace nodes to flatten the call trace + let root = tracer.traces(); + let steps = flatten_call_trace(0, root, record_info.start_node_idx); + + let debug_steps: Vec = + steps.iter().map(|&step| convert_call_trace_to_debug_step(step)).collect(); + + // Clean up the recording info + ccx.state.record_debug_steps_info = None; + + Ok(debug_steps.abi_encode()) + } +} + +pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result { let account = ccx.ecx.journaled_state.load_account(*address, &mut ccx.ecx.db)?; Ok(account.info.nonce.abi_encode()) } +fn inner_snapshot_state(ccx: &mut CheatsCtxt) -> Result { + Ok(ccx.ecx.db.snapshot_state(&ccx.ecx.journaled_state, &ccx.ecx.env).abi_encode()) +} + +fn inner_revert_to_state(ccx: &mut CheatsCtxt, snapshot_id: U256) -> Result { + let result = if let Some(journaled_state) = ccx.ecx.db.revert_state( + snapshot_id, + &ccx.ecx.journaled_state, + &mut ccx.ecx.env, + RevertStateSnapshotAction::RevertKeep, + ) { + // we reset the evm's journaled_state to the state of the snapshot previous state + ccx.ecx.journaled_state = journaled_state; + true + } else { + false + }; + Ok(result.abi_encode()) +} + +fn inner_revert_to_state_and_delete(ccx: &mut CheatsCtxt, snapshot_id: U256) -> Result { + let result = if let Some(journaled_state) = ccx.ecx.db.revert_state( + snapshot_id, + &ccx.ecx.journaled_state, + &mut ccx.ecx.env, + RevertStateSnapshotAction::RevertRemove, + ) { + // we reset the evm's journaled_state to the state of the snapshot previous state + ccx.ecx.journaled_state = journaled_state; + true + } else { + false + }; + Ok(result.abi_encode()) +} + +fn inner_delete_state_snapshot(ccx: &mut CheatsCtxt, snapshot_id: U256) -> Result { + let result = ccx.ecx.db.delete_state_snapshot(snapshot_id); + Ok(result.abi_encode()) +} + +fn inner_delete_state_snapshots(ccx: &mut CheatsCtxt) -> Result { + ccx.ecx.db.delete_state_snapshots(); + Ok(Default::default()) +} + +fn inner_value_snapshot( + ccx: &mut CheatsCtxt, + group: Option, + name: Option, + value: String, +) -> Result { + let (group, name) = derive_snapshot_name(ccx, group, name); + + ccx.state.gas_snapshots.entry(group).or_default().insert(name, value); + + Ok(Default::default()) +} + +fn inner_last_gas_snapshot( + ccx: &mut CheatsCtxt, + group: Option, + name: Option, + value: u64, +) -> Result { + let (group, name) = derive_snapshot_name(ccx, group, name); + + ccx.state.gas_snapshots.entry(group).or_default().insert(name, value.to_string()); + + Ok(value.abi_encode()) +} + +fn inner_start_gas_snapshot( + ccx: &mut CheatsCtxt, + group: Option, + name: Option, +) -> Result { + // Revert if there is an active gas snapshot as we can only have one active snapshot at a time. + if ccx.state.gas_metering.active_gas_snapshot.is_some() { + let (group, name) = ccx.state.gas_metering.active_gas_snapshot.as_ref().unwrap().clone(); + bail!("gas snapshot was already started with group: {group} and name: {name}"); + } + + let (group, name) = derive_snapshot_name(ccx, group, name); + + ccx.state.gas_metering.gas_records.push(GasRecord { + group: group.clone(), + name: name.clone(), + gas_used: 0, + depth: ccx.ecx.journaled_state.depth(), + }); + + ccx.state.gas_metering.active_gas_snapshot = Some((group, name)); + + ccx.state.gas_metering.start(); + + Ok(Default::default()) +} + +fn inner_stop_gas_snapshot( + ccx: &mut CheatsCtxt, + group: Option, + name: Option, +) -> Result { + // If group and name are not provided, use the last snapshot group and name. + let (group, name) = group.zip(name).unwrap_or_else(|| { + let (group, name) = ccx.state.gas_metering.active_gas_snapshot.as_ref().unwrap().clone(); + (group, name) + }); + + if let Some(record) = ccx + .state + .gas_metering + .gas_records + .iter_mut() + .find(|record| record.group == group && record.name == name) + { + // Calculate the gas used since the snapshot was started. + // We subtract 171 from the gas used to account for gas used by the snapshot itself. + let value = record.gas_used.saturating_sub(171); + + ccx.state + .gas_snapshots + .entry(group.clone()) + .or_default() + .insert(name.clone(), value.to_string()); + + // Stop the gas metering. + ccx.state.gas_metering.stop(); + + // Remove the gas record. + ccx.state + .gas_metering + .gas_records + .retain(|record| record.group != group && record.name != name); + + // Clear last snapshot cache if we have an exact match. + if let Some((snapshot_group, snapshot_name)) = &ccx.state.gas_metering.active_gas_snapshot { + if snapshot_group == &group && snapshot_name == &name { + ccx.state.gas_metering.active_gas_snapshot = None; + } + } + + Ok(value.abi_encode()) + } else { + bail!("no gas snapshot was started with the name: {name} in group: {group}"); + } +} + +// Derives the snapshot group and name from the provided group and name or the running contract. +fn derive_snapshot_name( + ccx: &CheatsCtxt, + group: Option, + name: Option, +) -> (String, String) { + let group = group.unwrap_or_else(|| { + ccx.state.config.running_contract.clone().expect("expected running contract") + }); + let name = name.unwrap_or_else(|| "default".to_string()); + (group, name) +} + /// Reads the current caller information and returns the current [CallerMode], `msg.sender` and /// `tx.origin`. /// @@ -695,10 +1014,10 @@ fn read_callers(state: &Cheatcodes, default_sender: &Address) -> Result { } /// Ensures the `Account` is loaded and touched. -pub(super) fn journaled_account( - ecx: &mut InnerEvmContext, +pub(super) fn journaled_account<'a>( + ecx: InnerEcx<'a, '_, '_>, addr: Address, -) -> Result<&mut Account> { +) -> Result<&'a mut Account> { ecx.load_account(addr)?; ecx.journaled_state.touch(&addr); Ok(ecx.journaled_state.state.get_mut(&addr).expect("account is loaded")) @@ -721,3 +1040,20 @@ fn get_state_diff(state: &mut Cheatcodes) -> Result { .collect::>(); Ok(res.abi_encode()) } + +/// Helper function that creates a `GenesisAccount` from a regular `Account`. +fn genesis_account(account: &Account) -> GenesisAccount { + GenesisAccount { + nonce: Some(account.info.nonce), + balance: account.info.balance, + code: account.info.code.as_ref().map(|o| o.original_bytes()), + storage: Some( + account + .storage + .iter() + .map(|(k, v)| (B256::from(*k), B256::from(v.present_value()))) + .collect(), + ), + private_key: None, + } +} diff --git a/crates/cheatcodes/src/evm/fork.rs b/crates/cheatcodes/src/evm/fork.rs index f48ed63c7..d1474fe99 100644 --- a/crates/cheatcodes/src/evm/fork.rs +++ b/crates/cheatcodes/src/evm/fork.rs @@ -1,4 +1,4 @@ -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Result, Vm::*}; +use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, DatabaseExt, Result, Vm::*}; use alloy_dyn_abi::DynSolValue; use alloy_primitives::{B256, U256}; use alloy_provider::Provider; @@ -8,7 +8,7 @@ use foundry_common::provider::ProviderBuilder; use foundry_evm_core::fork::CreateFork; impl Cheatcode for activeForkCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; ccx.ecx .db @@ -19,49 +19,49 @@ impl Cheatcode for activeForkCall { } impl Cheatcode for createFork_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias } = self; create_fork(ccx, urlOrAlias, None) } } impl Cheatcode for createFork_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias, blockNumber } = self; create_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to())) } } impl Cheatcode for createFork_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias, txHash } = self; create_fork_at_transaction(ccx, urlOrAlias, txHash) } } impl Cheatcode for createSelectFork_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias } = self; create_select_fork(ccx, urlOrAlias, None) } } impl Cheatcode for createSelectFork_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias, blockNumber } = self; create_select_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to())) } } impl Cheatcode for createSelectFork_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { urlOrAlias, txHash } = self; create_select_fork_at_transaction(ccx, urlOrAlias, txHash) } } impl Cheatcode for rollFork_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { blockNumber } = self; persist_caller(ccx); ccx.ecx.db.roll_fork( @@ -75,7 +75,7 @@ impl Cheatcode for rollFork_0Call { } impl Cheatcode for rollFork_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { txHash } = self; persist_caller(ccx); ccx.ecx.db.roll_fork_to_transaction( @@ -89,7 +89,7 @@ impl Cheatcode for rollFork_1Call { } impl Cheatcode for rollFork_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { forkId, blockNumber } = self; persist_caller(ccx); ccx.ecx.db.roll_fork( @@ -103,7 +103,7 @@ impl Cheatcode for rollFork_2Call { } impl Cheatcode for rollFork_3Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { forkId, txHash } = self; persist_caller(ccx); ccx.ecx.db.roll_fork_to_transaction( @@ -117,7 +117,7 @@ impl Cheatcode for rollFork_3Call { } impl Cheatcode for selectForkCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { forkId } = self; persist_caller(ccx); check_broadcast(ccx.state)?; @@ -130,43 +130,21 @@ impl Cheatcode for selectForkCall { } impl Cheatcode for transact_0Call { - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let Self { txHash } = *self; - ccx.ecx.db.transact( - None, - txHash, - &mut ccx.ecx.env, - &mut ccx.ecx.journaled_state, - &mut executor.get_inspector(ccx.state), - )?; - Ok(Default::default()) + transact(ccx, executor, txHash, None) } } impl Cheatcode for transact_1Call { - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let Self { forkId, txHash } = *self; - ccx.ecx.db.transact( - Some(forkId), - txHash, - &mut ccx.ecx.env, - &mut ccx.ecx.journaled_state, - &mut executor.get_inspector(ccx.state), - )?; - Ok(Default::default()) + transact(ccx, executor, txHash, Some(forkId)) } } impl Cheatcode for allowCheatcodesCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; ccx.ecx.db.allow_cheatcode_access(*account); Ok(Default::default()) @@ -174,7 +152,7 @@ impl Cheatcode for allowCheatcodesCall { } impl Cheatcode for makePersistent_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; ccx.ecx.db.add_persistent_account(*account); Ok(Default::default()) @@ -182,7 +160,7 @@ impl Cheatcode for makePersistent_0Call { } impl Cheatcode for makePersistent_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account0, account1 } = self; ccx.ecx.db.add_persistent_account(*account0); ccx.ecx.db.add_persistent_account(*account1); @@ -191,7 +169,7 @@ impl Cheatcode for makePersistent_1Call { } impl Cheatcode for makePersistent_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account0, account1, account2 } = self; ccx.ecx.db.add_persistent_account(*account0); ccx.ecx.db.add_persistent_account(*account1); @@ -201,7 +179,7 @@ impl Cheatcode for makePersistent_2Call { } impl Cheatcode for makePersistent_3Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { accounts } = self; for account in accounts { ccx.ecx.db.add_persistent_account(*account); @@ -211,7 +189,7 @@ impl Cheatcode for makePersistent_3Call { } impl Cheatcode for revokePersistent_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; ccx.ecx.db.remove_persistent_account(account); Ok(Default::default()) @@ -219,7 +197,7 @@ impl Cheatcode for revokePersistent_0Call { } impl Cheatcode for revokePersistent_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { accounts } = self; for account in accounts { ccx.ecx.db.remove_persistent_account(account); @@ -229,14 +207,14 @@ impl Cheatcode for revokePersistent_1Call { } impl Cheatcode for isPersistentCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { account } = self; Ok(ccx.ecx.db.is_persistent(account).abi_encode()) } } impl Cheatcode for rpc_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { method, params } = self; let url = ccx.ecx.db.active_fork_url().ok_or_else(|| fmt_err!("no active fork URL found"))?; @@ -253,7 +231,7 @@ impl Cheatcode for rpc_1Call { } impl Cheatcode for eth_getLogsCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { fromBlock, toBlock, target, topics } = self; let (Ok(from_block), Ok(to_block)) = (u64::try_from(fromBlock), u64::try_from(toBlock)) else { @@ -295,11 +273,7 @@ impl Cheatcode for eth_getLogsCall { } /// Creates and then also selects the new fork -fn create_select_fork( - ccx: &mut CheatsCtxt, - url_or_alias: &str, - block: Option, -) -> Result { +fn create_select_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option) -> Result { check_broadcast(ccx.state)?; let fork = create_fork_request(ccx, url_or_alias, block)?; @@ -310,19 +284,15 @@ fn create_select_fork( } /// Creates a new fork -fn create_fork( - ccx: &mut CheatsCtxt, - url_or_alias: &str, - block: Option, -) -> Result { +fn create_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option) -> Result { let fork = create_fork_request(ccx, url_or_alias, block)?; let id = ccx.ecx.db.create_fork(fork)?; Ok(id.abi_encode()) } /// Creates and then also selects the new fork at the given transaction -fn create_select_fork_at_transaction( - ccx: &mut CheatsCtxt, +fn create_select_fork_at_transaction( + ccx: &mut CheatsCtxt, url_or_alias: &str, transaction: &B256, ) -> Result { @@ -339,8 +309,8 @@ fn create_select_fork_at_transaction( } /// Creates a new fork at the given transaction -fn create_fork_at_transaction( - ccx: &mut CheatsCtxt, +fn create_fork_at_transaction( + ccx: &mut CheatsCtxt, url_or_alias: &str, transaction: &B256, ) -> Result { @@ -350,8 +320,8 @@ fn create_fork_at_transaction( } /// Creates the request object for a new fork request -fn create_fork_request( - ccx: &mut CheatsCtxt, +fn create_fork_request( + ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option, ) -> Result { @@ -370,7 +340,6 @@ fn create_fork_request( Ok(fork) } -#[inline] fn check_broadcast(state: &Cheatcodes) -> Result<()> { if state.broadcast.is_none() { Ok(()) @@ -379,12 +348,27 @@ fn check_broadcast(state: &Cheatcodes) -> Result<()> { } } +fn transact( + ccx: &mut CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, + transaction: B256, + fork_id: Option, +) -> Result { + ccx.ecx.db.transact( + fork_id, + transaction, + (*ccx.ecx.env).clone(), + &mut ccx.ecx.journaled_state, + &mut *executor.get_inspector(ccx.state), + )?; + Ok(Default::default()) +} + // Helper to add the caller of fork cheat code as persistent account (in order to make sure that the // state of caller contract is not lost when fork changes). // Applies to create, select and roll forks actions. // https://github.com/foundry-rs/foundry/issues/8004 -#[inline] -fn persist_caller(ccx: &mut CheatsCtxt) { +fn persist_caller(ccx: &mut CheatsCtxt) { ccx.ecx.db.add_persistent_account(ccx.caller); } diff --git a/crates/cheatcodes/src/evm/mapping.rs b/crates/cheatcodes/src/evm/mapping.rs index 679609274..e8525908e 100644 --- a/crates/cheatcodes/src/evm/mapping.rs +++ b/crates/cheatcodes/src/evm/mapping.rs @@ -1,26 +1,29 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; -use alloy_primitives::{keccak256, Address, B256, U256}; +use alloy_primitives::{ + keccak256, + map::{AddressHashMap, B256HashMap}, + Address, B256, U256, +}; use alloy_sol_types::SolValue; use revm::interpreter::{opcode, Interpreter}; -use std::collections::HashMap; /// Recorded mapping slots. #[derive(Clone, Debug, Default)] pub struct MappingSlots { /// Holds mapping parent (slots => slots) - pub parent_slots: HashMap, + pub parent_slots: B256HashMap, /// Holds mapping key (slots => key) - pub keys: HashMap, + pub keys: B256HashMap, /// Holds mapping child (slots => slots[]) - pub children: HashMap>, + pub children: B256HashMap>, /// Holds the last sha3 result `sha3_result => (data_low, data_high)`, this would only record /// when sha3 is called with `size == 0x40`, and the lower 256 bits would be stored in /// `data_low`, higher 256 bits in `data_high`. /// This is needed for mapping_key detect if the slot is for some mapping and record that. - pub seen_sha3: HashMap, + pub seen_sha3: B256HashMap<(B256, B256)>, } impl MappingSlots { @@ -113,7 +116,7 @@ fn slot_child<'a>( } #[cold] -pub(crate) fn step(mapping_slots: &mut HashMap, interpreter: &Interpreter) { +pub(crate) fn step(mapping_slots: &mut AddressHashMap, interpreter: &Interpreter) { match interpreter.current_opcode() { opcode::KECCAK256 => { if interpreter.stack.peek(1) == Ok(U256::from(0x40)) { diff --git a/crates/cheatcodes/src/evm/mock.rs b/crates/cheatcodes/src/evm/mock.rs index 720dfeea6..f08d43b8d 100644 --- a/crates/cheatcodes/src/evm/mock.rs +++ b/crates/cheatcodes/src/evm/mock.rs @@ -1,7 +1,8 @@ -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Result, Vm::*}; +use crate::{inspector::InnerEcx, Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; use alloy_primitives::{Address, Bytes, U256}; use foundry_cheatcodes_common::mock::{MockCallDataContext, MockCallReturnData}; -use revm::{interpreter::InstructionResult, primitives::Bytecode, InnerEvmContext}; +use revm::{interpreter::InstructionResult, primitives::Bytecode}; +use std::collections::VecDeque; impl Cheatcode for clearMockedCallsCall { fn apply(&self, state: &mut Cheatcodes) -> Result { @@ -12,7 +13,7 @@ impl Cheatcode for clearMockedCallsCall { } impl Cheatcode for mockCall_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { callee, data, returnData } = self; let _ = make_acc_non_empty(callee, ccx.ecx)?; @@ -26,7 +27,7 @@ impl Cheatcode for mockCall_0Call { } impl Cheatcode for mockCall_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { callee, msgValue, data, returnData } = self; ccx.ecx.load_account(*callee)?; mock_call(ccx.state, callee, data, Some(msgValue), returnData, InstructionResult::Return); @@ -34,8 +35,27 @@ impl Cheatcode for mockCall_1Call { } } +impl Cheatcode for mockCalls_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { callee, data, returnData } = self; + let _ = make_acc_non_empty(callee, ccx.ecx)?; + + mock_calls(ccx.state, callee, data, None, returnData, InstructionResult::Return); + Ok(Default::default()) + } +} + +impl Cheatcode for mockCalls_1Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { callee, msgValue, data, returnData } = self; + ccx.ecx.load_account(*callee)?; + mock_calls(ccx.state, callee, data, Some(msgValue), returnData, InstructionResult::Return); + Ok(Default::default()) + } +} + impl Cheatcode for mockCallRevert_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { callee, data, revertData } = self; let _ = make_acc_non_empty(callee, ccx.ecx)?; @@ -49,7 +69,7 @@ impl Cheatcode for mockCallRevert_0Call { } impl Cheatcode for mockCallRevert_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { callee, msgValue, data, revertData } = self; let _ = make_acc_non_empty(callee, ccx.ecx)?; @@ -67,7 +87,6 @@ impl Cheatcode for mockFunctionCall { } } -#[allow(clippy::ptr_arg)] // Not public API, doesn't matter fn mock_call( state: &mut Cheatcodes, callee: &Address, @@ -75,16 +94,30 @@ fn mock_call( value: Option<&U256>, rdata: &Bytes, ret_type: InstructionResult, +) { + mock_calls(state, callee, cdata, value, std::slice::from_ref(rdata), ret_type) +} + +fn mock_calls( + state: &mut Cheatcodes, + callee: &Address, + cdata: &Bytes, + value: Option<&U256>, + rdata_vec: &[Bytes], + ret_type: InstructionResult, ) { state.mocked_calls.entry(*callee).or_default().insert( MockCallDataContext { calldata: Bytes::copy_from_slice(cdata), value: value.copied() }, - MockCallReturnData { ret_type, data: Bytes::copy_from_slice(rdata) }, + rdata_vec + .iter() + .map(|rdata| MockCallReturnData { ret_type, data: rdata.clone() }) + .collect::>(), ); } // Etches a single byte onto the account if it is empty to circumvent the `extcodesize` // check Solidity might perform. -fn make_acc_non_empty(callee: &Address, ecx: &mut InnerEvmContext) -> Result { +fn make_acc_non_empty(callee: &Address, ecx: InnerEcx) -> Result { let acc = ecx.load_account(*callee)?; let empty_bytecode = acc.info.code.as_ref().map_or(true, Bytecode::is_empty); diff --git a/crates/cheatcodes/src/evm/prank.rs b/crates/cheatcodes/src/evm/prank.rs index fe5418b31..a310e28e5 100644 --- a/crates/cheatcodes/src/evm/prank.rs +++ b/crates/cheatcodes/src/evm/prank.rs @@ -1,4 +1,4 @@ -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Result, Vm::*}; +use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; use alloy_primitives::Address; /// Prank information. @@ -45,28 +45,28 @@ impl Prank { } impl Cheatcode for prank_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { msgSender } = self; prank(ccx, msgSender, None, true) } } impl Cheatcode for startPrank_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { msgSender } = self; prank(ccx, msgSender, None, false) } } impl Cheatcode for prank_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { msgSender, txOrigin } = self; prank(ccx, msgSender, Some(txOrigin), true) } } impl Cheatcode for startPrank_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { msgSender, txOrigin } = self; prank(ccx, msgSender, Some(txOrigin), false) } @@ -80,8 +80,8 @@ impl Cheatcode for stopPrankCall { } } -fn prank( - ccx: &mut CheatsCtxt, +fn prank( + ccx: &mut CheatsCtxt, new_caller: &Address, new_origin: Option<&Address>, single_call: bool, diff --git a/crates/cheatcodes/src/evm/record_debug_step.rs b/crates/cheatcodes/src/evm/record_debug_step.rs new file mode 100644 index 000000000..b9f0f89cb --- /dev/null +++ b/crates/cheatcodes/src/evm/record_debug_step.rs @@ -0,0 +1,144 @@ +use alloy_primitives::{Bytes, U256}; + +use foundry_evm_traces::CallTraceArena; +use revm::interpreter::{InstructionResult, OpCode}; + +use foundry_evm_core::buffer::{get_buffer_accesses, BufferKind}; +use revm_inspectors::tracing::types::{CallTraceStep, RecordedMemory, TraceMemberOrder}; +use spec::Vm::DebugStep; + +// Do a depth first traverse of the nodes and steps and return steps +// that are after `node_start_idx` +pub(crate) fn flatten_call_trace( + root: usize, + arena: &CallTraceArena, + node_start_idx: usize, +) -> Vec<&CallTraceStep> { + let mut steps = Vec::new(); + let mut record_started = false; + + // Start the recursion from the root node + recursive_flatten_call_trace(root, arena, node_start_idx, &mut record_started, &mut steps); + steps +} + +// Inner recursive function to process nodes. +// This implementation directly mutates `record_started` and `flatten_steps`. +// So the recursive call can change the `record_started` flag even for the parent +// unfinished processing, and append steps to the `flatten_steps` as the final result. +fn recursive_flatten_call_trace<'a>( + node_idx: usize, + arena: &'a CallTraceArena, + node_start_idx: usize, + record_started: &mut bool, + flatten_steps: &mut Vec<&'a CallTraceStep>, +) { + // Once node_idx exceeds node_start_idx, start recording steps + // for all the recursive processing. + if !*record_started && node_idx >= node_start_idx { + *record_started = true; + } + + let node = &arena.nodes()[node_idx]; + + for order in node.ordering.iter() { + match order { + TraceMemberOrder::Step(step_idx) => { + if *record_started { + let step = &node.trace.steps[*step_idx]; + flatten_steps.push(step); + } + } + TraceMemberOrder::Call(call_idx) => { + let child_node_idx = node.children[*call_idx]; + recursive_flatten_call_trace( + child_node_idx, + arena, + node_start_idx, + record_started, + flatten_steps, + ); + } + _ => {} + } + } +} + +// Function to convert CallTraceStep to DebugStep +pub(crate) fn convert_call_trace_to_debug_step(step: &CallTraceStep) -> DebugStep { + let opcode = step.op.get(); + let stack = get_stack_inputs_for_opcode(opcode, step.stack.as_ref()); + + let memory = get_memory_input_for_opcode(opcode, step.stack.as_ref(), step.memory.as_ref()); + + let is_out_of_gas = step.status == InstructionResult::OutOfGas || + step.status == InstructionResult::MemoryOOG || + step.status == InstructionResult::MemoryLimitOOG || + step.status == InstructionResult::PrecompileOOG || + step.status == InstructionResult::InvalidOperandOOG; + + DebugStep { + stack, + memoryInput: memory, + opcode: step.op.get(), + depth: step.depth, + isOutOfGas: is_out_of_gas, + contractAddr: step.contract, + } +} + +// The expected `stack` here is from the trace stack, where the top of the stack +// is the last value of the vector +fn get_memory_input_for_opcode( + opcode: u8, + stack: Option<&Vec>, + memory: Option<&RecordedMemory>, +) -> Bytes { + let mut memory_input = Bytes::new(); + let Some(stack_data) = stack else { return memory_input }; + let Some(memory_data) = memory else { return memory_input }; + + if let Some(accesses) = get_buffer_accesses(opcode, stack_data) { + if let Some((BufferKind::Memory, access)) = accesses.read { + memory_input = get_slice_from_memory(memory_data.as_bytes(), access.offset, access.len); + } + }; + + memory_input +} + +// The expected `stack` here is from the trace stack, where the top of the stack +// is the last value of the vector +fn get_stack_inputs_for_opcode(opcode: u8, stack: Option<&Vec>) -> Vec { + let mut inputs = Vec::new(); + + let Some(op) = OpCode::new(opcode) else { return inputs }; + let Some(stack_data) = stack else { return inputs }; + + let stack_input_size = op.inputs() as usize; + for i in 0..stack_input_size { + inputs.push(stack_data[stack_data.len() - 1 - i]); + } + inputs +} + +fn get_slice_from_memory(memory: &Bytes, start_index: usize, size: usize) -> Bytes { + let memory_len = memory.len(); + + let end_bound = start_index + size; + + // Return the bytes if data is within the range. + if start_index < memory_len && end_bound <= memory_len { + return memory.slice(start_index..end_bound); + } + + // Pad zero bytes if attempting to load memory partially out of range. + if start_index < memory_len && end_bound > memory_len { + let mut result = memory.slice(start_index..memory_len).to_vec(); + result.resize(size, 0u8); + return Bytes::from(result); + } + + // Return empty bytes with the size if not in range at all. + Bytes::from(vec![0u8; size]) +} diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index 1aa0c1ea8..b30a13085 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -4,17 +4,15 @@ use super::string::parse; use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; use alloy_dyn_abi::DynSolType; use alloy_json_abi::ContractObject; -use alloy_primitives::{hex, Bytes, U256}; +use alloy_primitives::{hex, map::Entry, Bytes, U256}; use alloy_sol_types::SolValue; use dialoguer::{Input, Password}; use foundry_common::fs; use foundry_config::fs_permissions::FsAccessKind; -use foundry_evm_core::backend::DatabaseExt; use foundry_zksync_compiler::ContractType; use revm::interpreter::CreateInputs; use semver::Version; use std::{ - collections::hash_map::Entry, io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::Command, @@ -251,6 +249,34 @@ impl Cheatcode for writeLineCall { } } +impl Cheatcode for getArtifactPathByCodeCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { code } = self; + let (artifact_id, _) = state + .config + .available_artifacts + .as_ref() + .and_then(|artifacts| artifacts.find_by_creation_code(code)) + .ok_or_else(|| fmt_err!("no matching artifact found"))?; + + Ok(artifact_id.path.to_string_lossy().abi_encode()) + } +} + +impl Cheatcode for getArtifactPathByDeployedCodeCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { deployedCode } = self; + let (artifact_id, _) = state + .config + .available_artifacts + .as_ref() + .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode)) + .ok_or_else(|| fmt_err!("no matching artifact found"))?; + + Ok(artifact_id.path.to_string_lossy().abi_encode()) + } +} + impl Cheatcode for getCodeCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { artifactPath: path } = self; @@ -266,11 +292,7 @@ impl Cheatcode for getDeployedCodeCall { } impl Cheatcode for deployCode_0Call { - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let Self { artifactPath: path } = self; let bytecode = get_artifact_code(ccx.state, path, false)?; let address = executor @@ -292,11 +314,7 @@ impl Cheatcode for deployCode_0Call { } impl Cheatcode for deployCode_1Call { - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let Self { artifactPath: path, constructorArgs } = self; let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec(); bytecode.extend_from_slice(constructorArgs); diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index e3ca3e1a8..d09139354 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -5,19 +5,23 @@ use crate::{ journaled_account, mapping::{self, MappingSlots}, prank::Prank, - DealRecord, + DealRecord, GasRecord, }, inspector::utils::CommonCreateInput, - script::{Broadcast, ScriptWallets}, + script::{Broadcast, Wallets}, test::{ assume::AssumeNoRevert, expect::{self, ExpectedEmit, ExpectedRevert, ExpectedRevertKind}, }, utils::IgnoredTraces, - CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result, Vm, - Vm::AccountAccess, + CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result, + Vm::{self, AccountAccess}, +}; +use alloy_primitives::{ + hex, keccak256, + map::{AddressHashMap, HashMap}, + Address, Bytes, Log, TxKind, B256, U256, }; -use alloy_primitives::{hex, keccak256, Address, Bytes, Log, TxKind, B256, U256}; use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; use alloy_sol_types::{SolCall, SolInterface, SolValue}; use foundry_cheatcodes_common::{ @@ -38,6 +42,8 @@ use foundry_evm_core::{ utils::new_evm_with_existing_context, InspectorExt, }; +use foundry_evm_traces::TracingInspectorConfig; +use foundry_wallets::multi_wallet::MultiWallet; use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts}; use foundry_zksync_core::{ convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, @@ -46,7 +52,8 @@ use foundry_zksync_core::{ }; use foundry_zksync_inspectors::TraceCollector; use itertools::Itertools; -use rand::{rngs::StdRng, Rng, SeedableRng}; +use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner}; +use rand::Rng; use revm::{ interpreter::{ opcode as op, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, @@ -59,10 +66,9 @@ use revm::{ }, EvmContext, InnerEvmContext, Inspector, }; -use rustc_hash::FxHashMap; use serde_json::Value; use std::{ - collections::{BTreeMap, HashMap, HashSet, VecDeque}, + collections::{BTreeMap, HashSet, VecDeque}, fs::File, io::BufReader, ops::Range, @@ -80,6 +86,9 @@ use zksync_web3_rs::eip712::PaymasterParams; mod utils; +pub type Ecx<'a, 'b, 'c> = &'a mut EvmContext<&'b mut (dyn DatabaseExt + 'c)>; +pub type InnerEcx<'a, 'b, 'c> = &'a mut InnerEvmContext<&'b mut (dyn DatabaseExt + 'c)>; + /// Helper trait for obtaining complete [revm::Inspector] instance from mutable reference to /// [Cheatcodes]. /// @@ -88,71 +97,30 @@ mod utils; pub trait CheatcodesExecutor { /// Core trait method accepting mutable reference to [Cheatcodes] and returning /// [revm::Inspector]. - fn get_inspector<'a, DB: DatabaseExt>( - &'a mut self, - cheats: &'a mut Cheatcodes, - ) -> impl InspectorExt + 'a; - - /// Constructs [revm::Evm] and runs a given closure with it. - fn with_evm( - &mut self, - ccx: &mut CheatsCtxt, - f: F, - ) -> Result> - where - F: for<'a, 'b> FnOnce( - &mut revm::Evm< - '_, - &'b mut dyn InspectorExt<&'a mut dyn DatabaseExt>, - &'a mut dyn DatabaseExt, - >, - ) -> Result>, - { - let mut inspector = self.get_inspector(ccx.state); - let error = std::mem::replace(&mut ccx.ecx.error, Ok(())); - let l1_block_info = std::mem::take(&mut ccx.ecx.l1_block_info); - - let inner = revm::InnerEvmContext { - env: ccx.ecx.env.clone(), - journaled_state: std::mem::replace( - &mut ccx.ecx.journaled_state, - revm::JournaledState::new(Default::default(), Default::default()), - ), - db: &mut ccx.ecx.db as &mut dyn DatabaseExt, - error, - l1_block_info, - }; - - let mut evm = new_evm_with_existing_context(inner, &mut inspector as _); - - let res = f(&mut evm)?; - - ccx.ecx.journaled_state = evm.context.evm.inner.journaled_state; - ccx.ecx.env = evm.context.evm.inner.env; - ccx.ecx.l1_block_info = evm.context.evm.inner.l1_block_info; - ccx.ecx.error = evm.context.evm.inner.error; - - Ok(res) - } + fn get_inspector<'a>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box; /// Obtains [revm::Evm] instance and executes the given CREATE frame. - fn exec_create( + fn exec_create( &mut self, inputs: CreateInputs, - ccx: &mut CheatsCtxt, - ) -> Result> { - self.with_evm(ccx, |evm| { + ccx: &mut CheatsCtxt, + ) -> Result> { + with_evm(self, ccx, |evm| { evm.context.evm.inner.journaled_state.depth += 1; // Handle EOF bytecode - let first_frame_or_result = if evm.handler.cfg.spec_id.is_enabled_in(SpecId::PRAGUE_EOF) - && inputs.scheme == CreateScheme::Create && inputs.init_code.starts_with(&EOF_MAGIC_BYTES) + let first_frame_or_result = if evm.handler.cfg.spec_id.is_enabled_in(SpecId::PRAGUE_EOF) && + inputs.scheme == CreateScheme::Create && + inputs.init_code.starts_with(&EOF_MAGIC_BYTES) { evm.handler.execution().eofcreate( &mut evm.context, - Box::new(EOFCreateInputs::new(inputs.caller, inputs.value, inputs.gas_limit, EOFCreateKind::Tx { - initdata: inputs.init_code, - })), + Box::new(EOFCreateInputs::new( + inputs.caller, + inputs.value, + inputs.gas_limit, + EOFCreateKind::Tx { initdata: inputs.init_code }, + )), )? } else { evm.handler.execution().create(&mut evm.context, Box::new(inputs))? @@ -176,8 +144,8 @@ pub trait CheatcodesExecutor { }) } - fn console_log(&mut self, ccx: &mut CheatsCtxt, message: String) { - self.get_inspector::(ccx.state).console_log(message); + fn console_log(&mut self, ccx: &mut CheatsCtxt, message: String) { + self.get_inspector(ccx.state).console_log(message); } /// Returns a mutable reference to the tracing inspector if it is available. @@ -185,27 +153,90 @@ pub trait CheatcodesExecutor { None } - fn trace_zksync( - &mut self, - ccx_state: &mut Cheatcodes, - ecx: &mut EvmContext, - call_traces: Vec, - ) { - self.get_inspector::(ccx_state).trace_zksync(ecx, call_traces); + fn trace_zksync(&mut self, ccx_state: &mut Cheatcodes, ecx: Ecx, call_traces: Vec) { + let mut inspector = self.get_inspector(ccx_state); + + // We recreate the EvmContext here to satisfy the lifetime parameters as 'static, with + // regards to the inspector's lifetime. + let mut ecx_inner = EvmContext { + inner: InnerEvmContext { + env: std::mem::take(&mut ecx.env), + journaled_state: std::mem::replace( + &mut ecx.journaled_state, + revm::JournaledState::new(Default::default(), Default::default()), + ), + error: std::mem::replace(&mut ecx.error, Ok(())), + l1_block_info: std::mem::take(&mut ecx.l1_block_info), + db: &mut ecx.db as &mut dyn DatabaseExt, + }, + precompiles: Default::default(), + }; + inspector.trace_zksync(&mut ecx_inner, call_traces); + + // re-apply the modified fields to the original ecx. + let env = std::mem::take(&mut ecx_inner.env); + let journaled_state = std::mem::replace( + &mut ecx_inner.journaled_state, + revm::JournaledState::new(Default::default(), Default::default()), + ); + let error = std::mem::replace(&mut ecx_inner.error, Ok(())); + let l1_block_info = std::mem::take(&mut ecx_inner.l1_block_info); + drop(ecx_inner); + + ecx.env = env; + ecx.journaled_state = journaled_state; + ecx.error = error; + ecx.l1_block_info = l1_block_info; } } +/// Constructs [revm::Evm] and runs a given closure with it. +fn with_evm( + executor: &mut E, + ccx: &mut CheatsCtxt, + f: F, +) -> Result> +where + E: CheatcodesExecutor + ?Sized, + F: for<'a, 'b> FnOnce( + &mut revm::Evm<'_, &'b mut dyn InspectorExt, &'a mut dyn DatabaseExt>, + ) -> Result>, +{ + let mut inspector = executor.get_inspector(ccx.state); + let error = std::mem::replace(&mut ccx.ecx.error, Ok(())); + let l1_block_info = std::mem::take(&mut ccx.ecx.l1_block_info); + + let inner = revm::InnerEvmContext { + env: ccx.ecx.env.clone(), + journaled_state: std::mem::replace( + &mut ccx.ecx.journaled_state, + revm::JournaledState::new(Default::default(), Default::default()), + ), + db: &mut ccx.ecx.db as &mut dyn DatabaseExt, + error, + l1_block_info, + }; + + let mut evm = new_evm_with_existing_context(inner, &mut *inspector); + + let res = f(&mut evm)?; + + ccx.ecx.journaled_state = evm.context.evm.inner.journaled_state; + ccx.ecx.env = evm.context.evm.inner.env; + ccx.ecx.l1_block_info = evm.context.evm.inner.l1_block_info; + ccx.ecx.error = evm.context.evm.inner.error; + + Ok(res) +} + /// Basic implementation of [CheatcodesExecutor] that simply returns the [Cheatcodes] instance as an /// inspector. #[derive(Debug, Default, Clone, Copy)] struct TransparentCheatcodesExecutor; impl CheatcodesExecutor for TransparentCheatcodesExecutor { - fn get_inspector<'a, DB: DatabaseExt>( - &'a mut self, - cheats: &'a mut Cheatcodes, - ) -> impl InspectorExt + 'a { - cheats + fn get_inspector<'a>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box { + Box::new(cheats) } } @@ -251,25 +282,53 @@ pub struct BroadcastableTransaction { pub zk_tx: Option, } +#[derive(Clone, Debug, Copy)] +pub struct RecordDebugStepInfo { + /// The debug trace node index when the recording starts. + pub start_node_idx: usize, + /// The original tracer config when the recording starts. + pub original_tracer_config: TracingInspectorConfig, +} + /// Holds gas metering state. #[derive(Clone, Debug, Default)] pub struct GasMetering { /// True if gas metering is paused. pub paused: bool, - /// True if gas metering was resumed or reseted during the test. + /// True if gas metering was resumed or reset during the test. /// Used to reconcile gas when frame ends (if spent less than refunded). pub touched: bool, /// True if gas metering should be reset to frame limit. pub reset: bool, - /// Stores frames paused gas. + /// Stores paused gas frames. pub paused_frames: Vec, + /// The group and name of the active snapshot. + pub active_gas_snapshot: Option<(String, String)>, + /// Cache of the amount of gas used in previous call. /// This is used by the `lastCallGas` cheatcode. pub last_call_gas: Option, + + /// True if gas recording is enabled. + pub recording: bool, + /// The gas used in the last frame. + pub last_gas_used: u64, + /// Gas records for the active snapshots. + pub gas_records: Vec, } impl GasMetering { + /// Start the gas recording. + pub fn start(&mut self) { + self.recording = true; + } + + /// Stop the gas recording. + pub fn stop(&mut self) { + self.recording = false; + } + /// Resume paused gas metering. pub fn resume(&mut self) { if self.paused { @@ -315,13 +374,7 @@ impl ArbitraryStorage { /// Saves arbitrary storage value for a given address: /// - store value in changed values cache. /// - update account's storage with given value. - pub fn save( - &mut self, - ecx: &mut InnerEvmContext, - address: Address, - slot: U256, - data: U256, - ) { + pub fn save(&mut self, ecx: InnerEcx, address: Address, slot: U256, data: U256) { self.values.get_mut(&address).expect("missing arbitrary address entry").insert(slot, data); if let Ok(mut account) = ecx.load_account(address) { account.storage.insert(slot, EvmStorageSlot::new(data)); @@ -333,13 +386,7 @@ impl ArbitraryStorage { /// existing value. /// - if no value was yet generated for given slot, then save new value in cache and update both /// source and target storages. - pub fn copy( - &mut self, - ecx: &mut InnerEvmContext, - target: Address, - slot: U256, - new_value: U256, - ) -> U256 { + pub fn copy(&mut self, ecx: InnerEcx, target: Address, slot: U256, new_value: U256) -> U256 { let source = self.copies.get(&target).expect("missing arbitrary copy target entry"); let storage_cache = self.values.get_mut(source).expect("missing arbitrary source storage"); let value = match storage_cache.get(&slot) { @@ -396,7 +443,7 @@ pub struct Cheatcodes { pub gas_price: Option, /// Address labels - pub labels: HashMap, + pub labels: AddressHashMap, /// Prank information pub prank: Option, @@ -420,12 +467,15 @@ pub struct Cheatcodes { /// merged into the previous vector. pub recorded_account_diffs_stack: Option>>, + /// The information of the debug step recording. + pub record_debug_steps_info: Option, + /// Recorded logs pub recorded_logs: Option>, /// Mocked calls // **Note**: inner must a BTreeMap because of special `Ord` impl for `MockCallDataContext` - pub mocked_calls: HashMap>, + pub mocked_calls: HashMap>>, /// Mocked functions. Maps target address to be mocked to pair of (calldata, mock address). pub mocked_functions: HashMap>, @@ -436,7 +486,7 @@ pub struct Cheatcodes { pub expected_emits: VecDeque, /// Map of context depths to memory offset ranges that may be written to within the call depth. - pub allowed_mem_writes: FxHashMap>>, + pub allowed_mem_writes: HashMap>>, /// Current broadcasting information pub broadcast: Option, @@ -464,8 +514,12 @@ pub struct Cheatcodes { /// Gas metering state. pub gas_metering: GasMetering, + /// Contains gas snapshots made over the course of a test suite. + // **Note**: both must a BTreeMap to ensure the order of the keys is deterministic. + pub gas_snapshots: BTreeMap>, + /// Mapping slots. - pub mapping_slots: Option>, + pub mapping_slots: Option>, /// The current program counter. pub pc: usize, @@ -473,8 +527,9 @@ pub struct Cheatcodes { /// `char -> (address, pc)` pub breakpoints: Breakpoints, - /// Optional RNG algorithm. - rng: Option, + /// Optional cheatcodes `TestRunner`. Used for generating random values from uint and int + /// strategies. + test_runner: Option, /// Ignored traces. pub ignored_traces: IgnoredTraces, @@ -482,6 +537,11 @@ pub struct Cheatcodes { /// Addresses with arbitrary storage. pub arbitrary_storage: Option, + /// Deprecated cheatcodes mapped to the reason. Used to report warnings on test results. + pub deprecated: HashMap<&'static str, Option<&'static str>>, + /// Unlocked wallets used in scripts and testing of scripts. + pub wallets: Option, + /// Use ZK-VM to execute CALLs and CREATEs. pub use_zk_vm: bool, @@ -584,6 +644,7 @@ impl Cheatcodes { accesses: Default::default(), recorded_account_diffs_stack: Default::default(), recorded_logs: Default::default(), + record_debug_steps_info: Default::default(), mocked_calls: Default::default(), mocked_functions: Default::default(), expected_calls: Default::default(), @@ -595,12 +656,15 @@ impl Cheatcodes { serialized_jsons: Default::default(), eth_deals: Default::default(), gas_metering: Default::default(), + gas_snapshots: Default::default(), mapping_slots: Default::default(), pc: Default::default(), breakpoints: Default::default(), - rng: Default::default(), + test_runner: Default::default(), ignored_traces: Default::default(), arbitrary_storage: Default::default(), + deprecated: Default::default(), + wallets: Default::default(), use_zk_vm: Default::default(), skip_zk_vm: Default::default(), skip_zk_vm_addresses: Default::default(), @@ -611,17 +675,22 @@ impl Cheatcodes { } } - /// Returns the configured script wallets. - pub fn script_wallets(&self) -> Option<&ScriptWallets> { - self.config.script_wallets.as_ref() + /// Returns the configured wallets if available, else creates a new instance. + pub fn wallets(&mut self) -> &Wallets { + self.wallets.get_or_insert(Wallets::new(MultiWallet::default(), None)) + } + + /// Sets the unlocked wallets. + pub fn set_wallets(&mut self, wallets: Wallets) { + self.wallets = Some(wallets); } /// Decodes the input data and applies the cheatcode. - fn apply_cheatcode( + fn apply_cheatcode( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, call: &CallInputs, - executor: &mut E, + executor: &mut dyn CheatcodesExecutor, ) -> Result { // decode the cheatcode call let decoded = Vm::VmCalls::abi_decode(&call.input, false).map_err(|e| { @@ -659,12 +728,7 @@ impl Cheatcodes { /// /// There may be cheatcodes in the constructor of the new contract, in order to allow them /// automatically we need to determine the new address. - fn allow_cheatcodes_on_create( - &self, - ecx: &mut InnerEvmContext, - caller: Address, - created_address: Address, - ) { + fn allow_cheatcodes_on_create(&self, ecx: InnerEcx, caller: Address, created_address: Address) { if ecx.journaled_state.depth <= 1 || ecx.db.has_cheatcode_access(&caller) { ecx.db.allow_cheatcode_access(created_address); } @@ -674,7 +738,7 @@ impl Cheatcodes { /// /// Cleanup any previously applied cheatcodes that altered the state in such a way that revm's /// revert would run into issues. - pub fn on_revert(&mut self, ecx: &mut EvmContext) { + pub fn on_revert(&mut self, ecx: Ecx) { trace!(deals=?self.eth_deals.len(), "rolling back deals"); // Delay revert clean up until expected revert is handled, if set. @@ -702,11 +766,7 @@ impl Cheatcodes { /// Additionally: /// * Translates block information /// * Translates all persisted addresses - pub fn select_fork_vm( - &mut self, - data: &mut InnerEvmContext, - fork_id: LocalForkId, - ) { + pub fn select_fork_vm(&mut self, data: InnerEcx, fork_id: LocalForkId) { let fork_info = data.db.get_fork_info(fork_id).expect("failed getting fork info"); if fork_info.fork_type.is_evm() { self.select_evm(data) @@ -717,7 +777,7 @@ impl Cheatcodes { /// Switch to EVM and translate block info, balances, nonces and deployed codes for persistent /// accounts - pub fn select_evm(&mut self, data: &mut InnerEvmContext) { + pub fn select_evm(&mut self, data: InnerEcx) { if !self.use_zk_vm { tracing::info!("already in EVM"); return @@ -789,11 +849,7 @@ impl Cheatcodes { /// Switch to ZK-VM and translate block info, balances, nonces and deployed codes for persistent /// accounts - pub fn select_zk_vm( - &mut self, - data: &mut InnerEvmContext, - new_env: Option<&Env>, - ) { + pub fn select_zk_vm(&mut self, data: InnerEcx, new_env: Option<&Env>) { if self.use_zk_vm { tracing::info!("already in ZK-VM"); return @@ -896,15 +952,14 @@ impl Cheatcodes { } // common create functionality for both legacy and EOF. - fn create_common( + fn create_common( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, mut input: Input, executor: &mut impl CheatcodesExecutor, ) -> Option where - DB: DatabaseExt, - Input: CommonCreateInput, + Input: CommonCreateInput, { let ecx_inner = &mut ecx.inner; let gas = Gas::new(input.gas_limit()); @@ -1033,11 +1088,7 @@ impl Cheatcodes { value: Some(input.value()), input: TransactionInput::new(call_init_code), nonce: Some(nonce), - gas: if is_fixed_gas_limit { - Some(input.gas_limit() as u128) - } else { - None - }, + gas: if is_fixed_gas_limit { Some(input.gas_limit()) } else { None }, ..Default::default() } .into(), @@ -1088,15 +1139,14 @@ impl Cheatcodes { /// Try handling the `CREATE` within zkEVM. /// If `Some` is returned then the result must be returned immediately, else the call must be /// handled in EVM. - fn try_create_in_zk( + fn try_create_in_zk( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, input: Input, executor: &mut impl CheatcodesExecutor, ) -> Option where - DB: DatabaseExt, - Input: CommonCreateInput, + Input: CommonCreateInput, { if self.skip_zk_vm { self.skip_zk_vm = false; // handled the skip, reset flag @@ -1255,14 +1305,8 @@ impl Cheatcodes { } // common create_end functionality for both legacy and EOF. - fn create_end_common( - &mut self, - ecx: &mut EvmContext, - mut outcome: CreateOutcome, - ) -> CreateOutcome - where - DB: DatabaseExt, - { + fn create_end_common(&mut self, ecx: Ecx, mut outcome: CreateOutcome) -> CreateOutcome +where { let ecx = &mut ecx.inner; // Clean up pranks @@ -1377,18 +1421,18 @@ impl Cheatcodes { outcome } - pub fn create_with_executor( + pub fn create_with_executor( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, call: &mut CreateInputs, executor: &mut impl CheatcodesExecutor, ) -> Option { self.create_common(ecx, call, executor) } - pub fn call_with_executor( + pub fn call_with_executor( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, call: &mut CallInputs, executor: &mut impl CheatcodesExecutor, ) -> Option { @@ -1499,26 +1543,36 @@ impl Cheatcodes { } // Handle mocked calls - if let Some(mocks) = self.mocked_calls.get(&call.bytecode_address) { + if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) { let ctx = MockCallDataContext { calldata: call.input.clone(), value: call.transfer_value() }; - if let Some(return_data) = mocks.get(&ctx).or_else(|| { - mocks - .iter() + + if let Some(return_data_queue) = match mocks.get_mut(&ctx) { + Some(queue) => Some(queue), + None => mocks + .iter_mut() .find(|(mock, _)| { call.input.get(..mock.calldata.len()) == Some(&mock.calldata[..]) && mock.value.map_or(true, |value| Some(value) == call.transfer_value()) }) - .map(|(_, v)| v) - }) { - return Some(CallOutcome { - result: InterpreterResult { - result: return_data.ret_type, - output: return_data.data.clone(), - gas, - }, - memory_offset: call.return_memory_offset.clone(), - }); + .map(|(_, v)| v), + } { + if let Some(return_data) = if return_data_queue.len() == 1 { + // If the mocked calls stack has a single element in it, don't empty it + return_data_queue.front().map(|x| x.to_owned()) + } else { + // Else, we pop the front element + return_data_queue.pop_front() + } { + return Some(CallOutcome { + result: InterpreterResult { + result: return_data.ret_type, + output: return_data.data, + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }); + } } } @@ -1635,11 +1689,7 @@ impl Cheatcodes { value: call.transfer_value(), input: TransactionInput::new(call.input.clone()), nonce: Some(nonce), - gas: if is_fixed_gas_limit { - Some(call.gas_limit as u128) - } else { - None - }, + gas: if is_fixed_gas_limit { Some(call.gas_limit) } else { None }, ..Default::default() } .into(), @@ -1727,16 +1777,13 @@ impl Cheatcodes { /// Try handling the `CALL` within zkEVM. /// If `Some` is returned then the result must be returned immediately, else the call must be /// handled in EVM. - fn try_call_in_zk( + fn try_call_in_zk( &mut self, factory_deps: Vec>, - ecx: &mut EvmContext, + ecx: Ecx, call: &mut CallInputs, executor: &mut impl CheatcodesExecutor, - ) -> Option - where - DB: DatabaseExt, - { + ) -> Option { // also skip if the target was created during a zkEVM skip self.skip_zk_vm = self.skip_zk_vm || self.skip_zk_vm_addresses.contains(&call.target_address); @@ -1867,9 +1914,16 @@ impl Cheatcodes { } pub fn rng(&mut self) -> &mut impl Rng { - self.rng.get_or_insert_with(|| match self.config.seed { - Some(seed) => StdRng::from_seed(seed.to_be_bytes::<32>()), - None => StdRng::from_entropy(), + self.test_runner().rng() + } + + pub fn test_runner(&mut self) -> &mut TestRunner { + self.test_runner.get_or_insert_with(|| match self.config.seed { + Some(seed) => TestRunner::new_with_rng( + proptest::test_runner::Config::default(), + TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>()), + ), + None => TestRunner::new(proptest::test_runner::Config::default()), }) } @@ -1896,9 +1950,9 @@ impl Cheatcodes { } } -impl Inspector for Cheatcodes { +impl Inspector<&mut dyn DatabaseExt> for Cheatcodes { #[inline] - fn initialize_interp(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn initialize_interp(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { // When the first interpreter is initialized we've circumvented the balance and gas checks, // so we apply our actual block data with the correct fees and all. if let Some(block) = self.block.take() { @@ -1920,7 +1974,7 @@ impl Inspector for Cheatcodes { } #[inline] - fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn step(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { self.pc = interpreter.program_counter(); // `pauseGasMetering`: pause / resume interpreter gas. @@ -1952,10 +2006,15 @@ impl Inspector for Cheatcodes { if let Some(mapping_slots) = &mut self.mapping_slots { mapping::step(mapping_slots, interpreter); } + + // `snapshotGas*`: take a snapshot of the current gas. + if self.gas_metering.recording { + self.meter_gas_record(interpreter, ecx); + } } #[inline] - fn step_end(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn step_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { // override address(x).balance retrieval to make it consistent between EraVM and EVM if self.use_zk_vm { let address = match interpreter.current_opcode() { @@ -1999,7 +2058,7 @@ impl Inspector for Cheatcodes { } } - fn log(&mut self, interpreter: &mut Interpreter, _ecx: &mut EvmContext, log: &Log) { + fn log(&mut self, interpreter: &mut Interpreter, _ecx: Ecx, log: &Log) { if !self.expected_emits.is_empty() { expect::handle_expect_emit(self, log, interpreter); } @@ -2014,22 +2073,17 @@ impl Inspector for Cheatcodes { } } - fn call(&mut self, ecx: &mut EvmContext, inputs: &mut CallInputs) -> Option { + fn call(&mut self, ecx: Ecx, inputs: &mut CallInputs) -> Option { Self::call_with_executor(self, ecx, inputs, &mut TransparentCheatcodesExecutor) } - fn call_end( - &mut self, - ecx: &mut EvmContext, - call: &CallInputs, - mut outcome: CallOutcome, - ) -> CallOutcome { + fn call_end(&mut self, ecx: Ecx, call: &CallInputs, mut outcome: CallOutcome) -> CallOutcome { let ecx = &mut ecx.inner; let cheatcode_call = call.target_address == CHEATCODE_ADDRESS || call.target_address == HARDHAT_CONSOLE_ADDRESS; // Clean up pranks/broadcasts if it's not a cheatcode call end. We shouldn't do - // it for cheatcode calls because they are not appplied for cheatcodes in the `call` hook. + // it for cheatcode calls because they are not applied for cheatcodes in the `call` hook. // This should be placed before the revert handling, because we might exit early there if !cheatcode_call { // Clean up pranks @@ -2319,34 +2373,26 @@ impl Inspector for Cheatcodes { outcome } - fn create( - &mut self, - ecx: &mut EvmContext, - call: &mut CreateInputs, - ) -> Option { + fn create(&mut self, ecx: Ecx, call: &mut CreateInputs) -> Option { self.create_common(ecx, call, &mut TransparentCheatcodesExecutor) } fn create_end( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, _call: &CreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { self.create_end_common(ecx, outcome) } - fn eofcreate( - &mut self, - ecx: &mut EvmContext, - call: &mut EOFCreateInputs, - ) -> Option { + fn eofcreate(&mut self, ecx: Ecx, call: &mut EOFCreateInputs) -> Option { self.create_common(ecx, call, &mut TransparentCheatcodesExecutor) } fn eofcreate_end( &mut self, - ecx: &mut EvmContext, + ecx: Ecx, _call: &EOFCreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { @@ -2354,12 +2400,8 @@ impl Inspector for Cheatcodes { } } -impl InspectorExt for Cheatcodes { - fn should_use_create2_factory( - &mut self, - ecx: &mut EvmContext, - inputs: &mut CreateInputs, - ) -> bool { +impl InspectorExt for Cheatcodes { + fn should_use_create2_factory(&mut self, ecx: Ecx, inputs: &mut CreateInputs) -> bool { if let CreateScheme::Create2 { .. } = inputs.scheme { let target_depth = if let Some(prank) = &self.prank { prank.depth @@ -2389,6 +2431,27 @@ impl Cheatcodes { } } + #[cold] + fn meter_gas_record(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { + if matches!(interpreter.instruction_result, InstructionResult::Continue) { + self.gas_metering.gas_records.iter_mut().for_each(|record| { + if ecx.journaled_state.depth() == record.depth { + // Skip the first opcode of the first call frame as it includes the gas cost of + // creating the snapshot. + if self.gas_metering.last_gas_used != 0 { + let gas_diff = + interpreter.gas.spent().saturating_sub(self.gas_metering.last_gas_used); + record.gas_used = record.gas_used.saturating_add(gas_diff); + } + + // Update `last_gas_used` to the current spent gas for the next iteration to + // compare against. + self.gas_metering.last_gas_used = interpreter.gas.spent(); + } + }); + } + } + #[cold] fn meter_gas_end(&mut self, interpreter: &mut Interpreter) { // Remove recorded gas if we exit frame. @@ -2425,11 +2488,7 @@ impl Cheatcodes { /// cache) from mapped source address to the target address. /// - generates arbitrary value and saves it in target address storage. #[cold] - fn arbitrary_storage_end( - &mut self, - interpreter: &mut Interpreter, - ecx: &mut EvmContext, - ) { + fn arbitrary_storage_end(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { let (key, target_address) = if interpreter.current_opcode() == op::SLOAD { (try_or_return!(interpreter.stack().peek(0)), interpreter.contract().target_address) } else { @@ -2479,11 +2538,7 @@ impl Cheatcodes { } #[cold] - fn record_state_diffs( - &mut self, - interpreter: &mut Interpreter, - ecx: &mut EvmContext, - ) { + fn record_state_diffs(&mut self, interpreter: &mut Interpreter, ecx: Ecx) { let Some(account_accesses) = &mut self.recorded_account_diffs_stack else { return }; match interpreter.current_opcode() { op::SELFDESTRUCT => { @@ -2830,10 +2885,7 @@ fn disallowed_mem_write( // Determines if the gas limit on a given call was manually set in the script and should therefore // not be overwritten by later estimations -fn check_if_fixed_gas_limit( - ecx: &InnerEvmContext, - call_gas_limit: u64, -) -> bool { +fn check_if_fixed_gas_limit(ecx: InnerEcx, call_gas_limit: u64) -> bool { // If the gas limit was not set in the source code it is set to the estimated gas left at the // time of the call, which should be rather close to configured gas limit. // TODO: Find a way to reliably make this determination. @@ -2903,59 +2955,27 @@ fn append_storage_access( } /// Dispatches the cheatcode call to the appropriate function. -fn apply_dispatch( +fn apply_dispatch( calls: &Vm::VmCalls, - ccx: &mut CheatsCtxt, - executor: &mut E, + ccx: &mut CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, ) -> Result { - macro_rules! dispatch { - ($($variant:ident),*) => { - match calls { - $(Vm::VmCalls::$variant(cheat) => crate::Cheatcode::apply_full(cheat, ccx, executor),)* - } - }; - } + let cheat = calls_as_dyn_cheatcode(calls); - let mut dyn_cheat = DynCheatCache::new(calls); - let _guard = trace_span_and_call(&mut dyn_cheat); - let mut result = vm_calls!(dispatch); - fill_and_trace_return(&mut dyn_cheat, &mut result); - result -} - -/// Helper function to check if frame execution will exit. -fn will_exit(ir: InstructionResult) -> bool { - !matches!(ir, InstructionResult::Continue | InstructionResult::CallOrCreate) -} + let _guard = debug_span!(target: "cheatcodes", "apply", id = %cheat.id()).entered(); + trace!(target: "cheatcodes", cheat = ?cheat.as_debug(), "applying"); -// Caches the result of `calls_as_dyn_cheatcode`. -// TODO: Remove this once Cheatcode is object-safe, as caching would not be necessary anymore. -struct DynCheatCache<'a> { - calls: &'a Vm::VmCalls, - slot: Option<&'a dyn DynCheatcode>, -} - -impl<'a> DynCheatCache<'a> { - fn new(calls: &'a Vm::VmCalls) -> Self { - Self { calls, slot: None } + if let spec::Status::Deprecated(replacement) = *cheat.status() { + ccx.state.deprecated.insert(cheat.signature(), replacement); } - fn get(&mut self) -> &dyn DynCheatcode { - *self.slot.get_or_insert_with(|| calls_as_dyn_cheatcode(self.calls)) - } -} + // Apply the cheatcode. + let mut result = cheat.dyn_apply(ccx, executor); -fn trace_span_and_call(dyn_cheat: &mut DynCheatCache) -> tracing::span::EnteredSpan { - let span = debug_span!(target: "cheatcodes", "apply", id = %dyn_cheat.get().id()); - let entered = span.entered(); - trace!(target: "cheatcodes", cheat = ?dyn_cheat.get().as_debug(), "applying"); - entered -} - -fn fill_and_trace_return(dyn_cheat: &mut DynCheatCache, result: &mut Result) { - if let Err(e) = result { + // Format the error message to include the cheatcode name. + if let Err(e) = &mut result { if e.is_str() { - let name = dyn_cheat.get().name(); + let name = cheat.name(); // Skip showing the cheatcode name for: // - assertions: too verbose, and can already be inferred from the error message // - `rpcUrl`: forge-std relies on it in `getChainWithUpdatedRpcUrl` @@ -2964,16 +2984,18 @@ fn fill_and_trace_return(dyn_cheat: &mut DynCheatCache, result: &mut Result) { } } } + trace!( target: "cheatcodes", - return = %match result { + return = %match &result { Ok(b) => hex::encode(b), Err(e) => e.to_string(), } ); + + result } -#[cold] fn calls_as_dyn_cheatcode(calls: &Vm::VmCalls) -> &dyn DynCheatcode { macro_rules! as_dyn { ($($variant:ident),*) => { @@ -2984,3 +3006,8 @@ fn calls_as_dyn_cheatcode(calls: &Vm::VmCalls) -> &dyn DynCheatcode { } vm_calls!(as_dyn) } + +/// Helper function to check if frame execution will exit. +fn will_exit(ir: InstructionResult) -> bool { + !matches!(ir, InstructionResult::Continue | InstructionResult::CallOrCreate) +} diff --git a/crates/cheatcodes/src/inspector/utils.rs b/crates/cheatcodes/src/inspector/utils.rs index dfccd4b55..a0d7820aa 100644 --- a/crates/cheatcodes/src/inspector/utils.rs +++ b/crates/cheatcodes/src/inspector/utils.rs @@ -1,13 +1,10 @@ +use super::InnerEcx; use crate::inspector::Cheatcodes; use alloy_primitives::{Address, Bytes, U256}; -use foundry_evm_core::backend::DatabaseExt; -use revm::{ - interpreter::{CreateInputs, CreateScheme, EOFCreateInputs, EOFCreateKind}, - InnerEvmContext, -}; +use revm::interpreter::{CreateInputs, CreateScheme, EOFCreateInputs, EOFCreateKind}; /// Common behaviour of legacy and EOF create inputs. -pub(crate) trait CommonCreateInput { +pub(crate) trait CommonCreateInput { fn caller(&self) -> Address; fn gas_limit(&self) -> u64; fn value(&self) -> U256; @@ -15,15 +12,11 @@ pub(crate) trait CommonCreateInput { fn scheme(&self) -> Option; fn set_caller(&mut self, caller: Address); fn log_debug(&self, cheatcode: &mut Cheatcodes, scheme: &CreateScheme); - fn allow_cheatcodes( - &self, - cheatcodes: &mut Cheatcodes, - ecx: &mut InnerEvmContext, - ) -> Address; + fn allow_cheatcodes(&self, cheatcodes: &mut Cheatcodes, ecx: InnerEcx) -> Address; fn computed_created_address(&self) -> Option
; } -impl CommonCreateInput for &mut CreateInputs { +impl CommonCreateInput for &mut CreateInputs { fn caller(&self) -> Address { self.caller } @@ -49,11 +42,7 @@ impl CommonCreateInput for &mut CreateInputs { }; debug!(target: "cheatcodes", tx=?cheatcode.broadcastable_transactions.back().unwrap(), "broadcastable {kind}"); } - fn allow_cheatcodes( - &self, - cheatcodes: &mut Cheatcodes, - ecx: &mut InnerEvmContext, - ) -> Address { + fn allow_cheatcodes(&self, cheatcodes: &mut Cheatcodes, ecx: InnerEcx) -> Address { let old_nonce = ecx .journaled_state .state @@ -69,7 +58,7 @@ impl CommonCreateInput for &mut CreateInputs { } } -impl CommonCreateInput for &mut EOFCreateInputs { +impl CommonCreateInput for &mut EOFCreateInputs { fn caller(&self) -> Address { self.caller } @@ -94,13 +83,9 @@ impl CommonCreateInput for &mut EOFCreateInputs { fn log_debug(&self, cheatcode: &mut Cheatcodes, _scheme: &CreateScheme) { debug!(target: "cheatcodes", tx=?cheatcode.broadcastable_transactions.back().unwrap(), "broadcastable eofcreate"); } - fn allow_cheatcodes( - &self, - cheatcodes: &mut Cheatcodes, - ecx: &mut InnerEvmContext, - ) -> Address { + fn allow_cheatcodes(&self, cheatcodes: &mut Cheatcodes, ecx: InnerEcx) -> Address { let created_address = - <&mut EOFCreateInputs as CommonCreateInput>::computed_created_address(self) + <&mut EOFCreateInputs as CommonCreateInput>::computed_created_address(self) .unwrap_or_default(); cheatcodes.allow_cheatcodes_on_create(ecx, self.caller, created_address); created_address diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs index dad879e83..0908f247c 100644 --- a/crates/cheatcodes/src/json.rs +++ b/crates/cheatcodes/src/json.rs @@ -594,7 +594,7 @@ fn serialize_value_as_json(value: DynSolValue) -> Result { match value { DynSolValue::Bool(b) => Ok(Value::Bool(b)), DynSolValue::String(s) => { - // Strings are allowed to contain strigified JSON objects, so we try to parse it like + // Strings are allowed to contain stringified JSON objects, so we try to parse it like // one first. if let Ok(map) = serde_json::from_str(&s) { Ok(Value::Object(map)) @@ -654,7 +654,7 @@ fn serialize_json( } /// Resolves a [DynSolType] from user input. -fn resolve_type(type_description: &str) -> Result { +pub(super) fn resolve_type(type_description: &str) -> Result { if let Ok(ty) = DynSolType::parse(type_description) { return Ok(ty); }; diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index 30c3938e3..a5cc5da5b 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -14,6 +14,7 @@ extern crate tracing; use alloy_primitives::Address; use foundry_evm_core::backend::DatabaseExt; use revm::{ContextPrecompiles, InnerEvmContext}; +use spec::Status; pub use config::CheatsConfig; pub use error::{Error, ErrorKind, Result}; @@ -44,7 +45,7 @@ mod inspector; mod json; mod script; -pub use script::{ScriptWallets, ScriptWalletsInner}; +pub use script::{Wallets, WalletsInner}; mod string; @@ -68,7 +69,7 @@ pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode { /// /// Implement this function if you need access to the EVM data. #[inline(always)] - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { self.apply(ccx.state) } @@ -76,50 +77,71 @@ pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode { /// /// Implement this function if you need access to the executor. #[inline(always)] - fn apply_full( - &self, - ccx: &mut CheatsCtxt, - executor: &mut E, - ) -> Result { + fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { let _ = executor; self.apply_stateful(ccx) } } -pub(crate) trait DynCheatcode { - fn name(&self) -> &'static str; - fn id(&self) -> &'static str; +pub(crate) trait DynCheatcode: 'static { + fn cheatcode(&self) -> &'static spec::Cheatcode<'static>; + fn as_debug(&self) -> &dyn std::fmt::Debug; + + fn dyn_apply(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result; } impl DynCheatcode for T { - fn name(&self) -> &'static str { - T::CHEATCODE.func.signature.split('(').next().unwrap() - } - fn id(&self) -> &'static str { - T::CHEATCODE.func.id + #[inline] + fn cheatcode(&self) -> &'static spec::Cheatcode<'static> { + Self::CHEATCODE } + + #[inline] fn as_debug(&self) -> &dyn std::fmt::Debug { self } + + #[inline] + fn dyn_apply(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { + self.apply_full(ccx, executor) + } +} + +impl dyn DynCheatcode { + pub(crate) fn name(&self) -> &'static str { + self.cheatcode().func.signature.split('(').next().unwrap() + } + + pub(crate) fn id(&self) -> &'static str { + self.cheatcode().func.id + } + + pub(crate) fn signature(&self) -> &'static str { + self.cheatcode().func.signature + } + + pub(crate) fn status(&self) -> &Status<'static> { + &self.cheatcode().status + } } /// The cheatcode context, used in `Cheatcode`. -pub struct CheatsCtxt<'cheats, 'evm, DB: DatabaseExt> { +pub struct CheatsCtxt<'cheats, 'evm, 'db, 'db2> { /// The cheatcodes inspector state. pub(crate) state: &'cheats mut Cheatcodes, /// The EVM data. - pub(crate) ecx: &'evm mut InnerEvmContext, + pub(crate) ecx: &'evm mut InnerEvmContext<&'db mut (dyn DatabaseExt + 'db2)>, /// The precompiles context. - pub(crate) precompiles: &'evm mut ContextPrecompiles, + pub(crate) precompiles: &'evm mut ContextPrecompiles<&'db mut (dyn DatabaseExt + 'db2)>, /// The original `msg.sender`. pub(crate) caller: Address, /// Gas limit of the current cheatcode call. pub(crate) gas_limit: u64, } -impl<'cheats, 'evm, DB: DatabaseExt> std::ops::Deref for CheatsCtxt<'cheats, 'evm, DB> { - type Target = InnerEvmContext; +impl<'db, 'db2> std::ops::Deref for CheatsCtxt<'_, '_, 'db, 'db2> { + type Target = InnerEvmContext<&'db mut (dyn DatabaseExt + 'db2)>; #[inline(always)] fn deref(&self) -> &Self::Target { @@ -127,14 +149,14 @@ impl<'cheats, 'evm, DB: DatabaseExt> std::ops::Deref for CheatsCtxt<'cheats, 'ev } } -impl<'cheats, 'evm, DB: DatabaseExt> std::ops::DerefMut for CheatsCtxt<'cheats, 'evm, DB> { +impl std::ops::DerefMut for CheatsCtxt<'_, '_, '_, '_> { #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.ecx } } -impl<'cheats, 'evm, DB: DatabaseExt> CheatsCtxt<'cheats, 'evm, DB> { +impl CheatsCtxt<'_, '_, '_, '_> { #[inline] pub(crate) fn is_precompile(&self, address: &Address) -> bool { self.precompiles.contains(address) diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 82eef2354..29a804efd 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -1,56 +1,57 @@ //! Implementations of [`Scripting`](spec::Group::Scripting) cheatcodes. -use crate::{Cheatcode, CheatsCtxt, DatabaseExt, Result, Vm::*}; +use crate::{Cheatcode, CheatsCtxt, Result, Vm::*}; use alloy_primitives::{Address, B256, U256}; use alloy_signer_local::PrivateKeySigner; +use alloy_sol_types::SolValue; use foundry_wallets::{multi_wallet::MultiWallet, WalletSigner}; use parking_lot::Mutex; use std::sync::Arc; impl Cheatcode for broadcast_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; broadcast(ccx, None, true) } } impl Cheatcode for broadcast_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { signer } = self; broadcast(ccx, Some(signer), true) } } impl Cheatcode for broadcast_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { privateKey } = self; broadcast_key(ccx, privateKey, true) } } impl Cheatcode for startBroadcast_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; broadcast(ccx, None, false) } } impl Cheatcode for startBroadcast_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { signer } = self; broadcast(ccx, Some(signer), false) } } impl Cheatcode for startBroadcast_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { privateKey } = self; broadcast_key(ccx, privateKey, false) } } impl Cheatcode for stopBroadcastCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; let Some(broadcast) = ccx.state.broadcast.take() else { bail!("no broadcast in progress to stop"); @@ -60,6 +61,13 @@ impl Cheatcode for stopBroadcastCall { } } +impl Cheatcode for getWalletsCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let wallets = ccx.state.wallets().signers().unwrap_or_default(); + Ok(wallets.abi_encode()) + } +} + #[derive(Clone, Debug, Default)] pub struct Broadcast { /// Address of the transaction origin @@ -76,29 +84,29 @@ pub struct Broadcast { /// Contains context for wallet management. #[derive(Debug)] -pub struct ScriptWalletsInner { +pub struct WalletsInner { /// All signers in scope of the script. pub multi_wallet: MultiWallet, /// Optional signer provided as `--sender` flag. pub provided_sender: Option
, } -/// Clonable wrapper around [`ScriptWalletsInner`]. +/// Clonable wrapper around [`WalletsInner`]. #[derive(Debug, Clone)] -pub struct ScriptWallets { +pub struct Wallets { /// Inner data. - pub inner: Arc>, + pub inner: Arc>, } -impl ScriptWallets { +impl Wallets { #[allow(missing_docs)] pub fn new(multi_wallet: MultiWallet, provided_sender: Option
) -> Self { - Self { inner: Arc::new(Mutex::new(ScriptWalletsInner { multi_wallet, provided_sender })) } + Self { inner: Arc::new(Mutex::new(WalletsInner { multi_wallet, provided_sender })) } } - /// Consumes [ScriptWallets] and returns [MultiWallet]. + /// Consumes [Wallets] and returns [MultiWallet]. /// - /// Panics if [ScriptWallets] is still in use. + /// Panics if [Wallets] is still in use. pub fn into_multi_wallet(self) -> MultiWallet { Arc::into_inner(self.inner) .map(|m| m.into_inner().multi_wallet) @@ -120,14 +128,25 @@ impl ScriptWallets { pub fn signers(&self) -> Result> { Ok(self.inner.lock().multi_wallet.signers()?.keys().cloned().collect()) } + + /// Number of signers in the [MultiWallet]. + pub fn len(&self) -> usize { + let mut inner = self.inner.lock(); + let signers = inner.multi_wallet.signers(); + if signers.is_err() { + return 0; + } + signers.unwrap().len() + } + + /// Whether the [MultiWallet] is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } /// Sets up broadcasting from a script using `new_origin` as the sender. -fn broadcast( - ccx: &mut CheatsCtxt, - new_origin: Option<&Address>, - single_call: bool, -) -> Result { +fn broadcast(ccx: &mut CheatsCtxt, new_origin: Option<&Address>, single_call: bool) -> Result { ensure!( ccx.state.prank.is_none(), "you have an active prank; broadcasting and pranks are not compatible" @@ -137,16 +156,14 @@ fn broadcast( let mut new_origin = new_origin.cloned(); if new_origin.is_none() { - if let Some(script_wallets) = ccx.state.script_wallets() { - let mut script_wallets = script_wallets.inner.lock(); - if let Some(provided_sender) = script_wallets.provided_sender { - new_origin = Some(provided_sender); - } else { - let signers = script_wallets.multi_wallet.signers()?; - if signers.len() == 1 { - let address = signers.keys().next().unwrap(); - new_origin = Some(*address); - } + let mut wallets = ccx.state.wallets().inner.lock(); + if let Some(provided_sender) = wallets.provided_sender { + new_origin = Some(provided_sender); + } else { + let signers = wallets.multi_wallet.signers()?; + if signers.len() == 1 { + let address = signers.keys().next().unwrap(); + new_origin = Some(*address); } } } @@ -164,21 +181,16 @@ fn broadcast( } /// Sets up broadcasting from a script with the sender derived from `private_key`. -/// Adds this private key to `state`'s `script_wallets` vector to later be used for signing +/// Adds this private key to `state`'s `wallets` vector to later be used for signing /// if broadcast is successful. -fn broadcast_key( - ccx: &mut CheatsCtxt, - private_key: &U256, - single_call: bool, -) -> Result { +fn broadcast_key(ccx: &mut CheatsCtxt, private_key: &U256, single_call: bool) -> Result { let wallet = super::crypto::parse_wallet(private_key)?; let new_origin = wallet.address(); let result = broadcast(ccx, Some(&new_origin), single_call); if result.is_ok() { - if let Some(script_wallets) = ccx.state.script_wallets() { - script_wallets.add_local_signer(wallet); - } + let wallets = ccx.state.wallets(); + wallets.add_local_signer(wallet); } result } diff --git a/crates/cheatcodes/src/string.rs b/crates/cheatcodes/src/string.rs index e7435d541..a4c06eef6 100644 --- a/crates/cheatcodes/src/string.rs +++ b/crates/cheatcodes/src/string.rs @@ -144,6 +144,14 @@ impl Cheatcode for indexOfCall { } } +// contains +impl Cheatcode for containsCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { subject, search } = self; + Ok(subject.contains(search).abi_encode()) + } +} + pub(super) fn parse(s: &str, ty: &DynSolType) -> Result { parse_value(s, ty).map(|v| v.abi_encode()) } diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index b1574e797..c4b5f219c 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -1,21 +1,20 @@ //! Implementations of [`Testing`](spec::Group::Testing) cheatcodes. -use chrono::DateTime; -use std::env; - -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Result, Vm::*}; +use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; use alloy_primitives::Address; use alloy_sol_types::SolValue; +use chrono::DateTime; use foundry_evm_core::constants::MAGIC_SKIP; use foundry_zksync_compiler::DualCompiledContract; use foundry_zksync_core::ZkPaymasterData; +use std::env; pub(crate) mod assert; pub(crate) mod assume; pub(crate) mod expect; impl Cheatcode for zkVmCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { enable } = *self; if enable { @@ -29,7 +28,7 @@ impl Cheatcode for zkVmCall { } impl Cheatcode for zkVmSkipCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { ccx.state.skip_zk_vm = ccx.state.use_zk_vm; Ok(Default::default()) @@ -37,7 +36,7 @@ impl Cheatcode for zkVmSkipCall { } impl Cheatcode for zkUsePaymasterCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { paymaster_address, paymaster_input } = self; ccx.state.paymaster_params = Some(ZkPaymasterData { address: *paymaster_address, input: paymaster_input.clone() }); @@ -46,7 +45,7 @@ impl Cheatcode for zkUsePaymasterCall { } impl Cheatcode for zkUseFactoryDepCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { name } = self; info!("Adding factory dependency: {:?}", name); ccx.state.zk_use_factory_deps.push(name.clone()); @@ -55,7 +54,7 @@ impl Cheatcode for zkUseFactoryDepCall { } impl Cheatcode for zkRegisterContractCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { name, evmBytecodeHash, @@ -91,14 +90,14 @@ impl Cheatcode for zkRegisterContractCall { } impl Cheatcode for breakpoint_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { char } = self; breakpoint(ccx.state, &ccx.caller, char, true) } } impl Cheatcode for breakpoint_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { char, value } = self; breakpoint(ccx.state, &ccx.caller, char, *value) } @@ -149,14 +148,14 @@ impl Cheatcode for sleepCall { } impl Cheatcode for skip_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { skipTest } = *self; skip_1Call { skipTest, reason: String::new() }.apply_stateful(ccx) } } impl Cheatcode for skip_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { skipTest, reason } = self; if *skipTest { // Skip should not work if called deeper than at test level. diff --git a/crates/cheatcodes/src/test/assert.rs b/crates/cheatcodes/src/test/assert.rs index 4ab97c031..b4b6652ac 100644 --- a/crates/cheatcodes/src/test/assert.rs +++ b/crates/cheatcodes/src/test/assert.rs @@ -2,7 +2,7 @@ use crate::{CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; use alloy_primitives::{hex, I256, U256}; use foundry_evm_core::{ abi::{format_units_int, format_units_uint}, - backend::{DatabaseExt, GLOBAL_FAIL_SLOT}, + backend::GLOBAL_FAIL_SLOT, constants::CHEATCODE_ADDRESS, }; use itertools::Itertools; @@ -37,27 +37,27 @@ macro_rules! format_values { }; } -impl<'a, T: Display> ComparisonAssertionError<'a, T> { +impl ComparisonAssertionError<'_, T> { fn format_for_values(&self) -> String { format_values!(self, T::to_string) } } -impl<'a, T: Display> ComparisonAssertionError<'a, Vec> { +impl ComparisonAssertionError<'_, Vec> { fn format_for_arrays(&self) -> String { let formatter = |v: &Vec| format!("[{}]", v.iter().format(", ")); format_values!(self, formatter) } } -impl<'a> ComparisonAssertionError<'a, U256> { +impl ComparisonAssertionError<'_, U256> { fn format_with_decimals(&self, decimals: &U256) -> String { let formatter = |v: &U256| format_units_uint(v, decimals); format_values!(self, formatter) } } -impl<'a> ComparisonAssertionError<'a, I256> { +impl ComparisonAssertionError<'_, I256> { fn format_with_decimals(&self, decimals: &U256) -> String { let formatter = |v: &I256| format_units_int(v, decimals); format_values!(self, formatter) @@ -169,10 +169,10 @@ impl EqRelAssertionError { type ComparisonResult<'a, T> = Result, ComparisonAssertionError<'a, T>>; -fn handle_assertion_result( +fn handle_assertion_result( result: core::result::Result, ERR>, - ccx: &mut CheatsCtxt, - executor: &mut E, + ccx: &mut CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, error_formatter: impl Fn(&ERR) -> String, error_msg: Option<&str>, format_error: bool, @@ -224,10 +224,10 @@ macro_rules! impl_assertions { }; (@impl $no_error:ident, $with_error:ident, ($($arg:ident),*), $body:expr, $error_formatter:expr, $format_error:literal) => { impl crate::Cheatcode for $no_error { - fn apply_full( + fn apply_full( &self, - ccx: &mut CheatsCtxt, - executor: &mut E, + ccx: &mut CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, ) -> Result { let Self { $($arg),* } = self; handle_assertion_result($body, ccx, executor, $error_formatter, None, $format_error) @@ -235,10 +235,10 @@ macro_rules! impl_assertions { } impl crate::Cheatcode for $with_error { - fn apply_full( + fn apply_full( &self, - ccx: &mut CheatsCtxt, - executor: &mut E, + ccx: &mut CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, ) -> Result { let Self { $($arg),*, error} = self; handle_assertion_result($body, ccx, executor, $error_formatter, Some(error), $format_error) diff --git a/crates/cheatcodes/src/test/assume.rs b/crates/cheatcodes/src/test/assume.rs index e100eeb9d..a0321b5a1 100644 --- a/crates/cheatcodes/src/test/assume.rs +++ b/crates/cheatcodes/src/test/assume.rs @@ -1,5 +1,5 @@ use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Error, Result}; -use foundry_evm_core::{backend::DatabaseExt, constants::MAGIC_ASSUME}; +use foundry_evm_core::constants::MAGIC_ASSUME; use spec::Vm::{assumeCall, assumeNoRevertCall}; use std::fmt::Debug; @@ -21,7 +21,7 @@ impl Cheatcode for assumeCall { } impl Cheatcode for assumeNoRevertCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { ccx.state.assume_no_revert = Some(AssumeNoRevert { depth: ccx.ecx.journaled_state.depth() }); Ok(Default::default()) diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index 16f40c441..2b7ecb62c 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -1,5 +1,7 @@ -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, DatabaseExt, Error, Result, Vm::*}; -use alloy_primitives::{address, hex, Address, Bytes, LogData as RawLog, U256}; +use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Error, Result, Vm::*}; +use alloy_primitives::{ + address, hex, map::hash_map::Entry, Address, Bytes, LogData as RawLog, U256, +}; use alloy_sol_types::{SolError, SolValue}; use foundry_cheatcodes_common::expect::{ExpectedCallData, ExpectedCallType}; use foundry_common::ContractsByArtifact; @@ -8,7 +10,6 @@ use revm::interpreter::{ return_ok, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, }; use spec::Vm; -use std::collections::hash_map::Entry; /// For some cheatcodes we may internally change the status of the call, i.e. in `expectRevert`. /// Solidity will see a successful call and attempt to decode the return data. Therefore, we need @@ -174,7 +175,7 @@ impl Cheatcode for expectCallMinGas_1Call { } impl Cheatcode for expectEmit_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { checkTopic1, checkTopic2, checkTopic3, checkData } = *self; expect_emit( ccx.state, @@ -187,7 +188,7 @@ impl Cheatcode for expectEmit_0Call { } impl Cheatcode for expectEmit_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { checkTopic1, checkTopic2, checkTopic3, checkData, emitter } = *self; expect_emit( ccx.state, @@ -200,21 +201,21 @@ impl Cheatcode for expectEmit_1Call { } impl Cheatcode for expectEmit_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], None, false) } } impl Cheatcode for expectEmit_3Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { emitter } = *self; expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], Some(emitter), false) } } impl Cheatcode for expectEmitAnonymous_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData } = *self; expect_emit( ccx.state, @@ -227,7 +228,7 @@ impl Cheatcode for expectEmitAnonymous_0Call { } impl Cheatcode for expectEmitAnonymous_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData, emitter } = *self; expect_emit( ccx.state, @@ -240,28 +241,28 @@ impl Cheatcode for expectEmitAnonymous_1Call { } impl Cheatcode for expectEmitAnonymous_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], None, true) } } impl Cheatcode for expectEmitAnonymous_3Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { emitter } = *self; expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], Some(emitter), true) } } impl Cheatcode for expectRevert_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; expect_revert(ccx.state, None, ccx.ecx.journaled_state.depth(), false, false, None) } } impl Cheatcode for expectRevert_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData } = self; expect_revert( ccx.state, @@ -275,7 +276,7 @@ impl Cheatcode for expectRevert_1Call { } impl Cheatcode for expectRevert_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData } = self; expect_revert( ccx.state, @@ -289,7 +290,7 @@ impl Cheatcode for expectRevert_2Call { } impl Cheatcode for expectRevert_3Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { reverter } = self; expect_revert( ccx.state, @@ -303,7 +304,7 @@ impl Cheatcode for expectRevert_3Call { } impl Cheatcode for expectRevert_4Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData, reverter } = self; expect_revert( ccx.state, @@ -317,7 +318,7 @@ impl Cheatcode for expectRevert_4Call { } impl Cheatcode for expectRevert_5Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData, reverter } = self; expect_revert( ccx.state, @@ -331,7 +332,7 @@ impl Cheatcode for expectRevert_5Call { } impl Cheatcode for expectPartialRevert_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData } = self; expect_revert( ccx.state, @@ -345,7 +346,7 @@ impl Cheatcode for expectPartialRevert_0Call { } impl Cheatcode for expectPartialRevert_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData, reverter } = self; expect_revert( ccx.state, @@ -359,13 +360,13 @@ impl Cheatcode for expectPartialRevert_1Call { } impl Cheatcode for _expectCheatcodeRevert_0Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { expect_revert(ccx.state, None, ccx.ecx.journaled_state.depth(), true, false, None) } } impl Cheatcode for _expectCheatcodeRevert_1Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData } = self; expect_revert( ccx.state, @@ -379,7 +380,7 @@ impl Cheatcode for _expectCheatcodeRevert_1Call { } impl Cheatcode for _expectCheatcodeRevert_2Call { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { revertData } = self; expect_revert( ccx.state, @@ -393,14 +394,14 @@ impl Cheatcode for _expectCheatcodeRevert_2Call { } impl Cheatcode for expectSafeMemoryCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { min, max } = *self; expect_safe_memory(ccx.state, min, max, ccx.ecx.journaled_state.depth()) } } impl Cheatcode for stopExpectSafeMemoryCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; ccx.state.allowed_mem_writes.remove(&ccx.ecx.journaled_state.depth()); Ok(Default::default()) @@ -408,7 +409,7 @@ impl Cheatcode for stopExpectSafeMemoryCall { } impl Cheatcode for expectSafeMemoryCallCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { min, max } = *self; expect_safe_memory(ccx.state, min, max, ccx.ecx.journaled_state.depth() + 1) } @@ -561,7 +562,7 @@ pub(crate) fn handle_expect_emit( let Some(expected) = &event_to_fill_or_check.log else { // Unless the caller is trying to match an anonymous event, the first topic must be // filled. - if event_to_fill_or_check.anonymous || log.topics().first().is_some() { + if event_to_fill_or_check.anonymous || !log.topics().is_empty() { event_to_fill_or_check.log = Some(log.data.clone()); // If we only filled the expected log then we put it back at the same position. state.expected_emits.insert(index_to_fill_or_check, event_to_fill_or_check); diff --git a/crates/cheatcodes/src/toml.rs b/crates/cheatcodes/src/toml.rs index e83a18390..b55ef2d16 100644 --- a/crates/cheatcodes/src/toml.rs +++ b/crates/cheatcodes/src/toml.rs @@ -3,12 +3,13 @@ use crate::{ json::{ canonicalize_json_path, check_json_key_exists, parse_json, parse_json_coerce, - parse_json_keys, + parse_json_keys, resolve_type, }, Cheatcode, Cheatcodes, Result, Vm::*, }; use alloy_dyn_abi::DynSolType; +use alloy_sol_types::SolValue; use foundry_common::fs; use foundry_config::fs_permissions::FsAccessKind; use serde_json::Value as JsonValue; @@ -133,6 +134,28 @@ impl Cheatcode for parseTomlBytes32ArrayCall { } } +impl Cheatcode for parseTomlType_0Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { toml, typeDescription } = self; + parse_toml_coerce(toml, "$", &resolve_type(typeDescription)?).map(|v| v.abi_encode()) + } +} + +impl Cheatcode for parseTomlType_1Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { toml, key, typeDescription } = self; + parse_toml_coerce(toml, key, &resolve_type(typeDescription)?).map(|v| v.abi_encode()) + } +} + +impl Cheatcode for parseTomlTypeArrayCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { toml, key, typeDescription } = self; + let ty = resolve_type(typeDescription)?; + parse_toml_coerce(toml, key, &DynSolType::Array(Box::new(ty))).map(|v| v.abi_encode()) + } +} + impl Cheatcode for parseTomlKeysCall { fn apply(&self, _state: &mut Cheatcodes) -> Result { let Self { toml, key } = self; diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs index a96a44832..79299d9fd 100644 --- a/crates/cheatcodes/src/utils.rs +++ b/crates/cheatcodes/src/utils.rs @@ -1,12 +1,13 @@ //! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes. -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; -use alloy_primitives::{Address, U256}; +use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; +use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_primitives::{aliases::B32, map::HashMap, B64, U256}; use alloy_sol_types::SolValue; use foundry_common::ens::namehash; -use foundry_evm_core::{backend::DatabaseExt, constants::DEFAULT_CREATE2_DEPLOYER}; -use rand::Rng; -use std::collections::HashMap; +use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER; +use proptest::prelude::Strategy; +use rand::{Rng, RngCore}; /// Contains locations of traces ignored via cheatcodes. /// @@ -71,44 +72,86 @@ impl Cheatcode for ensNamehashCall { impl Cheatcode for randomUint_0Call { fn apply(&self, state: &mut Cheatcodes) -> Result { - let Self {} = self; - let rng = state.rng(); - let random_number: U256 = rng.gen(); - Ok(random_number.abi_encode()) + random_uint(state, None, None) } } impl Cheatcode for randomUint_1Call { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { min, max } = *self; - ensure!(min <= max, "min must be less than or equal to max"); - // Generate random between range min..=max - let exclusive_modulo = max - min; - let rng = state.rng(); - let mut random_number = rng.gen::(); - if exclusive_modulo != U256::MAX { - let inclusive_modulo = exclusive_modulo + U256::from(1); - random_number %= inclusive_modulo; - } - random_number += min; - Ok(random_number.abi_encode()) + random_uint(state, None, Some((min, max))) + } +} + +impl Cheatcode for randomUint_2Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { bits } = *self; + random_uint(state, Some(bits), None) } } impl Cheatcode for randomAddressCall { fn apply(&self, state: &mut Cheatcodes) -> Result { - let Self {} = self; - let rng = state.rng(); - let addr = Address::random_with(rng); - Ok(addr.abi_encode()) + Ok(DynSolValue::type_strategy(&DynSolType::Address) + .new_tree(state.test_runner()) + .unwrap() + .current() + .abi_encode()) + } +} + +impl Cheatcode for randomInt_0Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + random_int(state, None) + } +} + +impl Cheatcode for randomInt_1Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { bits } = *self; + random_int(state, Some(bits)) + } +} + +impl Cheatcode for randomBoolCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let rand_bool: bool = state.rng().gen(); + Ok(rand_bool.abi_encode()) + } +} + +impl Cheatcode for randomBytesCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { len } = *self; + ensure!( + len <= U256::from(usize::MAX), + format!("bytes length cannot exceed {}", usize::MAX) + ); + let mut bytes = vec![0u8; len.to::()]; + state.rng().fill_bytes(&mut bytes); + Ok(bytes.abi_encode()) + } +} + +impl Cheatcode for randomBytes4Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let rand_u32 = state.rng().next_u32(); + Ok(B32::from(rand_u32).abi_encode()) + } +} + +impl Cheatcode for randomBytes8Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let rand_u64 = state.rng().next_u64(); + Ok(B64::from(rand_u64).abi_encode()) } } impl Cheatcode for pauseTracingCall { - fn apply_full( + fn apply_full( &self, - ccx: &mut crate::CheatsCtxt, - executor: &mut E, + ccx: &mut crate::CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, ) -> Result { let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_ref()) else { // No tracer -> nothing to pause @@ -128,10 +171,10 @@ impl Cheatcode for pauseTracingCall { } impl Cheatcode for resumeTracingCall { - fn apply_full( + fn apply_full( &self, - ccx: &mut crate::CheatsCtxt, - executor: &mut E, + ccx: &mut crate::CheatsCtxt, + executor: &mut dyn CheatcodesExecutor, ) -> Result { let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_ref()) else { // No tracer -> nothing to unpause @@ -151,7 +194,7 @@ impl Cheatcode for resumeTracingCall { } impl Cheatcode for setArbitraryStorageCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { target } = self; ccx.state.arbitrary_storage().mark_arbitrary(target); @@ -160,7 +203,7 @@ impl Cheatcode for setArbitraryStorageCall { } impl Cheatcode for copyStorageCall { - fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { from, to } = self; ensure!( @@ -181,3 +224,48 @@ impl Cheatcode for copyStorageCall { Ok(Default::default()) } } + +/// Helper to generate a random `uint` value (with given bits or bounded if specified) +/// from type strategy. +fn random_uint(state: &mut Cheatcodes, bits: Option, bounds: Option<(U256, U256)>) -> Result { + if let Some(bits) = bits { + // Generate random with specified bits. + ensure!(bits <= U256::from(256), "number of bits cannot exceed 256"); + return Ok(DynSolValue::type_strategy(&DynSolType::Uint(bits.to::())) + .new_tree(state.test_runner()) + .unwrap() + .current() + .abi_encode()) + } + + if let Some((min, max)) = bounds { + ensure!(min <= max, "min must be less than or equal to max"); + // Generate random between range min..=max + let exclusive_modulo = max - min; + let mut random_number: U256 = state.rng().gen(); + if exclusive_modulo != U256::MAX { + let inclusive_modulo = exclusive_modulo + U256::from(1); + random_number %= inclusive_modulo; + } + random_number += min; + return Ok(random_number.abi_encode()) + } + + // Generate random `uint256` value. + Ok(DynSolValue::type_strategy(&DynSolType::Uint(256)) + .new_tree(state.test_runner()) + .unwrap() + .current() + .abi_encode()) +} + +/// Helper to generate a random `int` value (with given bits if specified) from type strategy. +fn random_int(state: &mut Cheatcodes, bits: Option) -> Result { + let no_bits = bits.unwrap_or(U256::from(256)); + ensure!(no_bits <= U256::from(256), "number of bits cannot exceed 256"); + Ok(DynSolValue::type_strategy(&DynSolType::Int(no_bits.to::())) + .new_tree(state.test_runner()) + .unwrap() + .current() + .abi_encode()) +} diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index 5d8ad4d2c..7868137d8 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -68,7 +68,6 @@ tracing.workspace = true tikv-jemallocator = { workspace = true, optional = true } [dev-dependencies] -criterion = { version = "0.5", features = ["async_tokio"] } serial_test = "3" tracing-subscriber.workspace = true @@ -78,7 +77,3 @@ rustls = ["reqwest/rustls-tls", "reqwest/rustls-tls-native-roots"] openssl = ["foundry-compilers/openssl", "reqwest/default-tls"] asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["dep:tikv-jemallocator"] - -[[bench]] -name = "session_source" -harness = false diff --git a/crates/chisel/benches/session_source.rs b/crates/chisel/benches/session_source.rs deleted file mode 100644 index 49d127c47..000000000 --- a/crates/chisel/benches/session_source.rs +++ /dev/null @@ -1,84 +0,0 @@ -use chisel::session_source::{SessionSource, SessionSourceConfig}; -use criterion::{criterion_group, Criterion}; -use foundry_compilers::solc::Solc; -use semver::Version; -use std::{hint::black_box, sync::LazyLock}; -use tokio::runtime::Runtime; - -static SOLC: LazyLock = - LazyLock::new(|| Solc::find_or_install(&Version::new(0, 8, 19)).unwrap()); - -/// Benchmark for the `clone_with_new_line` function in [SessionSource] -fn clone_with_new_line(c: &mut Criterion) { - let mut g = c.benchmark_group("session_source"); - - // Grab an empty session source - g.bench_function("clone_with_new_line", |b| { - b.iter(|| { - let session_source = get_empty_session_source(); - let new_line = String::from("uint a = 1"); - black_box(session_source.clone_with_new_line(new_line).unwrap()); - }) - }); -} - -/// Benchmark for the `build` function in [SessionSource] -fn build(c: &mut Criterion) { - let mut g = c.benchmark_group("session_source"); - - g.bench_function("build", |b| { - b.iter(|| { - // Grab an empty session source - let mut session_source = get_empty_session_source(); - black_box(session_source.build().unwrap()) - }) - }); -} - -/// Benchmark for the `execute` function in [SessionSource] -fn execute(c: &mut Criterion) { - let mut g = c.benchmark_group("session_source"); - - g.bench_function("execute", |b| { - b.to_async(rt()).iter(|| async { - // Grab an empty session source - let mut session_source = get_empty_session_source(); - black_box(session_source.execute().await.unwrap()) - }) - }); -} - -/// Benchmark for the `inspect` function in [SessionSource] -fn inspect(c: &mut Criterion) { - let mut g = c.benchmark_group("session_source"); - - g.bench_function("inspect", |b| { - b.to_async(rt()).iter(|| async { - // Grab an empty session source - let mut session_source = get_empty_session_source(); - // Add a uint named "a" with value 1 to the session source - session_source.with_run_code("uint a = 1"); - black_box(session_source.inspect("a").await.unwrap()) - }) - }); -} - -/// Helper function for getting an empty [SessionSource] with default configuration -fn get_empty_session_source() -> SessionSource { - SessionSource::new(SOLC.clone(), SessionSourceConfig::default()) -} - -fn rt() -> Runtime { - Runtime::new().unwrap() -} - -fn main() { - // Install before benches if not present - let _ = LazyLock::force(&SOLC); - - session_source_benches(); - - Criterion::default().configure_from_args().final_summary() -} - -criterion_group!(session_source_benches, clone_with_new_line, build, execute, inspect); diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 7a0703e85..704252794 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -92,6 +92,12 @@ pub enum ChiselSubcommand { /// Clear all cached chisel sessions from the cache directory ClearCache, + + /// Simple evaluation of a command without entering the REPL + Eval { + /// The command to be evaluated. + command: String, + }, } fn main() -> eyre::Result<()> { @@ -102,6 +108,7 @@ fn main() -> eyre::Result<()> { main_args(args) } +#[allow(clippy::needless_return)] #[tokio::main] async fn main_args(args: Chisel) -> eyre::Result<()> { // Keeps track of whether or not an interrupt was the last input @@ -167,6 +174,10 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { } return Ok(()) } + Some(ChiselSubcommand::Eval { command }) => { + dispatch_repl_line(&mut dispatcher, command).await; + return Ok(()) + } None => { /* No chisel subcommand present; Continue */ } } diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index 6afa7ccb9..22ad242d0 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -10,7 +10,7 @@ use crate::{ }, session_source::SessionSource, }; -use alloy_json_abi::JsonAbi; +use alloy_json_abi::{InternalType, JsonAbi}; use alloy_primitives::{hex, Address}; use forge_fmt::FormatterConfig; use foundry_config::{Config, RpcEndpoint}; @@ -514,8 +514,7 @@ impl ChiselDispatcher { let json = response.json::().await.unwrap(); if json.status == "1" && json.result.is_some() { let abi = json.result.unwrap(); - let abi: serde_json::Result = - serde_json::from_slice(abi.as_bytes()); + let abi: serde_json::Result = serde_json::from_str(&abi); if let Ok(abi) = abi { let mut interface = format!( "// Interface of {}\ninterface {} {{\n", @@ -529,7 +528,22 @@ impl ChiselDispatcher { err.name, err.inputs .iter() - .map(|input| format_param!(input)) + .map(|input| { + let mut param_type = &input.ty; + // If complex type then add the name of custom type. + // see . + if input.is_complex_type() { + if let Some( + InternalType::Enum { contract: _, ty } | + InternalType::Struct { contract: _, ty } | + InternalType::Other { contract: _, ty }, + ) = &input.internal_type + { + param_type = ty; + } + } + format!("{} {}", param_type, input.name) + }) .collect::>() .join(",") )); diff --git a/crates/chisel/src/executor.rs b/crates/chisel/src/executor.rs index 05f4db1a6..ccaac8fea 100644 --- a/crates/chisel/src/executor.rs +++ b/crates/chisel/src/executor.rs @@ -49,6 +49,22 @@ impl SessionSource { // Fetch the run function's body statement let run_func_statements = compiled.intermediate.run_func_body()?; + // Record loc of first yul block return statement (if any). + // This is used to decide which is the final statement within the `run()` method. + // see . + let last_yul_return = run_func_statements.iter().find_map(|statement| { + if let pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } = statement { + if let Some(statement) = block.statements.last() { + if let pt::YulStatement::FunctionCall(yul_call) = statement { + if yul_call.id.name == "return" { + return Some(statement.loc()) + } + } + } + } + None + }); + // Find the last statement within the "run()" method and get the program // counter via the source map. if let Some(final_statement) = run_func_statements.last() { @@ -58,9 +74,13 @@ impl SessionSource { // // There is some code duplication within the arms due to the difference between // the [pt::Statement] type and the [pt::YulStatement] types. - let source_loc = match final_statement { + let mut source_loc = match final_statement { pt::Statement::Assembly { loc: _, dialect: _, flags: _, block } => { - if let Some(statement) = block.statements.last() { + // Select last non variable declaration statement, see . + let last_statement = block.statements.iter().rev().find(|statement| { + !matches!(statement, pt::YulStatement::VariableDeclaration(_, _, _)) + }); + if let Some(statement) = last_statement { statement.loc() } else { // In the case where the block is empty, attempt to grab the statement @@ -88,6 +108,13 @@ impl SessionSource { _ => final_statement.loc(), }; + // Consider yul return statement as final statement (if it's loc is lower) . + if let Some(yul_return) = last_yul_return { + if yul_return.end() < source_loc.start() { + source_loc = yul_return; + } + } + // Map the source location of the final statement of the `run()` function to its // corresponding runtime program counter let final_pc = { @@ -1163,7 +1190,7 @@ impl Type { Self::ethabi(&return_parameter.ty, Some(intermediate)).map(|p| (contract_expr.unwrap(), p)) } - /// Inverts Int to Uint and viceversa. + /// Inverts Int to Uint and vice-versa. fn invert_int(self) -> Self { match self { Self::Builtin(DynSolType::Uint(n)) => Self::Builtin(DynSolType::Int(n)), @@ -1372,7 +1399,7 @@ impl<'a> InstructionIter<'a> { } } -impl<'a> Iterator for InstructionIter<'a> { +impl Iterator for InstructionIter<'_> { type Item = Instruction; fn next(&mut self) -> Option { let pc = self.offset; diff --git a/crates/chisel/src/runner.rs b/crates/chisel/src/runner.rs index e78454ee3..72b083e1f 100644 --- a/crates/chisel/src/runner.rs +++ b/crates/chisel/src/runner.rs @@ -3,14 +3,13 @@ //! This module contains the `ChiselRunner` struct, which assists with deploying //! and calling the REPL contract on a in-memory REVM instance. -use alloy_primitives::{Address, Bytes, Log, U256}; +use alloy_primitives::{map::AddressHashMap, Address, Bytes, Log, U256}; use eyre::Result; use foundry_evm::{ executors::{DeployResult, Executor, RawCallResult}, traces::{TraceKind, Traces}, }; use revm::interpreter::{return_ok, InstructionResult}; -use std::collections::HashMap; /// The function selector of the REPL contract's entrypoint, the `run()` function. static RUN_SELECTOR: [u8; 4] = [0xc0, 0x40, 0x62, 0x26]; @@ -43,7 +42,7 @@ pub struct ChiselResult { /// Amount of gas used in the transaction pub gas_used: u64, /// Map of addresses to their labels - pub labeled_addresses: HashMap, + pub labeled_addresses: AddressHashMap, /// Return data pub returned: Bytes, /// Called address diff --git a/crates/chisel/src/session_source.rs b/crates/chisel/src/session_source.rs index 5ba75238a..f80761e0d 100644 --- a/crates/chisel/src/session_source.rs +++ b/crates/chisel/src/session_source.rs @@ -4,6 +4,7 @@ //! the REPL contract's source code. It provides simple compilation, parsing, and //! execution helpers. +use alloy_primitives::map::HashMap; use eyre::Result; use forge_fmt::solang_ext::SafeUnwrap; use foundry_compilers::{ @@ -15,7 +16,7 @@ use foundry_evm::{backend::Backend, opts::EvmOpts}; use semver::Version; use serde::{Deserialize, Serialize}; use solang_parser::{diagnostics::Diagnostic, pt}; -use std::{collections::HashMap, fs, path::PathBuf}; +use std::{fs, path::PathBuf}; use yansi::Paint; /// The minimum Solidity version of the `Vm` interface. @@ -105,16 +106,6 @@ impl SessionSourceConfig { match solc_req { SolcReq::Version(version) => { - // Validate that the requested evm version is supported by the solc version - let req_evm_version = self.foundry_config.evm_version; - if let Some(compat_evm_version) = req_evm_version.normalize_version_solc(&version) { - if req_evm_version > compat_evm_version { - eyre::bail!( - "The set evm version, {req_evm_version}, is not supported by solc {version}. Upgrade to a newer solc version." - ); - } - } - let solc = if let Some(solc) = Solc::find_svm_installed_version(&version)? { solc } else { @@ -321,7 +312,11 @@ impl SessionSource { let settings = Settings { remappings, - evm_version: Some(self.config.foundry_config.evm_version), + evm_version: self + .config + .foundry_config + .evm_version + .normalize_version_solc(&self.solc.version), ..Default::default() }; @@ -349,7 +344,7 @@ impl SessionSource { /// /// Optionally, a map of contract names to a vec of [IntermediateContract]s. pub fn generate_intermediate_contracts(&self) -> Result> { - let mut res_map = HashMap::new(); + let mut res_map = HashMap::default(); let parsed_map = self.compiler_input().sources; for source in parsed_map.values() { Self::get_intermediate_contract(&source.content, &mut res_map); diff --git a/crates/chisel/src/solidity_helper.rs b/crates/chisel/src/solidity_helper.rs index 4707ab37b..696cca1a1 100644 --- a/crates/chisel/src/solidity_helper.rs +++ b/crates/chisel/src/solidity_helper.rs @@ -278,7 +278,7 @@ pub trait TokenStyle { } /// [TokenStyle] implementation for [Token] -impl<'a> TokenStyle for Token<'a> { +impl TokenStyle for Token<'_> { fn style(&self) -> Style { use Token::*; match self { diff --git a/crates/chisel/tests/cache.rs b/crates/chisel/tests/cache.rs index 5f0864bee..7016bce09 100644 --- a/crates/chisel/tests/cache.rs +++ b/crates/chisel/tests/cache.rs @@ -1,7 +1,6 @@ use chisel::session::ChiselSession; use foundry_compilers::artifacts::EvmVersion; -use foundry_config::{Config, SolcReq}; -use semver::Version; +use foundry_config::Config; use serial_test::serial; use std::path::Path; @@ -221,27 +220,3 @@ fn test_load_latest_cache() { assert_eq!(new_env.id.unwrap(), "1"); assert_eq!(new_env.session_source.to_repl_source(), env.session_source.to_repl_source()); } - -#[test] -#[serial] -fn test_solc_evm_configuration_mismatch() { - // Create and clear the cache directory - ChiselSession::create_cache_dir().unwrap(); - ChiselSession::clear_cache().unwrap(); - - // Force the solc version to be 0.8.13 which does not support Paris - let foundry_config = Config { - evm_version: EvmVersion::Paris, - solc: Some(SolcReq::Version(Version::new(0, 8, 13))), - ..Default::default() - }; - - // Create a new session that is expected to fail - let error = ChiselSession::new(chisel::session_source::SessionSourceConfig { - foundry_config, - ..Default::default() - }) - .unwrap_err(); - - assert_eq!(error.to_string(), "The set evm version, paris, is not supported by solc 0.8.13. Upgrade to a newer solc version."); -} diff --git a/crates/cli/src/opts/build/core.rs b/crates/cli/src/opts/build/core.rs index 8df201f7d..809f791d6 100644 --- a/crates/cli/src/opts/build/core.rs +++ b/crates/cli/src/opts/build/core.rs @@ -126,6 +126,15 @@ pub struct CoreBuildArgs { #[serde(skip_serializing_if = "Option::is_none")] pub build_info_path: Option, + /// Use EOF-enabled solc binary. Enables via-ir and sets EVM version to Prague. Requires Docker + /// to be installed. + /// + /// Note that this is a temporary solution until the EOF support is merged into the main solc + /// release. + #[arg(long)] + #[serde(skip)] + pub eof: bool, + /// Skip building files whose names contain the given filter. /// /// `test` and `script` are aliases for `.t.sol` and `.s.sol`. @@ -264,8 +273,8 @@ impl Provider for CoreBuildArgs { dict.insert("ast".to_string(), true.into()); } - if self.compiler.optimize { - dict.insert("optimizer".to_string(), self.compiler.optimize.into()); + if let Some(optimize) = self.compiler.optimize { + dict.insert("optimizer".to_string(), optimize.into()); } if !self.compiler.extra_output.is_empty() { @@ -284,6 +293,10 @@ impl Provider for CoreBuildArgs { dict.insert("revert_strings".to_string(), revert.to_string().into()); } + if self.eof { + dict.insert("eof".to_string(), true.into()); + } + Ok(Map::from([(Config::selected_profile(), dict)])) } } diff --git a/crates/cli/src/opts/build/mod.rs b/crates/cli/src/opts/build/mod.rs index 83452eba6..16cbb4990 100644 --- a/crates/cli/src/opts/build/mod.rs +++ b/crates/cli/src/opts/build/mod.rs @@ -29,9 +29,9 @@ pub struct CompilerArgs { pub evm_version: Option, /// Activate the Solidity optimizer. - #[arg(long)] + #[arg(long, default_missing_value="true", num_args = 0..=1)] #[serde(skip)] - pub optimize: bool, + pub optimize: Option, /// The number of runs specifies roughly how often each opcode of the deployed code will be /// executed across the life-time of the contract. This means it is a trade-off parameter diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 2431af9fc..07f661ef3 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -18,7 +18,8 @@ use foundry_evm::{ debug::DebugTraceIdentifier, decode_trace_arena, identifier::{EtherscanIdentifier, SignaturesIdentifier}, - render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, + render_trace_arena_with_bytecodes, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, + Traces, }, }; use std::{ @@ -393,6 +394,7 @@ pub async fn handle_traces( labels: Vec, debug: bool, decode_internal: bool, + verbose: bool, ) -> Result<()> { let labels = labels.iter().filter_map(|label_str| { let mut iter = label_str.split(':'); @@ -442,19 +444,23 @@ pub async fn handle_traces( .build(); debugger.try_run()?; } else { - print_traces(&mut result, &decoder).await?; + print_traces(&mut result, &decoder, verbose).await?; } Ok(()) } -pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder) -> Result<()> { +pub async fn print_traces( + result: &mut TraceResult, + decoder: &CallTraceDecoder, + verbose: bool, +) -> Result<()> { let traces = result.traces.as_mut().expect("No traces found"); println!("Traces:"); for (_, arena) in traces { decode_trace_arena(arena, decoder).await?; - println!("{}", render_trace_arena(arena)); + println!("{}", render_trace_arena_with_bytecodes(arena, verbose)); } println!(); diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index f5bee0a77..af3c5e0a5 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -163,7 +163,7 @@ pub fn block_on(future: F) -> F::Output { /// Conditionally print a message /// -/// This macro accepts a predicate and the message to print if the predicate is tru +/// This macro accepts a predicate and the message to print if the predicate is true /// /// ```ignore /// let quiet = true; @@ -463,10 +463,7 @@ and it requires clean working and staging areas, including no untracked files. Check the current git repository's status with `git status`. Then, you can track files with `git add ...` and then commit them with `git commit`, -ignore them in the `.gitignore` file, or run this command again with the `--no-commit` flag. - -If none of the previous steps worked, please open an issue at: -https://github.com/foundry-rs/foundry/issues/new/choose" +ignore them in the `.gitignore` file, or run this command again with the `--no-commit` flag." )) } } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index b83b04ad8..5bb6a2024 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -20,7 +20,6 @@ foundry-zksync-compiler.workspace = true foundry-common-fmt.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true -foundry-linking.workspace = true alloy-contract.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } @@ -56,7 +55,6 @@ dunce.workspace = true eyre.workspace = true num-format.workspace = true reqwest.workspace = true -rustc-hash.workspace = true semver.workspace = true serde_json.workspace = true serde.workspace = true diff --git a/crates/common/fmt/src/console.rs b/crates/common/fmt/src/console.rs index 7473ccdec..5bc291e03 100644 --- a/crates/common/fmt/src/console.rs +++ b/crates/common/fmt/src/console.rs @@ -1,61 +1,175 @@ use super::UIfmt; use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256}; -use std::iter::Peekable; +use std::fmt::{self, Write}; + +/// A piece is a portion of the format string which represents the next part to emit. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Piece<'a> { + /// A literal string which should directly be emitted. + String(&'a str), + /// A format specifier which should be replaced with the next argument. + NextArgument(FormatSpec), +} /// A format specifier. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub enum FormatSpec { - /// %s format spec + /// `%s` #[default] String, - /// %d format spec + /// `%d` Number, - /// %i format spec + /// `%i` Integer, - /// %o format spec + /// `%o` Object, - /// %e format spec with an optional precision + /// `%e`, `%18e` Exponential(Option), - /// %x format spec + /// `%x` Hexadecimal, } -impl FormatSpec { - fn from_chars(iter: &mut Peekable) -> Result - where - I: Iterator, - { - match iter.next().ok_or_else(String::new)? { - 's' => Ok(Self::String), - 'd' => Ok(Self::Number), - 'i' => Ok(Self::Integer), - 'o' => Ok(Self::Object), - 'e' => Ok(Self::Exponential(None)), - 'x' => Ok(Self::Hexadecimal), - ch if ch.is_ascii_digit() => { - let mut num = ch.to_string(); - while let Some(&ch) = iter.peek() { - if ch.is_ascii_digit() { - num.push(ch); - iter.next(); - } else { - break; - } +impl fmt::Display for FormatSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("%")?; + match *self { + Self::String => f.write_str("s"), + Self::Number => f.write_str("d"), + Self::Integer => f.write_str("i"), + Self::Object => f.write_str("o"), + Self::Exponential(Some(n)) => write!(f, "{n}e"), + Self::Exponential(None) => f.write_str("e"), + Self::Hexadecimal => f.write_str("x"), + } + } +} + +enum ParseArgError { + /// Failed to parse the argument. + Err, + /// Escape `%%`. + Skip, +} + +/// Parses a format string into a sequence of [pieces][Piece]. +#[derive(Debug)] +pub struct Parser<'a> { + input: &'a str, + chars: std::str::CharIndices<'a>, +} + +impl<'a> Parser<'a> { + /// Creates a new parser for the given input. + pub fn new(input: &'a str) -> Self { + Self { input, chars: input.char_indices() } + } + + /// Parses a string until the next format specifier. + /// + /// `skip` is the number of format specifier characters (`%`) to ignore before returning the + /// string. + fn string(&mut self, start: usize, mut skip: usize) -> &'a str { + while let Some((pos, c)) = self.peek() { + if c == '%' { + if skip == 0 { + return &self.input[start..pos]; } - if let Some(&ch) = iter.peek() { - if ch == 'e' { - let num = num.parse().map_err(|_| num)?; - iter.next(); - Ok(Self::Exponential(Some(num))) - } else { - Err(num) - } - } else { - Err(num) + skip -= 1; + } + self.chars.next(); + } + &self.input[start..] + } + + /// Parses a format specifier. + /// + /// If `Err` is returned, the internal iterator may have been advanced and it may be in an + /// invalid state. + fn argument(&mut self) -> Result { + let (start, ch) = self.peek().ok_or(ParseArgError::Err)?; + let simple_spec = match ch { + 's' => Some(FormatSpec::String), + 'd' => Some(FormatSpec::Number), + 'i' => Some(FormatSpec::Integer), + 'o' => Some(FormatSpec::Object), + 'e' => Some(FormatSpec::Exponential(None)), + 'x' => Some(FormatSpec::Hexadecimal), + // "%%" is a literal '%'. + '%' => return Err(ParseArgError::Skip), + _ => None, + }; + if let Some(spec) = simple_spec { + self.chars.next(); + return Ok(spec); + } + + // %e + if ch.is_ascii_digit() { + let n = self.integer(start); + if let Some((_, 'e')) = self.peek() { + self.chars.next(); + return Ok(FormatSpec::Exponential(n)); + } + } + + Err(ParseArgError::Err) + } + + fn integer(&mut self, start: usize) -> Option { + let mut end = start; + while let Some((pos, ch)) = self.peek() { + if !ch.is_ascii_digit() { + end = pos; + break; + } + self.chars.next(); + } + self.input[start..end].parse().ok() + } + + fn current_pos(&mut self) -> usize { + self.peek().map(|(n, _)| n).unwrap_or(self.input.len()) + } + + fn peek(&mut self) -> Option<(usize, char)> { + self.peek_n(0) + } + + fn peek_n(&mut self, n: usize) -> Option<(usize, char)> { + self.chars.clone().nth(n) + } +} + +impl<'a> Iterator for Parser<'a> { + type Item = Piece<'a>; + + fn next(&mut self) -> Option { + let (mut start, ch) = self.peek()?; + let mut skip = 0; + if ch == '%' { + let prev = self.chars.clone(); + self.chars.next(); + match self.argument() { + Ok(arg) => { + debug_assert_eq!(arg.to_string(), self.input[start..self.current_pos()]); + return Some(Piece::NextArgument(arg)); + } + + // Skip the argument if we encountered "%%". + Err(ParseArgError::Skip) => { + start = self.current_pos(); + skip += 1; + } + + // Reset the iterator if we failed to parse the argument, and include any + // parsed and unparsed specifier in `String`. + Err(ParseArgError::Err) => { + self.chars = prev; + skip += 1; } } - ch => Err(String::from(ch)), } + Some(Piece::String(self.string(start, skip))) } } @@ -249,7 +363,7 @@ impl ConsoleFmt for [u8] { /// assert_eq!(formatted, "foo has 3 characters"); /// ``` pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String { - let mut values = values.iter().copied().peekable(); + let mut values = values.iter().copied(); let mut result = String::with_capacity(spec.len()); // for the first space @@ -275,45 +389,19 @@ pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String { fn format_spec<'a>( s: &str, - values: &mut Peekable>, + mut values: impl Iterator, result: &mut String, ) { - let mut expect_fmt = false; - let mut chars = s.chars().peekable(); - - while chars.peek().is_some() { - if expect_fmt { - expect_fmt = false; - match FormatSpec::from_chars(&mut chars) { - Ok(spec) => { - let value = values.next().expect("value existence is checked"); - // format and write the value + for piece in Parser::new(s) { + match piece { + Piece::String(s) => result.push_str(s), + Piece::NextArgument(spec) => { + if let Some(value) = values.next() { result.push_str(&value.fmt(spec)); - } - Err(consumed) => { - // on parser failure, write '%' and consumed characters - result.push('%'); - result.push_str(&consumed); - } - } - } else { - let ch = chars.next().unwrap(); - if ch == '%' { - if let Some(&next_ch) = chars.peek() { - if next_ch == '%' { - result.push('%'); - chars.next(); - } else if values.peek().is_some() { - // only try formatting if there are values to format - expect_fmt = true; - } else { - result.push(ch); - } } else { - result.push(ch); + // Write the format specifier as-is if there are no more values. + write!(result, "{spec}").unwrap(); } - } else { - result.push(ch); } } } diff --git a/crates/common/fmt/src/ui.rs b/crates/common/fmt/src/ui.rs index 64c520769..a82853145 100644 --- a/crates/common/fmt/src/ui.rs +++ b/crates/common/fmt/src/ui.rs @@ -622,6 +622,7 @@ mixHash {} nonce {} number {} parentHash {} +parentBeaconRoot {} transactionsRoot {} receiptsRoot {} sha3Uncles {} @@ -642,6 +643,7 @@ totalDifficulty {}", block.header.nonce.pretty(), block.header.number.pretty(), block.header.parent_hash.pretty(), + block.header.parent_beacon_block_root.pretty(), block.header.transactions_root.pretty(), block.header.receipts_root.pretty(), block.header.uncles_hash.pretty(), diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 47c25844b..1495c445a 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,16 +1,15 @@ //! Support for compiling [foundry_compilers::Project] -use crate::{compact_to_contract, term::SpinnerReporter, TestFunctionExt}; +use crate::{term::SpinnerReporter, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table}; -use eyre::{Context, Result}; +use eyre::Result; use foundry_block_explorers::contract::Metadata; use foundry_compilers::{ - artifacts::{remappings::Remapping, BytecodeObject, ContractBytecodeSome, Libraries, Source}, + artifacts::{remappings::Remapping, BytecodeObject, Source}, compilers::{ solc::{Solc, SolcCompiler}, Compiler, }, - multi::MultiCompilerLanguage, report::{BasicStdoutReporter, NoReporter, Report}, solc::SolcSettings, zksolc::{ZkSolc, ZkSolcCompiler}, @@ -20,16 +19,13 @@ use foundry_compilers::{ }, Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; -use foundry_linking::Linker; use foundry_zksync_compiler::libraries::{self, ZkMissingLibrary}; use num_format::{Locale, ToFormattedString}; -use rustc_hash::FxHashMap; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, fmt::Display, io::IsTerminal, path::{Path, PathBuf}, - sync::Arc, time::Instant, }; @@ -510,132 +506,6 @@ impl ProjectCompiler { } } -#[derive(Clone, Debug)] -pub struct SourceData { - pub source: Arc, - pub language: MultiCompilerLanguage, - pub name: String, -} - -#[derive(Clone, Debug)] -pub struct ArtifactData { - pub bytecode: ContractBytecodeSome, - pub build_id: String, - pub file_id: u32, -} - -/// Contract source code and bytecode data used for debugger. -#[derive(Clone, Debug, Default)] -pub struct ContractSources { - /// Map over build_id -> file_id -> (source code, language) - pub sources_by_id: HashMap>, - /// Map over contract name -> Vec<(bytecode, build_id, file_id)> - pub artifacts_by_name: HashMap>, -} - -impl ContractSources { - /// Collects the contract sources and artifacts from the project compile output. - pub fn from_project_output( - output: &ProjectCompileOutput, - root: impl AsRef, - libraries: Option<&Libraries>, - ) -> Result { - let mut sources = Self::default(); - - sources.insert(output, root, libraries)?; - - Ok(sources) - } - - pub fn insert( - &mut self, - output: &ProjectCompileOutput, - root: impl AsRef, - libraries: Option<&Libraries>, - ) -> Result<()> - where - C::Language: Into, - { - let root = root.as_ref(); - let link_data = libraries.map(|libraries| { - let linker = Linker::new(root, output.artifact_ids().collect()); - (linker, libraries) - }); - - for (id, artifact) in output.artifact_ids() { - if let Some(file_id) = artifact.id { - let artifact = if let Some((linker, libraries)) = link_data.as_ref() { - linker.link(&id, libraries)?.into_contract_bytecode() - } else { - artifact.clone().into_contract_bytecode() - }; - let bytecode = compact_to_contract(artifact.clone().into_contract_bytecode())?; - - self.artifacts_by_name.entry(id.name.clone()).or_default().push(ArtifactData { - bytecode, - build_id: id.build_id.clone(), - file_id, - }); - } else { - warn!(id = id.identifier(), "source not found"); - } - } - - // Not all source files produce artifacts, so we are populating sources by using build - // infos. - let mut files: BTreeMap> = BTreeMap::new(); - for (build_id, build) in output.builds() { - for (source_id, path) in &build.source_id_to_path { - let source_code = if let Some(source) = files.get(path) { - source.clone() - } else { - let source = Source::read(path).wrap_err_with(|| { - format!("failed to read artifact source file for `{}`", path.display()) - })?; - files.insert(path.clone(), source.content.clone()); - source.content - }; - - self.sources_by_id.entry(build_id.clone()).or_default().insert( - *source_id, - SourceData { - source: source_code, - language: build.language.into(), - name: path.strip_prefix(root).unwrap_or(path).to_string_lossy().to_string(), - }, - ); - } - } - - Ok(()) - } - - /// Returns all sources for a contract by name. - pub fn get_sources( - &self, - name: &str, - ) -> Option> { - self.artifacts_by_name.get(name).map(|artifacts| { - artifacts.iter().filter_map(|artifact| { - let source = - self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?; - Some((artifact, source)) - }) - }) - } - - /// Returns all (name, bytecode, source) sets. - pub fn entries(&self) -> impl Iterator { - self.artifacts_by_name.iter().flat_map(|(name, artifacts)| { - artifacts.iter().filter_map(|artifact| { - let source = - self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?; - Some((name.as_str(), artifact, source)) - }) - }) - } -} - // https://eips.ethereum.org/EIPS/eip-170 const CONTRACT_SIZE_LIMIT: usize = 24576; diff --git a/crates/common/src/evm.rs b/crates/common/src/evm.rs index d281d5652..e738cc6dd 100644 --- a/crates/common/src/evm.rs +++ b/crates/common/src/evm.rs @@ -1,5 +1,6 @@ -//! cli arguments for configuring the evm settings -use alloy_primitives::{Address, B256, U256}; +//! CLI arguments for configuring the EVM settings. + +use alloy_primitives::{map::HashMap, Address, B256, U256}; use clap::{ArgAction, Parser}; use eyre::ContextCompat; use foundry_config::{ @@ -11,11 +12,10 @@ use foundry_config::{ }, Chain, Config, }; -use rustc_hash::FxHashMap; use serde::Serialize; /// Map keyed by breakpoints char to their location (contract address, pc) -pub type Breakpoints = FxHashMap; +pub type Breakpoints = HashMap; /// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy. /// @@ -147,7 +147,7 @@ pub struct EvmArgs { pub isolate: bool, /// Whether to enable Alphanet features. - #[arg(long)] + #[arg(long, alias = "odyssey")] #[serde(skip)] pub alphanet: bool, } diff --git a/crates/common/src/fs.rs b/crates/common/src/fs.rs index 8ee47d2fd..71a62d13a 100644 --- a/crates/common/src/fs.rs +++ b/crates/common/src/fs.rs @@ -43,9 +43,8 @@ pub fn read_to_string(path: impl AsRef) -> Result { pub fn read_json_file(path: &Path) -> Result { // read the file into a byte array first // https://github.com/serde-rs/json/issues/160 - let bytes = read(path)?; - serde_json::from_slice(&bytes) - .map_err(|source| FsPathError::ReadJson { source, path: path.into() }) + let s = read_to_string(path)?; + serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() }) } /// Writes the object as a JSON object. @@ -57,6 +56,15 @@ pub fn write_json_file(path: &Path, obj: &T) -> Result<()> { writer.flush().map_err(|e| FsPathError::write(e, path)) } +/// Writes the object as a pretty JSON object. +pub fn write_pretty_json_file(path: &Path, obj: &T) -> Result<()> { + let file = create_file(path)?; + let mut writer = BufWriter::new(file); + serde_json::to_writer_pretty(&mut writer, obj) + .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?; + writer.flush().map_err(|e| FsPathError::write(e, path)) +} + /// Wrapper for `std::fs::write` pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { let path = path.as_ref(); diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index c22ee3076..23a272a2a 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -4,11 +4,11 @@ use crate::abi::abi_decode_calldata; use alloy_json_abi::JsonAbi; +use alloy_primitives::map::HashMap; use eyre::Context; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ - collections::HashMap, fmt, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -16,7 +16,6 @@ use std::{ }, time::Duration, }; - const SELECTOR_LOOKUP_URL: &str = "https://api.openchain.xyz/signature-database/v1/lookup"; const SELECTOR_IMPORT_URL: &str = "https://api.openchain.xyz/signature-database/v1/import"; @@ -580,6 +579,7 @@ pub fn parse_signatures(tokens: Vec) -> ParsedSignatures { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index 9df8f896f..7335714e5 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -2,7 +2,10 @@ use alloy_consensus::{Transaction, TxEnvelope}; use alloy_primitives::{Address, TxKind, U256}; -use alloy_provider::{network::AnyNetwork, Provider}; +use alloy_provider::{ + network::{AnyNetwork, TransactionBuilder}, + Provider, +}; use alloy_rpc_types::{AnyTransactionReceipt, BlockId, TransactionRequest}; use alloy_serde::WithOtherFields; use alloy_transport::Transport; @@ -212,8 +215,8 @@ impl TransactionMaybeSigned { pub fn gas(&self) -> Option { match self { - Self::Signed { tx, .. } => Some(tx.gas_limit()), - Self::Unsigned(tx) => tx.gas, + Self::Signed { tx, .. } => Some(tx.gas_limit() as u128), + Self::Unsigned(tx) => tx.gas_limit().map(|g| g as u128), } } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index d15f3172e..a83408073 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -43,6 +43,7 @@ toml = { version = "0.8", features = ["preserve_order"] } toml_edit = "0.22.4" tracing.workspace = true walkdir.workspace = true +yansi.workspace = true [target.'cfg(target_os = "windows")'.dependencies] path-slash = "0.2.1" diff --git a/crates/config/README.md b/crates/config/README.md index dc70095d9..4e04da2a5 100644 --- a/crates/config/README.md +++ b/crates/config/README.md @@ -100,7 +100,7 @@ model_checker = { contracts = { 'a.sol' = [ ], timeout = 10000 } verbosity = 0 eth_rpc_url = "https://example.com/" -# Setting this option enables decoding of error traces from mainnet deployed / verfied contracts via etherscan +# Setting this option enables decoding of error traces from mainnet deployed / verified contracts via etherscan etherscan_api_key = "YOURETHERSCANAPIKEY" # ignore solc warnings for missing license and exceeded contract size # known error codes are: ["unreachable", "unused-return", "unused-param", "unused-var", "code-size", "shadowing", "func-mutability", "license", "pragma-solidity", "virtual-interfaces", "same-varname", "too-many-warnings", "constructor-visibility", "init-code-size", "missing-receive-ether", "unnamed-return", "transient-storage"] diff --git a/crates/config/src/error.rs b/crates/config/src/error.rs index 3da1aee09..09f21605d 100644 --- a/crates/config/src/error.rs +++ b/crates/config/src/error.rs @@ -1,8 +1,8 @@ //! error handling and solc error codes +use alloy_primitives::map::HashSet; use figment::providers::{Format, Toml}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::{collections::HashSet, error::Error, fmt, str::FromStr}; - +use std::{error::Error, fmt, str::FromStr}; /// The message shown upon panic if the config could not be extracted from the figment pub const FAILED_TO_EXTRACT_CONFIG_PANIC_MSG: &str = "failed to extract foundry config:"; diff --git a/crates/config/src/inline/mod.rs b/crates/config/src/inline/mod.rs index 36bad2514..8b5616a21 100644 --- a/crates/config/src/inline/mod.rs +++ b/crates/config/src/inline/mod.rs @@ -1,6 +1,6 @@ use crate::Config; -use std::{collections::HashMap, sync::LazyLock}; - +use alloy_primitives::map::HashMap; +use std::sync::LazyLock; mod conf_parser; pub use conf_parser::*; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index f7c5a5215..8a729f0b1 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -37,15 +37,16 @@ use foundry_compilers::{ }; use inflector::Inflector; use regex::Regex; -use revm_primitives::{FixedBytes, SpecId}; +use revm_primitives::{map::AddressHashMap, FixedBytes, SpecId}; use semver::Version; use serde::{Deserialize, Serialize, Serializer}; use std::{ borrow::Cow, - collections::HashMap, fs, path::{Path, PathBuf}, str::FromStr, + sync::mpsc::{self, RecvTimeoutError}, + time::Duration, }; mod macros; @@ -177,6 +178,8 @@ pub struct Config { pub cache: bool, /// where the cache is stored if enabled pub cache_path: PathBuf, + /// where the gas snapshots are stored + pub snapshots: PathBuf, /// where the broadcast logs are stored pub broadcast: PathBuf, /// additional solc allow paths for `--allow-paths` @@ -420,7 +423,7 @@ pub struct Config { pub disable_block_gas_limit: bool, /// Address labels - pub labels: HashMap, + pub labels: AddressHashMap, /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations. /// If disabled, it is possible to access artifacts which were not recompiled or cached. @@ -467,6 +470,9 @@ pub struct Config { /// Timeout for transactions in seconds. pub transaction_timeout: u64, + /// Use EOF-enabled solc for compilation. + pub eof: bool, + /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information #[serde(rename = "__warnings", default, skip_serializing)] pub warnings: Vec, @@ -534,6 +540,9 @@ impl Config { /// Default salt for create2 library deployments pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO; + /// Docker image with eof-enabled solc binary + pub const EOF_SOLC_IMAGE: &'static str = "ghcr.io/paradigmxyz/forge-eof@sha256:46f868ce5264e1190881a3a335d41d7f42d6f26ed20b0c823609c715e38d603f"; + /// Returns the current `Config` /// /// See [`figment`](Self::figment) for more details. @@ -724,6 +733,7 @@ impl Config { self.out = p(&root, &self.out); self.broadcast = p(&root, &self.broadcast); self.cache_path = p(&root, &self.cache_path); + self.snapshots = p(&root, &self.snapshots); if let Some(build_info_path) = self.build_info_path { self.build_info_path = Some(p(&root, &build_info_path)); @@ -789,6 +799,8 @@ impl Config { config.libs.sort_unstable(); config.libs.dedup(); + config.sanitize_eof_settings(); + config } @@ -806,6 +818,22 @@ impl Config { } } + /// Adjusts settings if EOF compilation is enabled. + /// + /// This includes enabling via_ir, eof_version and ensuring that evm_version is not lower than + /// Prague. + pub fn sanitize_eof_settings(&mut self) { + if self.eof { + self.via_ir = true; + if self.eof_version.is_none() { + self.eof_version = Some(EofVersion::V1); + } + if self.evm_version < EvmVersion::Prague { + self.evm_version = EvmVersion::Prague; + } + } + } + /// Returns the directory in which dependencies should be installed /// /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty @@ -894,6 +922,12 @@ impl Config { remove_test_dir(&self.fuzz.failure_persist_dir); remove_test_dir(&self.invariant.failure_persist_dir); + // Remove snapshot directory. + let snapshot_dir = project.root().join(&self.snapshots); + if snapshot_dir.exists() { + let _ = fs::remove_dir_all(&snapshot_dir); + } + Ok(()) } @@ -904,6 +938,40 @@ impl Config { /// /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists. fn ensure_solc(&self) -> Result, SolcError> { + if self.eof { + let (tx, rx) = mpsc::channel(); + let root = self.root.0.clone(); + std::thread::spawn(move || { + tx.send( + Solc::new_with_args( + "docker", + [ + "run", + "--rm", + "-i", + "-v", + &format!("{}:/app/root", root.display()), + Self::EOF_SOLC_IMAGE, + ], + ) + .map(Some), + ) + }); + // If it takes more than 1 second, this likely means we are pulling the image. + return match rx.recv_timeout(Duration::from_secs(1)) { + Ok(res) => res, + Err(RecvTimeoutError::Timeout) => { + eprintln!( + "{}", + yansi::Paint::yellow( + "Pulling Docker image for eof-solc, this might take some time..." + ) + ); + rx.recv().expect("sender dropped") + } + Err(RecvTimeoutError::Disconnected) => panic!("sender dropped"), + } + } if let Some(ref solc) = self.solc { let solc = match solc { SolcReq::Version(version) => { @@ -2136,6 +2204,7 @@ impl Default for Config { cache: true, cache_path: "cache".into(), broadcast: "broadcast".into(), + snapshots: "snapshots".into(), allow_paths: vec![], include_paths: vec![], force: false, @@ -2231,6 +2300,7 @@ impl Default for Config { eof_version: None, alphanet: false, transaction_timeout: 120, + eof: false, _non_exhaustive: (), zksync: Default::default(), } @@ -2427,6 +2497,10 @@ impl Provider for BackwardsCompatTomlProvider

{ dict.insert("solc".to_string(), v); } } + + if let Some(v) = dict.remove("odyssey") { + dict.insert("alphanet".to_string(), v); + } map.insert(profile, dict); } Ok(map) @@ -2436,7 +2510,7 @@ impl Provider for BackwardsCompatTomlProvider

{ /// A provider that sets the `src` and `output` path depending on their existence. struct DappHardhatDirProvider<'a>(&'a Path); -impl<'a> Provider for DappHardhatDirProvider<'a> { +impl Provider for DappHardhatDirProvider<'_> { fn metadata(&self) -> Metadata { Metadata::named("Dapp Hardhat dir compat") } @@ -2767,7 +2841,7 @@ impl Provider for OptionalStrictProfileProvider

{ figment.data().map_err(|err| { // figment does tag metadata and tries to map metadata to an error, since we use a new // figment in this provider this new figment does not know about the metadata of the - // provider and can't map the metadata to the error. Therefor we return the root error + // provider and can't map the metadata to the error. Therefore we return the root error // if this error originated in the provider's data. if let Err(root_err) = self.provider.data() { return root_err; @@ -5084,7 +5158,7 @@ mod tests { let config = Config::load(); assert_eq!( config.labels, - HashMap::from_iter(vec![ + AddressHashMap::from_iter(vec![ ( Address::from_str("0x1F98431c8aD98523631AE4a59f267346ea31F984").unwrap(), "Uniswap V3: Factory".to_string() diff --git a/crates/config/src/providers/remappings.rs b/crates/config/src/providers/remappings.rs index 171967934..2e849bea4 100644 --- a/crates/config/src/providers/remappings.rs +++ b/crates/config/src/providers/remappings.rs @@ -39,9 +39,9 @@ impl Remappings { /// Consumes the wrapper and returns the inner remappings vector. pub fn into_inner(self) -> Vec { - let mut tmp = HashSet::new(); + let mut seen = HashSet::new(); let remappings = - self.remappings.iter().filter(|r| tmp.insert(Self::filter_key(r))).cloned().collect(); + self.remappings.iter().filter(|r| seen.insert(Self::filter_key(r))).cloned().collect(); remappings } @@ -87,7 +87,7 @@ pub struct RemappingsProvider<'a> { pub remappings: Result, Error>, } -impl<'a> RemappingsProvider<'a> { +impl RemappingsProvider<'_> { /// Find and parse remappings for the projects /// /// **Order** @@ -240,7 +240,7 @@ impl<'a> RemappingsProvider<'a> { } } -impl<'a> Provider for RemappingsProvider<'a> { +impl Provider for RemappingsProvider<'_> { fn metadata(&self) -> Metadata { Metadata::named("Remapping Provider") } diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index 6ccb630ca..4fb417db5 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -16,6 +16,7 @@ workspace = true foundry-common.workspace = true foundry-compilers.workspace = true foundry-evm-traces.workspace = true +foundry-evm-core.workspace = true revm-inspectors.workspace = true alloy-primitives.workspace = true diff --git a/crates/debugger/src/tui/builder.rs b/crates/debugger/src/tui/builder.rs index fd952def5..484611c70 100644 --- a/crates/debugger/src/tui/builder.rs +++ b/crates/debugger/src/tui/builder.rs @@ -1,11 +1,9 @@ //! TUI debugger builder. use crate::{node::flatten_call_trace, DebugNode, Debugger}; -use alloy_primitives::Address; +use alloy_primitives::{map::AddressHashMap, Address}; use foundry_common::{evm::Breakpoints, get_contract_name}; use foundry_evm_traces::{debug::ContractSources, CallTraceArena, CallTraceDecoder, Traces}; -use std::collections::HashMap; - /// Debugger builder. #[derive(Debug, Default)] #[must_use = "builders do nothing unless you call `build` on them"] @@ -13,7 +11,7 @@ pub struct DebuggerBuilder { /// Debug traces returned from the EVM execution. debug_arena: Vec, /// Identified contracts. - identified_contracts: HashMap, + identified_contracts: AddressHashMap, /// Map of source files. sources: ContractSources, /// Map of the debugger breakpoints. diff --git a/crates/debugger/src/tui/context.rs b/crates/debugger/src/tui/context.rs index 6792145fe..c3645e31b 100644 --- a/crates/debugger/src/tui/context.rs +++ b/crates/debugger/src/tui/context.rs @@ -3,6 +3,7 @@ use crate::{DebugNode, Debugger, ExitReason}; use alloy_primitives::{hex, Address}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; +use foundry_evm_core::buffer::BufferKind; use revm::interpreter::OpCode; use revm_inspectors::tracing::types::{CallKind, CallTraceStep}; use std::ops::ControlFlow; @@ -15,34 +16,6 @@ pub(crate) struct DrawMemory { pub(crate) current_stack_startline: usize, } -/// Used to keep track of which buffer is currently active to be drawn by the debugger. -#[derive(Debug, PartialEq)] -pub(crate) enum BufferKind { - Memory, - Calldata, - Returndata, -} - -impl BufferKind { - /// Helper to cycle through the active buffers. - pub(crate) fn next(&self) -> Self { - match self { - Self::Memory => Self::Calldata, - Self::Calldata => Self::Returndata, - Self::Returndata => Self::Memory, - } - } - - /// Helper to format the title of the active buffer pane - pub(crate) fn title(&self, size: usize) -> String { - match self { - Self::Memory => format!("Memory (max expansion: {size} bytes)"), - Self::Calldata => format!("Calldata (size: {size} bytes)"), - Self::Returndata => format!("Returndata (size: {size} bytes)"), - } - } -} - pub(crate) struct DebuggerContext<'a> { pub(crate) debugger: &'a mut Debugger, diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index 0f2399a20..55e4834f5 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -1,9 +1,9 @@ //! TUI draw implementation. -use super::context::{BufferKind, DebuggerContext}; +use super::context::DebuggerContext; use crate::op::OpcodeParam; -use alloy_primitives::U256; use foundry_compilers::artifacts::sourcemap::SourceElement; +use foundry_evm_core::buffer::{get_buffer_accesses, BufferKind}; use foundry_evm_traces::debug::SourceData; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -12,7 +12,6 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, Frame, }; -use revm::interpreter::opcode; use revm_inspectors::tracing::types::CallKind; use std::{collections::VecDeque, fmt::Write, io}; @@ -624,91 +623,6 @@ impl<'a> SourceLines<'a> { } } -/// Container for buffer access information. -struct BufferAccess { - offset: usize, - len: usize, -} - -/// Container for read and write buffer access information. -struct BufferAccesses { - /// The read buffer kind and access information. - read: Option<(BufferKind, BufferAccess)>, - /// The only mutable buffer is the memory buffer, so don't store the buffer kind. - write: Option, -} - -/// The memory_access variable stores the index on the stack that indicates the buffer -/// offset/len accessed by the given opcode: -/// (read buffer, buffer read offset, buffer read len, write memory offset, write memory len) -/// \>= 1: the stack index -/// 0: no memory access -/// -1: a fixed len of 32 bytes -/// -2: a fixed len of 1 byte -/// -/// The return value is a tuple about accessed buffer region by the given opcode: -/// (read buffer, buffer read offset, buffer read len, write memory offset, write memory len) -fn get_buffer_accesses(op: u8, stack: &[U256]) -> Option { - let buffer_access = match op { - opcode::KECCAK256 | opcode::RETURN | opcode::REVERT => { - (Some((BufferKind::Memory, 1, 2)), None) - } - opcode::CALLDATACOPY => (Some((BufferKind::Calldata, 2, 3)), Some((1, 3))), - opcode::RETURNDATACOPY => (Some((BufferKind::Returndata, 2, 3)), Some((1, 3))), - opcode::CALLDATALOAD => (Some((BufferKind::Calldata, 1, -1)), None), - opcode::CODECOPY => (None, Some((1, 3))), - opcode::EXTCODECOPY => (None, Some((2, 4))), - opcode::MLOAD => (Some((BufferKind::Memory, 1, -1)), None), - opcode::MSTORE => (None, Some((1, -1))), - opcode::MSTORE8 => (None, Some((1, -2))), - opcode::LOG0 | opcode::LOG1 | opcode::LOG2 | opcode::LOG3 | opcode::LOG4 => { - (Some((BufferKind::Memory, 1, 2)), None) - } - opcode::CREATE | opcode::CREATE2 => (Some((BufferKind::Memory, 2, 3)), None), - opcode::CALL | opcode::CALLCODE => (Some((BufferKind::Memory, 4, 5)), None), - opcode::DELEGATECALL | opcode::STATICCALL => (Some((BufferKind::Memory, 3, 4)), None), - opcode::MCOPY => (Some((BufferKind::Memory, 2, 3)), Some((1, 3))), - opcode::RETURNDATALOAD => (Some((BufferKind::Returndata, 1, -1)), None), - opcode::EOFCREATE => (Some((BufferKind::Memory, 3, 4)), None), - opcode::RETURNCONTRACT => (Some((BufferKind::Memory, 1, 2)), None), - opcode::DATACOPY => (None, Some((1, 3))), - opcode::EXTCALL | opcode::EXTSTATICCALL | opcode::EXTDELEGATECALL => { - (Some((BufferKind::Memory, 2, 3)), None) - } - _ => Default::default(), - }; - - let stack_len = stack.len(); - let get_size = |stack_index| match stack_index { - -2 => Some(1), - -1 => Some(32), - 0 => None, - 1.. => { - if (stack_index as usize) <= stack_len { - Some(stack[stack_len - stack_index as usize].saturating_to()) - } else { - None - } - } - _ => panic!("invalid stack index"), - }; - - if buffer_access.0.is_some() || buffer_access.1.is_some() { - let (read, write) = buffer_access; - let read_access = read.and_then(|b| { - let (buffer, offset, len) = b; - Some((buffer, BufferAccess { offset: get_size(offset)?, len: get_size(len)? })) - }); - let write_access = write.and_then(|b| { - let (offset, len) = b; - Some(BufferAccess { offset: get_size(offset)?, len: get_size(len)? }) - }); - Some(BufferAccesses { read: read_access, write: write_access }) - } else { - None - } -} - fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec>, f: impl Fn(usize, u8) -> Style) { for (i, &byte) in bytes.iter().enumerate() { if i > 0 { diff --git a/crates/debugger/src/tui/mod.rs b/crates/debugger/src/tui/mod.rs index 155973e15..75b747a7e 100644 --- a/crates/debugger/src/tui/mod.rs +++ b/crates/debugger/src/tui/mod.rs @@ -1,6 +1,6 @@ //! The TUI implementation. -use alloy_primitives::Address; +use alloy_primitives::map::AddressHashMap; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event}, execute, @@ -14,7 +14,6 @@ use ratatui::{ Terminal, }; use std::{ - collections::HashMap, io, ops::ControlFlow, sync::{mpsc, Arc}, @@ -44,7 +43,7 @@ pub enum ExitReason { /// The TUI debugger. pub struct Debugger { debug_arena: Vec, - identified_contracts: HashMap, + identified_contracts: AddressHashMap, /// Source map of contract sources contracts_sources: ContractSources, breakpoints: Breakpoints, @@ -60,7 +59,7 @@ impl Debugger { /// Creates a new debugger. pub fn new( debug_arena: Vec, - identified_contracts: HashMap, + identified_contracts: AddressHashMap, contracts_sources: ContractSources, breakpoints: Breakpoints, ) -> Self { diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 0dcd40747..3ebc7f8da 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -20,7 +20,6 @@ foundry-config.workspace = true alloy-primitives.workspace = true -auto_impl.workspace = true derive_more.workspace = true eyre.workspace = true itertools.workspace = true diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index e21e80c22..3ec433c39 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -2,6 +2,7 @@ use crate::{ document::DocumentContent, helpers::merge_toml_table, AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor, }; +use alloy_primitives::map::HashMap; use forge_fmt::{FormatterConfig, Visitable}; use foundry_compilers::{compilers::solc::SOLC_EXTENSIONS, utils::source_files_iter}; use foundry_config::{filter::expand_globs, DocConfig}; @@ -10,7 +11,6 @@ use mdbook::MDBook; use rayon::prelude::*; use std::{ cmp::Ordering, - collections::HashMap, fs, path::{Path, PathBuf}, }; diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index be4c1e647..10f72a672 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -1,6 +1,6 @@ use crate::{DocBuilder, ParseItem, PreprocessorId, PreprocessorOutput}; +use alloy_primitives::map::HashMap; use std::{ - collections::HashMap, path::{Path, PathBuf}, slice::IterMut, sync::Mutex, diff --git a/crates/doc/src/parser/comment.rs b/crates/doc/src/parser/comment.rs index 280dcfd0d..4954cd7cd 100644 --- a/crates/doc/src/parser/comment.rs +++ b/crates/doc/src/parser/comment.rs @@ -1,6 +1,6 @@ +use alloy_primitives::map::HashMap; use derive_more::{Deref, DerefMut}; use solang_parser::doccomment::DocCommentTag; -use std::collections::HashMap; /// The natspec comment tag explaining the purpose of the comment. /// See: . @@ -96,6 +96,11 @@ impl Comment { }, ) } + + /// Check if this comment is a custom tag. + pub fn is_custom(&self) -> bool { + matches!(self.tag, CommentTag::Custom(_)) + } } /// The collection of natspec [Comment] items. @@ -157,18 +162,18 @@ impl From> for Comments { pub struct CommentsRef<'a>(Vec<&'a Comment>); impl<'a> CommentsRef<'a> { - /// Filter a collection of comments and return only those that match a provided tag + /// Filter a collection of comments and return only those that match a provided tag. pub fn include_tag(&self, tag: CommentTag) -> Self { self.include_tags(&[tag]) } - /// Filter a collection of comments and return only those that match provided tags + /// Filter a collection of comments and return only those that match provided tags. pub fn include_tags(&self, tags: &[CommentTag]) -> Self { // Cloning only references here CommentsRef(self.iter().cloned().filter(|c| tags.contains(&c.tag)).collect()) } - /// Filter a collection of comments and return only those that do not match provided tags + /// Filter a collection of comments and return only those that do not match provided tags. pub fn exclude_tags(&self, tags: &[CommentTag]) -> Self { // Cloning only references here CommentsRef(self.iter().cloned().filter(|c| !tags.contains(&c.tag)).collect()) @@ -192,6 +197,11 @@ impl<'a> CommentsRef<'a> { .find(|c| matches!(c.tag, CommentTag::Inheritdoc)) .and_then(|c| c.value.split_whitespace().next()) } + + /// Filter a collection of comments and only return the custom tags. + pub fn get_custom_tags(&self) -> Self { + CommentsRef(self.iter().cloned().filter(|c| c.is_custom()).collect()) + } } impl<'a> From<&'a Comments> for CommentsRef<'a> { @@ -228,4 +238,32 @@ mod tests { assert_eq!(CommentTag::from_str("custom"), None); assert_eq!(CommentTag::from_str("sometag"), None); } + + #[test] + fn test_is_custom() { + // Test custom tag. + let custom_comment = Comment::new( + CommentTag::from_str("custom:test").unwrap(), + "dummy custom tag".to_owned(), + ); + assert!(custom_comment.is_custom(), "Custom tag should return true for is_custom"); + + // Test non-custom tags. + let non_custom_tags = [ + CommentTag::Title, + CommentTag::Author, + CommentTag::Notice, + CommentTag::Dev, + CommentTag::Param, + CommentTag::Return, + CommentTag::Inheritdoc, + ]; + for tag in non_custom_tags { + let comment = Comment::new(tag.clone(), "Non-custom comment".to_string()); + assert!( + !comment.is_custom(), + "Non-custom tag {tag:?} should return false for is_custom" + ); + } + } } diff --git a/crates/doc/src/preprocessor/contract_inheritance.rs b/crates/doc/src/preprocessor/contract_inheritance.rs index 7d74589bd..ac229a434 100644 --- a/crates/doc/src/preprocessor/contract_inheritance.rs +++ b/crates/doc/src/preprocessor/contract_inheritance.rs @@ -1,7 +1,8 @@ use super::{Preprocessor, PreprocessorId}; use crate::{document::DocumentContent, Document, ParseSource, PreprocessorOutput}; +use alloy_primitives::map::HashMap; use forge_fmt::solang_ext::SafeUnwrap; -use std::{collections::HashMap, path::PathBuf}; +use std::path::PathBuf; /// [ContractInheritance] preprocessor id. pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inheritance"); diff --git a/crates/doc/src/preprocessor/infer_hyperlinks.rs b/crates/doc/src/preprocessor/infer_hyperlinks.rs index 2d0802789..25fafc032 100644 --- a/crates/doc/src/preprocessor/infer_hyperlinks.rs +++ b/crates/doc/src/preprocessor/infer_hyperlinks.rs @@ -200,7 +200,7 @@ impl<'a> InlineLinkTarget<'a> { } } -impl<'a> std::fmt::Display for InlineLinkTarget<'a> { +impl std::fmt::Display for InlineLinkTarget<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // NOTE: the url should be absolute for markdown and section names are lowercase write!(f, "/{}#{}", self.target_path.display(), self.section.to_lowercase()) diff --git a/crates/doc/src/preprocessor/inheritdoc.rs b/crates/doc/src/preprocessor/inheritdoc.rs index 8d29a64fc..65cbb688c 100644 --- a/crates/doc/src/preprocessor/inheritdoc.rs +++ b/crates/doc/src/preprocessor/inheritdoc.rs @@ -2,8 +2,8 @@ use super::{Preprocessor, PreprocessorId}; use crate::{ document::DocumentContent, Comments, Document, ParseItem, ParseSource, PreprocessorOutput, }; +use alloy_primitives::map::HashMap; use forge_fmt::solang_ext::SafeUnwrap; -use std::collections::HashMap; /// [`Inheritdoc`] preprocessor ID. pub const INHERITDOC_ID: PreprocessorId = PreprocessorId("inheritdoc"); diff --git a/crates/doc/src/preprocessor/mod.rs b/crates/doc/src/preprocessor/mod.rs index 25ed4db23..5011b59a1 100644 --- a/crates/doc/src/preprocessor/mod.rs +++ b/crates/doc/src/preprocessor/mod.rs @@ -1,7 +1,8 @@ //! Module containing documentation preprocessors. use crate::{Comments, Document}; -use std::{collections::HashMap, fmt::Debug, path::PathBuf}; +use alloy_primitives::map::HashMap; +use std::{fmt::Debug, path::PathBuf}; mod contract_inheritance; pub use contract_inheritance::{ContractInheritance, CONTRACT_INHERITANCE_ID}; diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index a21a59c11..d68f7a8e5 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -14,7 +14,6 @@ use std::path::{Path, PathBuf}; pub type AsDocResult = Result; /// A trait for formatting a parse unit as documentation. -#[auto_impl::auto_impl(&)] pub trait AsDoc { /// Formats a parse tree item into a doc string. fn as_doc(&self) -> AsDocResult; @@ -32,7 +31,7 @@ impl AsDoc for Comments { } } -impl<'a> AsDoc for CommentsRef<'a> { +impl AsDoc for CommentsRef<'_> { // TODO: support other tags fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); @@ -47,18 +46,32 @@ impl<'a> AsDoc for CommentsRef<'a> { // Write notice tags let notices = self.include_tag(CommentTag::Notice); - for notice in notices.iter() { - writer.writeln_raw(¬ice.value)?; + for n in notices.iter() { + writer.writeln_raw(&n.value)?; writer.writeln()?; } // Write dev tags let devs = self.include_tag(CommentTag::Dev); - for dev in devs.iter() { - writer.write_italic(&dev.value)?; + for d in devs.iter() { + writer.write_italic(&d.value)?; writer.writeln()?; } + // Write custom tags + let customs = self.get_custom_tags(); + if !customs.is_empty() { + writer.write_bold(&format!("Note{}:", if customs.len() == 1 { "" } else { "s" }))?; + for c in customs.iter() { + writer.writeln_raw(format!( + "{}{}", + if customs.len() == 1 { "" } else { "- " }, + &c.value + ))?; + writer.writeln()?; + } + } + Ok(writer.finish()) } } @@ -224,7 +237,7 @@ impl AsDoc for Document { // TODO: cleanup // Write function docs writer.writeln_doc( - item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), + &item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), )?; // Write function header @@ -235,7 +248,7 @@ impl AsDoc for Document { func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); writer.try_write_param_table(CommentTag::Param, ¶ms, &item.comments)?; - // Write function parameter comments in a table + // Write function return parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); writer.try_write_param_table( @@ -295,7 +308,7 @@ impl Document { writer.writeln()?; // Write function docs - writer.writeln_doc(comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]))?; + writer.writeln_doc(&comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]))?; // Write function header writer.write_code(code)?; @@ -304,7 +317,7 @@ impl Document { let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); writer.try_write_param_table(CommentTag::Param, ¶ms, &comments)?; - // Write function parameter comments in a table + // Write function return parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); writer.try_write_param_table(CommentTag::Return, &returns, &comments)?; diff --git a/crates/doc/src/writer/buf_writer.rs b/crates/doc/src/writer/buf_writer.rs index dfec68fe2..e6109c338 100644 --- a/crates/doc/src/writer/buf_writer.rs +++ b/crates/doc/src/writer/buf_writer.rs @@ -43,7 +43,7 @@ impl BufWriter { } /// Write [AsDoc] implementation to the buffer with newline. - pub fn writeln_doc(&mut self, doc: T) -> fmt::Result { + pub fn writeln_doc(&mut self, doc: &T) -> fmt::Result { writeln!(self.buf, "{}", doc.as_doc()?) } diff --git a/crates/doc/src/writer/markdown.rs b/crates/doc/src/writer/markdown.rs index 2e577ad96..7583c35e7 100644 --- a/crates/doc/src/writer/markdown.rs +++ b/crates/doc/src/writer/markdown.rs @@ -21,7 +21,7 @@ pub enum Markdown<'a> { CodeBlock(&'a str, &'a str), } -impl<'a> AsDoc for Markdown<'a> { +impl AsDoc for Markdown<'_> { fn as_doc(&self) -> AsDocResult { let doc = match self { Self::H1(val) => format!("# {val}"), @@ -37,7 +37,7 @@ impl<'a> AsDoc for Markdown<'a> { } } -impl<'a> std::fmt::Display for Markdown<'a> { +impl std::fmt::Display for Markdown<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}", self.as_doc()?)) } diff --git a/crates/doc/src/writer/traits.rs b/crates/doc/src/writer/traits.rs index b59708ed9..0b79718d5 100644 --- a/crates/doc/src/writer/traits.rs +++ b/crates/doc/src/writer/traits.rs @@ -56,7 +56,7 @@ impl ParamLike for solang_parser::pt::ErrorParameter { } } -impl<'a, T> ParamLike for &'a T +impl ParamLike for &T where T: ParamLike, { diff --git a/crates/evm/abi/Cargo.toml b/crates/evm/abi/Cargo.toml index 330ea6626..b3202c967 100644 --- a/crates/evm/abi/Cargo.toml +++ b/crates/evm/abi/Cargo.toml @@ -22,7 +22,6 @@ alloy-sol-types = { workspace = true, features = ["json"] } derive_more.workspace = true itertools.workspace = true -rustc-hash.workspace = true [dev-dependencies] foundry-test-utils.workspace = true diff --git a/crates/evm/abi/src/console/hardhat.rs b/crates/evm/abi/src/console/hardhat.rs index 5f2a5829a..4081982e3 100644 --- a/crates/evm/abi/src/console/hardhat.rs +++ b/crates/evm/abi/src/console/hardhat.rs @@ -1,8 +1,7 @@ -use alloy_primitives::{address, Address, Selector}; +use alloy_primitives::{address, map::HashMap, Address, Selector}; use alloy_sol_types::sol; use foundry_common_fmt::*; use foundry_macros::ConsoleFmt; -use rustc_hash::FxHashMap; use std::sync::LazyLock; sol!( @@ -44,8 +43,8 @@ pub fn hh_console_selector(input: &[u8]) -> Option<&'static Selector> { /// `hardhat/console.log` logs its events manually, and in functions that accept integers they're /// encoded as `abi.encodeWithSignature("log(int)", p0)`, which is not the canonical ABI encoding /// for `int` that Solidity and [`sol!`] use. -pub static HARDHAT_CONSOLE_SELECTOR_PATCHES: LazyLock> = - LazyLock::new(|| FxHashMap::from_iter(include!("./patches.rs"))); +pub static HARDHAT_CONSOLE_SELECTOR_PATCHES: LazyLock> = + LazyLock::new(|| HashMap::from_iter(include!("./patches.rs"))); #[cfg(test)] mod tests { diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index edfca5892..fe46a1f4f 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -46,6 +46,7 @@ revm = { workspace = true, features = [ "arbitrary", "optimism", "c-kzg", + "blst", ] } revm-inspectors.workspace = true @@ -54,7 +55,6 @@ eyre.workspace = true futures.workspace = true itertools.workspace = true parking_lot.workspace = true -rustc-hash.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index 9389daddd..e1879c33a 100644 --- a/crates/evm/core/src/backend/cow.rs +++ b/crates/evm/core/src/backend/cow.rs @@ -3,13 +3,13 @@ use super::{BackendError, ForkInfo}; use crate::{ backend::{ - diagnostic::RevertDiagnostic, Backend, DatabaseExt, LocalForkId, RevertSnapshotAction, + diagnostic::RevertDiagnostic, Backend, DatabaseExt, LocalForkId, RevertStateSnapshotAction, }, fork::{CreateFork, ForkId}, InspectorExt, }; use alloy_genesis::GenesisAccount; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{map::HashMap, Address, B256, U256}; use alloy_rpc_types::TransactionRequest; use eyre::WrapErr; use foundry_fork_db::DatabaseError; @@ -21,10 +21,7 @@ use revm::{ }, Database, DatabaseCommit, JournaledState, }; -use std::{ - borrow::Cow, - collections::{BTreeMap, HashMap}, -}; +use std::{borrow::Cow, collections::BTreeMap}; /// A wrapper around `Backend` that ensures only `revm::DatabaseRef` functions are called. /// @@ -78,10 +75,11 @@ impl<'a> CowBackend<'a> { /// /// Note: in case there are any cheatcodes executed that modify the environment, this will /// update the given `env` with the new values. - pub fn inspect<'b, I: InspectorExt<&'b mut Self>>( - &'b mut self, + #[instrument(name = "inspect", level = "debug", skip_all)] + pub fn inspect( + &mut self, env: &mut EnvWithHandlerCfg, - inspector: I, + inspector: &mut I, ) -> eyre::Result { // this is a new call to inspect with a new env, so even if we've cloned the backend // already, we reset the initialized state @@ -100,11 +98,11 @@ impl<'a> CowBackend<'a> { Self { backend: Cow::Borrowed(backend), is_initialized: false, spec_id: SpecId::LATEST } } - /// Returns whether there was a snapshot failure in the backend. + /// Returns whether there was a state snapshot failure in the backend. /// /// This is bubbled up from the underlying Copy-On-Write backend when a revert occurs. - pub fn has_snapshot_failure(&self) -> bool { - self.backend.has_snapshot_failure() + pub fn has_state_snapshot_failure(&self) -> bool { + self.backend.has_state_snapshot_failure() } /// Returns a mutable instance of the Backend. @@ -130,36 +128,36 @@ impl<'a> CowBackend<'a> { } } -impl<'a> DatabaseExt for CowBackend<'a> { +impl DatabaseExt for CowBackend<'_> { fn get_fork_info(&mut self, id: LocalForkId) -> eyre::Result { self.backend.to_mut().get_fork_info(id) } - fn snapshot(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { - self.backend_mut(env).snapshot(journaled_state, env) + fn snapshot_state(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { + self.backend_mut(env).snapshot_state(journaled_state, env) } - fn revert( + fn revert_state( &mut self, id: U256, journaled_state: &JournaledState, current: &mut Env, - action: RevertSnapshotAction, + action: RevertStateSnapshotAction, ) -> Option { - self.backend_mut(current).revert(id, journaled_state, current, action) + self.backend_mut(current).revert_state(id, journaled_state, current, action) } - fn delete_snapshot(&mut self, id: U256) -> bool { - // delete snapshot requires a previous snapshot to be initialized + fn delete_state_snapshot(&mut self, id: U256) -> bool { + // delete state snapshot requires a previous snapshot to be initialized if let Some(backend) = self.initialized_backend_mut() { - return backend.delete_snapshot(id) + return backend.delete_state_snapshot(id) } false } - fn delete_snapshots(&mut self) { + fn delete_state_snapshots(&mut self) { if let Some(backend) = self.initialized_backend_mut() { - backend.delete_snapshots() + backend.delete_state_snapshots() } } @@ -208,21 +206,21 @@ impl<'a> DatabaseExt for CowBackend<'a> { &mut self, id: Option, transaction: B256, - env: &mut Env, + env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()> { - self.backend_mut(env).transact(id, transaction, env, journaled_state, inspector) + self.backend_mut(&env).transact(id, transaction, env, journaled_state, inspector) } fn transact_from_tx( &mut self, - transaction: TransactionRequest, - env: &Env, + transaction: &TransactionRequest, + env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()> { - self.backend_mut(env).transact_from_tx(transaction, env, journaled_state, inspector) + self.backend_mut(&env).transact_from_tx(transaction, env, journaled_state, inspector) } fn active_fork_id(&self) -> Option { @@ -257,6 +255,15 @@ impl<'a> DatabaseExt for CowBackend<'a> { self.backend_mut(&Env::default()).load_allocs(allocs, journaled_state) } + fn clone_account( + &mut self, + source: &GenesisAccount, + target: &Address, + journaled_state: &mut JournaledState, + ) -> Result<(), BackendError> { + self.backend_mut(&Env::default()).clone_account(source, target, journaled_state) + } + fn is_persistent(&self, acc: &Address) -> bool { self.backend.is_persistent(acc) } @@ -294,7 +301,7 @@ impl<'a> DatabaseExt for CowBackend<'a> { } } -impl<'a> DatabaseRef for CowBackend<'a> { +impl DatabaseRef for CowBackend<'_> { type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { @@ -314,7 +321,7 @@ impl<'a> DatabaseRef for CowBackend<'a> { } } -impl<'a> Database for CowBackend<'a> { +impl Database for CowBackend<'_> { type Error = DatabaseError; fn basic(&mut self, address: Address) -> Result, Self::Error> { @@ -334,7 +341,7 @@ impl<'a> Database for CowBackend<'a> { } } -impl<'a> DatabaseCommit for CowBackend<'a> { +impl DatabaseCommit for CowBackend<'_> { fn commit(&mut self, changes: Map) { self.backend.to_mut().commit(changes) } diff --git a/crates/evm/core/src/backend/diagnostic.rs b/crates/evm/core/src/backend/diagnostic.rs index 109190a8f..df215508d 100644 --- a/crates/evm/core/src/backend/diagnostic.rs +++ b/crates/evm/core/src/backend/diagnostic.rs @@ -1,7 +1,6 @@ use crate::backend::LocalForkId; -use alloy_primitives::Address; +use alloy_primitives::{map::AddressHashMap, Address}; use itertools::Itertools; -use std::collections::HashMap; /// Represents possible diagnostic cases on revert #[derive(Clone, Debug)] @@ -21,7 +20,7 @@ pub enum RevertDiagnostic { impl RevertDiagnostic { /// Converts the diagnostic to a readable error message - pub fn to_error_msg(&self, labels: &HashMap) -> String { + pub fn to_error_msg(&self, labels: &AddressHashMap) -> String { let get_label = |addr: &Address| labels.get(addr).cloned().unwrap_or_else(|| addr.to_string()); diff --git a/crates/evm/core/src/backend/in_memory_db.rs b/crates/evm/core/src/backend/in_memory_db.rs index e819c5313..4e90bfec0 100644 --- a/crates/evm/core/src/backend/in_memory_db.rs +++ b/crates/evm/core/src/backend/in_memory_db.rs @@ -1,6 +1,6 @@ //! In-memory database. -use crate::snapshot::Snapshots; +use crate::state_snapshot::StateSnapshots; use alloy_primitives::{Address, B256, U256}; use foundry_fork_db::DatabaseError; use revm::{ @@ -20,12 +20,12 @@ pub type FoundryEvmInMemoryDB = CacheDB; #[derive(Debug)] pub struct MemDb { pub inner: FoundryEvmInMemoryDB, - pub snapshots: Snapshots, + pub state_snapshots: StateSnapshots, } impl Default for MemDb { fn default() -> Self { - Self { inner: CacheDB::new(Default::default()), snapshots: Default::default() } + Self { inner: CacheDB::new(Default::default()), state_snapshots: Default::default() } } } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index db33dea50..1cac94852 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -3,12 +3,12 @@ use crate::{ constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, TEST_CONTRACT_ADDRESS}, fork::{CreateFork, ForkId, MultiFork}, - snapshot::Snapshots, - utils::{configure_tx_env, new_evm_with_inspector}, + state_snapshot::StateSnapshots, + utils::{configure_tx_env, configure_tx_req_env, new_evm_with_inspector}, InspectorExt, }; use alloy_genesis::GenesisAccount; -use alloy_primitives::{keccak256, uint, Address, B256, U256}; +use alloy_primitives::{keccak256, map::HashMap, uint, Address, B256, U256}; use alloy_rpc_types::{Block, BlockNumberOrTag, Transaction, TransactionRequest}; use alloy_serde::WithOtherFields; use eyre::Context; @@ -29,7 +29,7 @@ use revm::{ Database, DatabaseCommit, JournaledState, }; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, time::Instant, }; @@ -46,7 +46,7 @@ mod in_memory_db; pub use in_memory_db::{EmptyDBWrapper, FoundryEvmInMemoryDB, MemDb}; mod snapshot; -pub use snapshot::{BackendSnapshot, RevertSnapshotAction, StateSnapshot}; +pub use snapshot::{BackendStateSnapshot, RevertStateSnapshotAction, StateSnapshot}; mod fork_type; pub use fork_type::{CachedForkType, ForkType}; @@ -86,12 +86,12 @@ pub struct ForkInfo { /// An extension trait that allows us to easily extend the `revm::Inspector` capabilities #[auto_impl::auto_impl(&mut)] pub trait DatabaseExt: Database + DatabaseCommit { - /// Creates a new snapshot at the current point of execution. + /// Creates a new state snapshot at the current point of execution. /// - /// A snapshot is associated with a new unique id that's created for the snapshot. - /// Snapshots can be reverted: [DatabaseExt::revert], however, depending on the - /// [RevertSnapshotAction], it will keep the snapshot alive or delete it. - fn snapshot(&mut self, journaled_state: &JournaledState, env: &Env) -> U256; + /// A state snapshot is associated with a new unique id that's created for the snapshot. + /// State snapshots can be reverted: [DatabaseExt::revert_state], however, depending on the + /// [RevertStateSnapshotAction], it will keep the snapshot alive or delete it. + fn snapshot_state(&mut self, journaled_state: &JournaledState, env: &Env) -> U256; /// Retrieves information about a fork /// @@ -109,25 +109,25 @@ pub trait DatabaseExt: Database + DatabaseCommit { /// since the snapshots was created. This way we can show logs that were emitted between /// snapshot and its revert. /// This will also revert any changes in the `Env` and replace it with the captured `Env` of - /// `Self::snapshot`. + /// `Self::snapshot_state`. /// - /// Depending on [RevertSnapshotAction] it will keep the snapshot alive or delete it. - fn revert( + /// Depending on [RevertStateSnapshotAction] it will keep the snapshot alive or delete it. + fn revert_state( &mut self, id: U256, journaled_state: &JournaledState, env: &mut Env, - action: RevertSnapshotAction, + action: RevertStateSnapshotAction, ) -> Option; - /// Deletes the snapshot with the given `id` + /// Deletes the state snapshot with the given `id` /// /// Returns `true` if the snapshot was successfully deleted, `false` if no snapshot for that id /// exists. - fn delete_snapshot(&mut self, id: U256) -> bool; + fn delete_state_snapshot(&mut self, id: U256) -> bool; - /// Deletes all snapshots. - fn delete_snapshots(&mut self); + /// Deletes all state snapshots. + fn delete_state_snapshots(&mut self); /// Creates and also selects a new fork /// @@ -220,18 +220,18 @@ pub trait DatabaseExt: Database + DatabaseCommit { &mut self, id: Option, transaction: B256, - env: &mut Env, + env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()>; /// Executes a given TransactionRequest, commits the new state to the DB fn transact_from_tx( &mut self, - transaction: TransactionRequest, - env: &Env, + transaction: &TransactionRequest, + env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()>; /// Returns the `ForkId` that's currently used in the database, if fork mode is on @@ -301,6 +301,17 @@ pub trait DatabaseExt: Database + DatabaseCommit { journaled_state: &mut JournaledState, ) -> Result<(), BackendError>; + /// Copies bytecode, storage, nonce and balance from the given genesis account to the target + /// address. + /// + /// Returns [Ok] if data was successfully inserted into the journal, [Err] otherwise. + fn clone_account( + &mut self, + source: &GenesisAccount, + target: &Address, + journaled_state: &mut JournaledState, + ) -> Result<(), BackendError>; + /// Returns true if the given account is currently marked as persistent. fn is_persistent(&self, acc: &Address) -> bool; @@ -438,7 +449,7 @@ struct _ObjectSafe(dyn DatabaseExt); /// afterwards, as well as any snapshots taken after the reverted snapshot, (e.g.: reverting to id /// 0x1 will delete snapshots with ids 0x1, 0x2, etc.) /// -/// **Note:** Snapshots work across fork-swaps, e.g. if fork `A` is currently active, then a +/// **Note:** State snapshots work across fork-swaps, e.g. if fork `A` is currently active, then a /// snapshot is created before fork `B` is selected, then fork `A` will be the active fork again /// after reverting the snapshot. #[derive(Clone, Debug)] @@ -595,8 +606,10 @@ impl Backend { } /// Returns all snapshots created in this backend - pub fn snapshots(&self) -> &Snapshots> { - &self.inner.snapshots + pub fn state_snapshots( + &self, + ) -> &StateSnapshots> { + &self.inner.state_snapshots } /// Sets the address of the `DSTest` contract that is being executed @@ -638,18 +651,18 @@ impl Backend { self.inner.caller } - /// Failures occurred in snapshots are tracked when the snapshot is reverted + /// Failures occurred in state snapshots are tracked when the state snapshot is reverted. /// - /// If an error occurs in a restored snapshot, the test is considered failed. + /// If an error occurs in a restored state snapshot, the test is considered failed. /// - /// This returns whether there was a reverted snapshot that recorded an error - pub fn has_snapshot_failure(&self) -> bool { - self.inner.has_snapshot_failure + /// This returns whether there was a reverted state snapshot that recorded an error. + pub fn has_state_snapshot_failure(&self) -> bool { + self.inner.has_state_snapshot_failure } - /// Sets the snapshot failure flag. - pub fn set_snapshot_failure(&mut self, has_snapshot_failure: bool) { - self.inner.has_snapshot_failure = has_snapshot_failure + /// Sets the state snapshot failure flag. + pub fn set_state_snapshot_failure(&mut self, has_state_snapshot_failure: bool) { + self.inner.has_state_snapshot_failure = has_state_snapshot_failure } /// When creating or switching forks, we update the AccountInfo of the contract @@ -793,10 +806,10 @@ impl Backend { /// Note: in case there are any cheatcodes executed that modify the environment, this will /// update the given `env` with the new values. #[instrument(name = "inspect", level = "debug", skip_all)] - pub fn inspect<'a, I: InspectorExt<&'a mut Self>>( - &'a mut self, + pub fn inspect( + &mut self, env: &mut EnvWithHandlerCfg, - inspector: I, + inspector: &mut I, ) -> eyre::Result { self.initialize(env); let mut evm = crate::utils::new_evm_with_inspector(self, env.clone(), inspector); @@ -924,7 +937,7 @@ impl Backend { let fork = self.inner.get_fork_by_id_mut(id)?; let full_block = fork.db.db.get_full_block(env.block.number.to::())?; - for tx in full_block.transactions.clone().into_transactions() { + for tx in full_block.inner.transactions.into_transactions() { // System transactions such as on L2s don't contain any pricing info so we skip them // otherwise this would cause reverts if is_known_system_sender(tx.from) || @@ -941,7 +954,7 @@ impl Backend { trace!(tx=?tx.hash, "committing transaction"); commit_transaction( - tx, + &tx.inner, env.clone(), journaled_state, fork, @@ -971,9 +984,9 @@ impl DatabaseExt for Backend { Ok(ForkInfo { fork_type, fork_env }) } - fn snapshot(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { + fn snapshot_state(&mut self, journaled_state: &JournaledState, env: &Env) -> U256 { trace!("create snapshot"); - let id = self.inner.snapshots.insert(BackendSnapshot::new( + let id = self.inner.state_snapshots.insert(BackendStateSnapshot::new( self.create_db_snapshot(), journaled_state.clone(), env.clone(), @@ -982,18 +995,18 @@ impl DatabaseExt for Backend { id } - fn revert( + fn revert_state( &mut self, id: U256, current_state: &JournaledState, current: &mut Env, - action: RevertSnapshotAction, + action: RevertStateSnapshotAction, ) -> Option { trace!(?id, "revert snapshot"); - if let Some(mut snapshot) = self.inner.snapshots.remove_at(id) { + if let Some(mut snapshot) = self.inner.state_snapshots.remove_at(id) { // Re-insert snapshot to persist it if action.is_keep() { - self.inner.snapshots.insert_at(snapshot.clone(), id); + self.inner.state_snapshots.insert_at(snapshot.clone(), id); } // https://github.com/foundry-rs/foundry/issues/3055 @@ -1003,14 +1016,14 @@ impl DatabaseExt for Backend { if let Some(account) = current_state.state.get(&CHEATCODE_ADDRESS) { if let Some(slot) = account.storage.get(&GLOBAL_FAIL_SLOT) { if !slot.present_value.is_zero() { - self.set_snapshot_failure(true); + self.set_state_snapshot_failure(true); } } } // merge additional logs snapshot.merge(current_state); - let BackendSnapshot { db, mut journaled_state, env } = snapshot; + let BackendStateSnapshot { db, mut journaled_state, env } = snapshot; match db { BackendDatabaseSnapshot::InMemory(mem_db) => { self.mem_db = mem_db; @@ -1033,7 +1046,7 @@ impl DatabaseExt for Backend { } caller_account.into() }); - self.inner.revert_snapshot(id, fork_id, idx, *fork); + self.inner.revert_state_snapshot(id, fork_id, idx, *fork); self.active_fork_ids = Some((id, idx)) } } @@ -1048,12 +1061,12 @@ impl DatabaseExt for Backend { } } - fn delete_snapshot(&mut self, id: U256) -> bool { - self.inner.snapshots.remove_at(id).is_some() + fn delete_state_snapshot(&mut self, id: U256) -> bool { + self.inner.state_snapshots.remove_at(id).is_some() } - fn delete_snapshots(&mut self) { - self.inner.snapshots.clear() + fn delete_state_snapshots(&mut self) { + self.inner.state_snapshots.clear() } fn create_fork(&mut self, create_fork: CreateFork) -> eyre::Result { @@ -1308,9 +1321,9 @@ impl DatabaseExt for Backend { &mut self, maybe_id: Option, transaction: B256, - env: &mut Env, + mut env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()> { trace!(?maybe_id, ?transaction, "execute transaction"); let persistent_accounts = self.inner.persistent_accounts.clone(); @@ -1322,17 +1335,20 @@ impl DatabaseExt for Backend { fork.db.db.get_transaction(transaction)? }; - // This is a bit ambiguous because the user wants to transact an arbitrary transaction in the current context, but we're assuming the user wants to transact the transaction as it was mined. Usually this is used in a combination of a fork at the transaction's parent transaction in the block and then the transaction is transacted: - // So we modify the env to match the transaction's block + // This is a bit ambiguous because the user wants to transact an arbitrary transaction in + // the current context, but we're assuming the user wants to transact the transaction as it + // was mined. Usually this is used in a combination of a fork at the transaction's parent + // transaction in the block and then the transaction is transacted: + // + // So we modify the env to match the transaction's block. let (_fork_block, block) = self.get_block_number_and_block_for_transaction(id, transaction)?; - let mut env = env.clone(); update_env_block(&mut env, &block); let env = self.env_with_handler_cfg(env); let fork = self.inner.get_fork_by_id_mut(id)?; commit_transaction( - tx, + &tx, env, journaled_state, fork, @@ -1344,36 +1360,21 @@ impl DatabaseExt for Backend { fn transact_from_tx( &mut self, - tx: TransactionRequest, - env: &Env, + tx: &TransactionRequest, + mut env: Env, journaled_state: &mut JournaledState, - inspector: &mut dyn InspectorExt, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()> { trace!(?tx, "execute signed transaction"); - let mut env = env.clone(); - - env.tx.caller = - tx.from.ok_or_else(|| eyre::eyre!("transact_from_tx: No `from` field found"))?; - env.tx.gas_limit = - tx.gas.ok_or_else(|| eyre::eyre!("transact_from_tx: No `gas` field found"))? as u64; - env.tx.gas_price = U256::from(tx.gas_price.or(tx.max_fee_per_gas).unwrap_or_default()); - env.tx.gas_priority_fee = tx.max_priority_fee_per_gas.map(U256::from); - env.tx.nonce = tx.nonce; - env.tx.access_list = tx.access_list.clone().unwrap_or_default().0.into_iter().collect(); - env.tx.value = - tx.value.ok_or_else(|| eyre::eyre!("transact_from_tx: No `value` field found"))?; - env.tx.data = tx.input.into_input().unwrap_or_default(); - env.tx.transact_to = - tx.to.ok_or_else(|| eyre::eyre!("transact_from_tx: No `to` field found"))?; - env.tx.chain_id = tx.chain_id; - self.commit(journaled_state.state.clone()); let res = { - let db = self.clone(); + configure_tx_req_env(&mut env, tx)?; let env = self.env_with_handler_cfg(env); - let mut evm = new_evm_with_inspector(db, env, inspector); + + let mut db = self.clone(); + let mut evm = new_evm_with_inspector(&mut db, env, inspector); evm.context.evm.journaled_state.depth = journaled_state.depth + 1; evm.transact()? }; @@ -1464,44 +1465,59 @@ impl DatabaseExt for Backend { ) -> Result<(), BackendError> { // Loop through all of the allocs defined in the map and commit them to the journal. for (addr, acc) in allocs.iter() { - // Fetch the account from the journaled state. Will create a new account if it does - // not already exist. - let mut state_acc = journaled_state.load_account(*addr, self)?; - - // Set the account's bytecode and code hash, if the `bytecode` field is present. - if let Some(bytecode) = acc.code.as_ref() { - state_acc.info.code_hash = keccak256(bytecode); - let bytecode = Bytecode::new_raw(bytecode.0.clone().into()); - state_acc.info.code = Some(bytecode); - } + self.clone_account(acc, addr, journaled_state)?; + } - // Set the account's storage, if the `storage` field is present. - if let Some(storage) = acc.storage.as_ref() { - state_acc.storage = storage - .iter() - .map(|(slot, value)| { - let slot = U256::from_be_bytes(slot.0); - ( - slot, - EvmStorageSlot::new_changed( - state_acc - .storage - .get(&slot) - .map(|s| s.present_value) - .unwrap_or_default(), - U256::from_be_bytes(value.0), - ), - ) - }) - .collect(); - } - // Set the account's nonce and balance. - state_acc.info.nonce = acc.nonce.unwrap_or_default(); - state_acc.info.balance = acc.balance; + Ok(()) + } - // Touch the account to ensure the loaded information persists if called in `setUp`. - journaled_state.touch(addr); + /// Copies bytecode, storage, nonce and balance from the given genesis account to the target + /// address. + /// + /// Returns [Ok] if data was successfully inserted into the journal, [Err] otherwise. + fn clone_account( + &mut self, + source: &GenesisAccount, + target: &Address, + journaled_state: &mut JournaledState, + ) -> Result<(), BackendError> { + // Fetch the account from the journaled state. Will create a new account if it does + // not already exist. + let mut state_acc = journaled_state.load_account(*target, self)?; + + // Set the account's bytecode and code hash, if the `bytecode` field is present. + if let Some(bytecode) = source.code.as_ref() { + state_acc.info.code_hash = keccak256(bytecode); + let bytecode = Bytecode::new_raw(bytecode.0.clone().into()); + state_acc.info.code = Some(bytecode); + } + + // Set the account's storage, if the `storage` field is present. + if let Some(storage) = source.storage.as_ref() { + state_acc.storage = storage + .iter() + .map(|(slot, value)| { + let slot = U256::from_be_bytes(slot.0); + ( + slot, + EvmStorageSlot::new_changed( + state_acc + .storage + .get(&slot) + .map(|s| s.present_value) + .unwrap_or_default(), + U256::from_be_bytes(value.0), + ), + ) + }) + .collect(); } + // Set the account's nonce and balance. + state_acc.info.nonce = source.nonce.unwrap_or_default(); + state_acc.info.balance = source.balance; + + // Touch the account to ensure the loaded information persists if called in `setUp`. + journaled_state.touch(target); Ok(()) } @@ -1686,8 +1702,8 @@ pub struct BackendInner { /// Holds all created fork databases // Note: data is stored in an `Option` so we can remove it without reshuffling pub forks: Vec>, - /// Contains snapshots made at a certain point - pub snapshots: Snapshots>, + /// Contains state snapshots made at a certain point + pub state_snapshots: StateSnapshots>, /// Tracks whether there was a failure in a snapshot that was reverted /// /// The Test contract contains a bool variable that is set to true when an `assert` function @@ -1697,7 +1713,7 @@ pub struct BackendInner { /// reverted we get the _current_ `revm::JournaledState` which contains the state that we can /// check if the `_failed` variable is set, /// additionally - pub has_snapshot_failure: bool, + pub has_state_snapshot_failure: bool, /// Tracks the address of a Test contract /// /// This address can be used to inspect the state of the contract when a test is being @@ -1786,7 +1802,7 @@ impl BackendInner { } /// Reverts the entire fork database - pub fn revert_snapshot( + pub fn revert_state_snapshot( &mut self, id: LocalForkId, fork_id: ForkId, @@ -1889,8 +1905,8 @@ impl Default for BackendInner { issued_local_fork_ids: Default::default(), created_forks: Default::default(), forks: vec![], - snapshots: Default::default(), - has_snapshot_failure: false, + state_snapshots: Default::default(), + has_state_snapshot_failure: false, test_contract_address: None, caller: None, next_fork_id: Default::default(), @@ -2051,26 +2067,26 @@ fn update_env_block(env: &mut Env, block: &Block) { } /// Executes the given transaction and commits state changes to the database _and_ the journaled -/// state, with an optional inspector -fn commit_transaction>( - tx: WithOtherFields, +/// state, with an inspector. +fn commit_transaction( + tx: &Transaction, mut env: EnvWithHandlerCfg, journaled_state: &mut JournaledState, fork: &mut Fork, fork_id: &ForkId, persistent_accounts: &HashSet

, - inspector: I, + inspector: &mut dyn InspectorExt, ) -> eyre::Result<()> { - configure_tx_env(&mut env.env, &tx.inner); + configure_tx_env(&mut env.env, tx); let now = Instant::now(); let res = { let fork = fork.clone(); let journaled_state = journaled_state.clone(); let depth = journaled_state.depth; - let db = Backend::new_with_fork(fork_id, fork, journaled_state); + let mut db = Backend::new_with_fork(fork_id, fork, journaled_state); - let mut evm = crate::utils::new_evm_with_inspector(db, env, inspector); + let mut evm = crate::utils::new_evm_with_inspector(&mut db as _, env, inspector); // Adjust inner EVM depth to ensure that inspectors receive accurate data. evm.context.evm.inner.journaled_state.depth = depth + 1; evm.transact().wrap_err("backend: failed committing transaction")? @@ -2118,6 +2134,7 @@ fn apply_state_changeset( } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use crate::{backend::Backend, fork::CreateFork, opts::EvmOpts}; use alloy_primitives::{Address, U256}; diff --git a/crates/evm/core/src/backend/snapshot.rs b/crates/evm/core/src/backend/snapshot.rs index f8961c7a0..36c4657c2 100644 --- a/crates/evm/core/src/backend/snapshot.rs +++ b/crates/evm/core/src/backend/snapshot.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{map::AddressHashMap, B256, U256}; use revm::{ primitives::{AccountInfo, Env, HashMap}, JournaledState, @@ -8,14 +8,14 @@ use serde::{Deserialize, Serialize}; /// A minimal abstraction of a state at a certain point in time #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct StateSnapshot { - pub accounts: HashMap, - pub storage: HashMap>, + pub accounts: AddressHashMap, + pub storage: AddressHashMap>, pub block_hashes: HashMap, } -/// Represents a snapshot taken during evm execution +/// Represents a state snapshot taken during evm execution #[derive(Clone, Debug)] -pub struct BackendSnapshot { +pub struct BackendStateSnapshot { pub db: T, /// The journaled_state state at a specific point pub journaled_state: JournaledState, @@ -23,38 +23,38 @@ pub struct BackendSnapshot { pub env: Env, } -impl BackendSnapshot { - /// Takes a new snapshot +impl BackendStateSnapshot { + /// Takes a new state snapshot. pub fn new(db: T, journaled_state: JournaledState, env: Env) -> Self { Self { db, journaled_state, env } } - /// Called when this snapshot is reverted. + /// Called when this state snapshot is reverted. /// /// Since we want to keep all additional logs that were emitted since the snapshot was taken /// we'll merge additional logs into the snapshot's `revm::JournaledState`. Additional logs are /// those logs that are missing in the snapshot's journaled_state, since the current /// journaled_state includes the same logs, we can simply replace use that See also - /// `DatabaseExt::revert` + /// `DatabaseExt::revert`. pub fn merge(&mut self, current: &JournaledState) { self.journaled_state.logs.clone_from(¤t.logs); } } -/// What to do when reverting a snapshot +/// What to do when reverting a state snapshot. /// -/// Whether to remove the snapshot or keep it +/// Whether to remove the state snapshot or keep it. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum RevertSnapshotAction { - /// Remove the snapshot after reverting +pub enum RevertStateSnapshotAction { + /// Remove the state snapshot after reverting. #[default] RevertRemove, - /// Keep the snapshot after reverting + /// Keep the state snapshot after reverting. RevertKeep, } -impl RevertSnapshotAction { - /// Returns `true` if the action is to keep the snapshot +impl RevertStateSnapshotAction { + /// Returns `true` if the action is to keep the state snapshot. pub fn is_keep(&self) -> bool { matches!(self, Self::RevertKeep) } diff --git a/crates/evm/core/src/buffer.rs b/crates/evm/core/src/buffer.rs new file mode 100644 index 000000000..1db7420d7 --- /dev/null +++ b/crates/evm/core/src/buffer.rs @@ -0,0 +1,117 @@ +use alloy_primitives::U256; +use revm::interpreter::opcode; + +/// Used to keep track of which buffer is currently active to be drawn by the debugger. +#[derive(Debug, PartialEq)] +pub enum BufferKind { + Memory, + Calldata, + Returndata, +} + +impl BufferKind { + /// Helper to cycle through the active buffers. + pub fn next(&self) -> Self { + match self { + Self::Memory => Self::Calldata, + Self::Calldata => Self::Returndata, + Self::Returndata => Self::Memory, + } + } + + /// Helper to format the title of the active buffer pane + pub fn title(&self, size: usize) -> String { + match self { + Self::Memory => format!("Memory (max expansion: {size} bytes)"), + Self::Calldata => format!("Calldata (size: {size} bytes)"), + Self::Returndata => format!("Returndata (size: {size} bytes)"), + } + } +} + +/// Container for buffer access information. +pub struct BufferAccess { + pub offset: usize, + pub len: usize, +} + +/// Container for read and write buffer access information. +pub struct BufferAccesses { + /// The read buffer kind and access information. + pub read: Option<(BufferKind, BufferAccess)>, + /// The only mutable buffer is the memory buffer, so don't store the buffer kind. + pub write: Option, +} + +/// A utility function to get the buffer access. +/// +/// The memory_access variable stores the index on the stack that indicates the buffer +/// offset/len accessed by the given opcode: +/// (read buffer, buffer read offset, buffer read len, write memory offset, write memory len) +/// \>= 1: the stack index +/// 0: no memory access +/// -1: a fixed len of 32 bytes +/// -2: a fixed len of 1 byte +/// +/// The return value is a tuple about accessed buffer region by the given opcode: +/// (read buffer, buffer read offset, buffer read len, write memory offset, write memory len) +pub fn get_buffer_accesses(op: u8, stack: &[U256]) -> Option { + let buffer_access = match op { + opcode::KECCAK256 | opcode::RETURN | opcode::REVERT => { + (Some((BufferKind::Memory, 1, 2)), None) + } + opcode::CALLDATACOPY => (Some((BufferKind::Calldata, 2, 3)), Some((1, 3))), + opcode::RETURNDATACOPY => (Some((BufferKind::Returndata, 2, 3)), Some((1, 3))), + opcode::CALLDATALOAD => (Some((BufferKind::Calldata, 1, -1)), None), + opcode::CODECOPY => (None, Some((1, 3))), + opcode::EXTCODECOPY => (None, Some((2, 4))), + opcode::MLOAD => (Some((BufferKind::Memory, 1, -1)), None), + opcode::MSTORE => (None, Some((1, -1))), + opcode::MSTORE8 => (None, Some((1, -2))), + opcode::LOG0 | opcode::LOG1 | opcode::LOG2 | opcode::LOG3 | opcode::LOG4 => { + (Some((BufferKind::Memory, 1, 2)), None) + } + opcode::CREATE | opcode::CREATE2 => (Some((BufferKind::Memory, 2, 3)), None), + opcode::CALL | opcode::CALLCODE => (Some((BufferKind::Memory, 4, 5)), None), + opcode::DELEGATECALL | opcode::STATICCALL => (Some((BufferKind::Memory, 3, 4)), None), + opcode::MCOPY => (Some((BufferKind::Memory, 2, 3)), Some((1, 3))), + opcode::RETURNDATALOAD => (Some((BufferKind::Returndata, 1, -1)), None), + opcode::EOFCREATE => (Some((BufferKind::Memory, 3, 4)), None), + opcode::RETURNCONTRACT => (Some((BufferKind::Memory, 1, 2)), None), + opcode::DATACOPY => (None, Some((1, 3))), + opcode::EXTCALL | opcode::EXTSTATICCALL | opcode::EXTDELEGATECALL => { + (Some((BufferKind::Memory, 2, 3)), None) + } + _ => Default::default(), + }; + + let stack_len = stack.len(); + let get_size = |stack_index| match stack_index { + -2 => Some(1), + -1 => Some(32), + 0 => None, + 1.. => { + if (stack_index as usize) <= stack_len { + Some(stack[stack_len - stack_index as usize].saturating_to()) + } else { + None + } + } + _ => panic!("invalid stack index"), + }; + + if buffer_access.0.is_some() || buffer_access.1.is_some() { + let (read, write) = buffer_access; + let read_access = read.and_then(|b| { + let (buffer, offset, len) = b; + Some((buffer, BufferAccess { offset: get_size(offset)?, len: get_size(len)? })) + }); + let write_access = write.and_then(|b| { + let (offset, len) = b; + Some(BufferAccess { offset: get_size(offset)?, len: get_size(len)? }) + }); + Some(BufferAccesses { read: read_access, write: write_access }) + } else { + None + } +} diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 19aeaedf2..368693b32 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -2,14 +2,13 @@ use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::{Error, JsonAbi}; -use alloy_primitives::{hex, Log, Selector}; -use alloy_sol_types::{SolCall, SolError, SolEventInterface, SolInterface, SolValue}; +use alloy_primitives::{hex, map::HashMap, Log, Selector}; +use alloy_sol_types::{SolEventInterface, SolInterface, SolValue}; use foundry_cheatcodes_spec::Vm; use foundry_common::SELECTOR_LEN; use foundry_evm_abi::Console; use itertools::Itertools; use revm::interpreter::InstructionResult; -use rustc_hash::FxHashMap; use std::{fmt, sync::OnceLock}; /// A skip reason. @@ -61,7 +60,7 @@ pub fn decode_console_log(log: &Log) -> Option { #[derive(Clone, Debug, Default)] pub struct RevertDecoder { /// The custom errors to use for decoding. - pub errors: FxHashMap>, + pub errors: HashMap>, } impl Default for &RevertDecoder { @@ -140,7 +139,7 @@ impl RevertDecoder { /// /// See [`decode`](Self::decode) for more information. pub fn maybe_decode(&self, err: &[u8], status: Option) -> Option { - if err.len() < SELECTOR_LEN { + let Some((selector, data)) = err.split_first_chunk::() else { if let Some(status) = status { if !status.is_ok() { return Some(format!("EvmError: {status:?}")); @@ -151,59 +150,17 @@ impl RevertDecoder { } else { Some(format!("custom error bytes {}", hex::encode_prefixed(err))) }; - } + }; if let Some(reason) = SkipReason::decode(err) { return Some(reason.to_string()); } - // Solidity's `Error(string)` or `Panic(uint256)` - if let Ok(e) = alloy_sol_types::GenericContractError::abi_decode(err, false) { + // Solidity's `Error(string)` or `Panic(uint256)`, or `Vm`'s custom errors. + if let Ok(e) = alloy_sol_types::ContractError::::abi_decode(err, false) { return Some(e.to_string()); } - let (selector, data) = err.split_at(SELECTOR_LEN); - let selector: &[u8; 4] = selector.try_into().unwrap(); - - match *selector { - // `CheatcodeError(string)` - Vm::CheatcodeError::SELECTOR => { - let e = Vm::CheatcodeError::abi_decode_raw(data, false).ok()?; - return Some(e.message); - } - // `expectRevert(bytes)` - Vm::expectRevert_2Call::SELECTOR => { - let e = Vm::expectRevert_2Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - // `expectRevert(bytes,address)` - Vm::expectRevert_5Call::SELECTOR => { - let e = Vm::expectRevert_5Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - // `expectRevert(bytes4)` - Vm::expectRevert_1Call::SELECTOR => { - let e = Vm::expectRevert_1Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - // `expectRevert(bytes4,address)` - Vm::expectRevert_4Call::SELECTOR => { - let e = Vm::expectRevert_4Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - // `expectPartialRevert(bytes4)` - Vm::expectPartialRevert_0Call::SELECTOR => { - let e = Vm::expectPartialRevert_0Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - // `expectPartialRevert(bytes4,address)` - Vm::expectPartialRevert_1Call::SELECTOR => { - let e = Vm::expectPartialRevert_1Call::abi_decode_raw(data, false).ok()?; - return self.maybe_decode(&e.revertData[..], status); - } - _ => {} - } - // Custom errors. if let Some(errors) = self.errors.get(selector) { for error in errors { diff --git a/crates/evm/core/src/fork/database.rs b/crates/evm/core/src/fork/database.rs index de6b2a6b9..29dccb9c7 100644 --- a/crates/evm/core/src/fork/database.rs +++ b/crates/evm/core/src/fork/database.rs @@ -1,16 +1,16 @@ //! A revm database that forks off a remote client use crate::{ - backend::{RevertSnapshotAction, StateSnapshot}, - snapshot::Snapshots, + backend::{RevertStateSnapshotAction, StateSnapshot}, + state_snapshot::StateSnapshots, }; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{map::HashMap, Address, B256, U256}; use alloy_rpc_types::BlockId; use foundry_fork_db::{BlockchainDb, DatabaseError, SharedBackend}; use parking_lot::Mutex; use revm::{ db::{CacheDB, DatabaseRef}, - primitives::{Account, AccountInfo, Bytecode, HashMap as Map}, + primitives::{Account, AccountInfo, Bytecode}, Database, DatabaseCommit, }; use std::sync::Arc; @@ -23,22 +23,22 @@ use std::sync::Arc; /// `backend` will also write (missing) data to the `db` in the background #[derive(Clone, Debug)] pub struct ForkedDatabase { - /// responsible for fetching missing data + /// Responsible for fetching missing data. /// - /// This is responsible for getting data + /// This is responsible for getting data. 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 + /// - reads from the `SharedBackend as DatabaseRef` writes to the internal cache storage. cache_db: CacheDB, - /// Contains all the data already fetched + /// Contains all the data already fetched. /// - /// This exclusively stores the _unchanged_ remote client state + /// This exclusively stores the _unchanged_ remote client state. db: BlockchainDb, - /// holds the snapshot state of a blockchain - snapshots: Arc>>, + /// Holds the state snapshots of a blockchain. + state_snapshots: Arc>>, } impl ForkedDatabase { @@ -48,7 +48,7 @@ impl ForkedDatabase { cache_db: CacheDB::new(backend.clone()), backend, db, - snapshots: Arc::new(Mutex::new(Default::default())), + state_snapshots: Arc::new(Mutex::new(Default::default())), } } @@ -60,8 +60,8 @@ impl ForkedDatabase { &mut self.cache_db } - pub fn snapshots(&self) -> &Arc>> { - &self.snapshots + pub fn state_snapshots(&self) -> &Arc>> { + &self.state_snapshots } /// Reset the fork to a fresh forked state, and optionally update the fork config @@ -92,35 +92,35 @@ impl ForkedDatabase { &self.db } - pub fn create_snapshot(&self) -> ForkDbSnapshot { + pub fn create_state_snapshot(&self) -> ForkDbStateSnapshot { let db = self.db.db(); - let snapshot = StateSnapshot { + let state_snapshot = StateSnapshot { accounts: db.accounts.read().clone(), storage: db.storage.read().clone(), block_hashes: db.block_hashes.read().clone(), }; - ForkDbSnapshot { local: self.cache_db.clone(), snapshot } + ForkDbStateSnapshot { local: self.cache_db.clone(), state_snapshot } } - pub fn insert_snapshot(&self) -> U256 { - let snapshot = self.create_snapshot(); - let mut snapshots = self.snapshots().lock(); - let id = snapshots.insert(snapshot); + pub fn insert_state_snapshot(&self) -> U256 { + let state_snapshot = self.create_state_snapshot(); + let mut state_snapshots = self.state_snapshots().lock(); + let id = state_snapshots.insert(state_snapshot); trace!(target: "backend::forkdb", "Created new snapshot {}", id); id } /// Removes the snapshot from the tracked snapshot and sets it as the current state - pub fn revert_snapshot(&mut self, id: U256, action: RevertSnapshotAction) -> bool { - let snapshot = { self.snapshots().lock().remove_at(id) }; - if let Some(snapshot) = snapshot { + pub fn revert_state_snapshot(&mut self, id: U256, action: RevertStateSnapshotAction) -> bool { + let state_snapshot = { self.state_snapshots().lock().remove_at(id) }; + if let Some(state_snapshot) = state_snapshot { if action.is_keep() { - self.snapshots().lock().insert_at(snapshot.clone(), id); + self.state_snapshots().lock().insert_at(state_snapshot.clone(), id); } - let ForkDbSnapshot { + let ForkDbStateSnapshot { local, - snapshot: StateSnapshot { accounts, storage, block_hashes }, - } = snapshot; + state_snapshot: StateSnapshot { accounts, storage, block_hashes }, + } = state_snapshot; let db = self.inner().db(); { let mut accounts_lock = db.accounts.write(); @@ -193,7 +193,7 @@ impl DatabaseRef for ForkedDatabase { } impl DatabaseCommit for ForkedDatabase { - fn commit(&mut self, changes: Map) { + fn commit(&mut self, changes: HashMap) { self.database_mut().commit(changes) } } @@ -202,12 +202,12 @@ impl DatabaseCommit for ForkedDatabase { /// /// This mimics `revm::CacheDB` #[derive(Clone, Debug)] -pub struct ForkDbSnapshot { +pub struct ForkDbStateSnapshot { pub local: CacheDB, - pub snapshot: StateSnapshot, + pub state_snapshot: StateSnapshot, } -impl ForkDbSnapshot { +impl ForkDbStateSnapshot { fn get_storage(&self, address: Address, index: U256) -> Option { self.local.accounts.get(&address).and_then(|account| account.storage.get(&index)).copied() } @@ -216,14 +216,14 @@ impl ForkDbSnapshot { // 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 ForkDbSnapshot { +impl DatabaseRef for ForkDbStateSnapshot { type Error = DatabaseError; fn basic_ref(&self, address: Address) -> Result, Self::Error> { match self.local.accounts.get(&address) { Some(account) => Ok(Some(account.info.clone())), None => { - let mut acc = self.snapshot.accounts.get(&address).cloned(); + let mut acc = self.state_snapshot.accounts.get(&address).cloned(); if acc.is_none() { acc = self.local.basic_ref(address)?; @@ -254,7 +254,7 @@ impl DatabaseRef for ForkDbSnapshot { } fn block_hash_ref(&self, number: u64) -> Result { - match self.snapshot.block_hashes.get(&U256::from(number)).copied() { + match self.state_snapshot.block_hashes.get(&U256::from(number)).copied() { None => self.local.block_hash_ref(number), Some(block_hash) => Ok(block_hash), } @@ -262,6 +262,7 @@ impl DatabaseRef for ForkDbSnapshot { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; use crate::backend::BlockchainDbMeta; diff --git a/crates/evm/core/src/fork/multi.rs b/crates/evm/core/src/fork/multi.rs index a39b543ea..14fa59aaf 100644 --- a/crates/evm/core/src/fork/multi.rs +++ b/crates/evm/core/src/fork/multi.rs @@ -4,7 +4,7 @@ //! concurrently active pairs at once. use super::CreateFork; -use alloy_primitives::U256; +use alloy_primitives::{map::HashMap, U256}; use alloy_provider::network::{BlockResponse, HeaderResponse}; use alloy_transport::layers::RetryBackoffService; use foundry_common::provider::{ @@ -20,7 +20,6 @@ use futures::{ }; use revm::primitives::Env; use std::{ - collections::HashMap, fmt::{self, Write}, pin::Pin, sync::{ diff --git a/crates/evm/core/src/ic.rs b/crates/evm/core/src/ic.rs index acb9cc50e..2711f8933 100644 --- a/crates/evm/core/src/ic.rs +++ b/crates/evm/core/src/ic.rs @@ -1,12 +1,16 @@ -use revm::interpreter::opcode::{PUSH0, PUSH1, PUSH32}; -use rustc_hash::FxHashMap; +use alloy_primitives::map::HashMap; +use revm::interpreter::{ + opcode::{PUSH0, PUSH1, PUSH32}, + OpCode, +}; +use revm_inspectors::opcode::immediate_size; /// Maps from program counter to instruction counter. /// /// Inverse of [`IcPcMap`]. #[derive(Debug, Clone)] pub struct PcIcMap { - pub inner: FxHashMap, + pub inner: HashMap, } impl PcIcMap { @@ -35,7 +39,7 @@ impl PcIcMap { /// /// Inverse of [`PcIcMap`]. pub struct IcPcMap { - pub inner: FxHashMap, + pub inner: HashMap, } impl IcPcMap { @@ -60,8 +64,8 @@ impl IcPcMap { } } -fn make_map(code: &[u8]) -> FxHashMap { - let mut map = FxHashMap::default(); +fn make_map(code: &[u8]) -> HashMap { + let mut map = HashMap::default(); let mut pc = 0; let mut cumulative_push_size = 0; @@ -84,3 +88,30 @@ fn make_map(code: &[u8]) -> FxHashMap { } map } + +/// Represents a single instruction consisting of the opcode and its immediate data. +pub struct Instruction<'a> { + /// OpCode, if it could be decoded. + pub op: Option, + /// Immediate data following the opcode. + pub immediate: &'a [u8], + /// Program counter of the opcode. + pub pc: usize, +} + +/// Decodes raw opcode bytes into [`Instruction`]s. +pub fn decode_instructions(code: &[u8]) -> Vec> { + let mut pc = 0; + let mut steps = Vec::new(); + + while pc < code.len() { + let op = OpCode::new(code[pc]); + let immediate_size = op.map(|op| immediate_size(op, &code[pc + 1..])).unwrap_or(0) as usize; + + steps.push(Instruction { op, pc, immediate: &code[pc + 1..pc + 1 + immediate_size] }); + + pc += 1 + immediate_size; + } + + steps +} diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index ca316b0fb..34d43b152 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -6,8 +6,9 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] use auto_impl::auto_impl; +use backend::DatabaseExt; use foundry_zksync_core::Call; -use revm::{inspectors::NoOpInspector, interpreter::CreateInputs, Database, EvmContext, Inspector}; +use revm::{inspectors::NoOpInspector, interpreter::CreateInputs, EvmContext, Inspector}; use revm_inspectors::access_list::AccessListInspector; #[macro_use] @@ -21,41 +22,49 @@ pub mod abi { mod ic; pub mod backend; +pub mod buffer; pub mod constants; pub mod decode; pub mod fork; pub mod opcodes; pub mod opts; pub mod precompiles; -pub mod snapshot; +pub mod state_snapshot; pub mod utils; /// An extension trait that allows us to add additional hooks to Inspector for later use in /// handlers. #[auto_impl(&mut, Box)] -pub trait InspectorExt: Inspector { +pub trait InspectorExt: for<'a> Inspector<&'a mut dyn DatabaseExt> { /// Determines whether the `DEFAULT_CREATE2_DEPLOYER` should be used for a CREATE2 frame. /// /// If this function returns true, we'll replace CREATE2 frame with a CALL frame to CREATE2 /// factory. fn should_use_create2_factory( &mut self, - _context: &mut EvmContext, + _context: &mut EvmContext<&mut dyn DatabaseExt>, _inputs: &mut CreateInputs, ) -> bool { false } - // Simulates `console.log` invocation. + /// Simulates `console.log` invocation. fn console_log(&mut self, _input: String) {} - // Appends provided zksync traces. - fn trace_zksync(&mut self, _context: &mut EvmContext, _call_traces: Vec) {} - + /// Returns `true` if the current network is Alphanet. fn is_alphanet(&self) -> bool { false } + + /// Appends provided zksync traces. + fn trace_zksync( + &mut self, + _context: &mut EvmContext<&mut dyn DatabaseExt>, + _call_traces: Vec, + ) { + } } -impl InspectorExt for NoOpInspector {} -impl InspectorExt for AccessListInspector {} +impl InspectorExt for NoOpInspector {} + +impl InspectorExt for AccessListInspector {} diff --git a/crates/evm/core/src/snapshot.rs b/crates/evm/core/src/snapshot.rs deleted file mode 100644 index 423f853ee..000000000 --- a/crates/evm/core/src/snapshot.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! support for snapshotting different states - -use alloy_primitives::U256; -use std::{collections::HashMap, ops::Add}; - -/// Represents all snapshots -#[derive(Clone, Debug)] -pub struct Snapshots { - id: U256, - snapshots: HashMap, -} - -impl Snapshots { - fn next_id(&mut self) -> U256 { - let id = self.id; - self.id = id.saturating_add(U256::from(1)); - id - } - - /// Returns the snapshot with the given id `id` - pub fn get(&self, id: U256) -> Option<&T> { - self.snapshots.get(&id) - } - - /// Removes the snapshot with the given `id`. - /// - /// This will also remove any snapshots taken after the snapshot with the `id`. e.g.: reverting - /// to id 1 will delete snapshots with ids 1, 2, 3, etc.) - pub fn remove(&mut self, id: U256) -> Option { - let snapshot = self.snapshots.remove(&id); - - // revert all snapshots taken after the snapshot - let mut to_revert = id.add(U256::from(1)); - while to_revert < self.id { - self.snapshots.remove(&to_revert); - to_revert += U256::from(1); - } - - snapshot - } - - /// Removes all snapshots - pub fn clear(&mut self) { - self.snapshots.clear(); - } - - /// Removes the snapshot with the given `id`. - /// - /// Does not remove snapshots after it. - pub fn remove_at(&mut self, id: U256) -> Option { - self.snapshots.remove(&id) - } - - /// Inserts the new snapshot and returns the id - pub fn insert(&mut self, snapshot: T) -> U256 { - let id = self.next_id(); - self.snapshots.insert(id, snapshot); - id - } - - /// Inserts the new snapshot at the given `id`. - /// - /// Does not auto-increment the next `id`. - pub fn insert_at(&mut self, snapshot: T, id: U256) -> U256 { - self.snapshots.insert(id, snapshot); - id - } -} - -impl Default for Snapshots { - fn default() -> Self { - Self { id: U256::ZERO, snapshots: HashMap::new() } - } -} diff --git a/crates/evm/core/src/state_snapshot.rs b/crates/evm/core/src/state_snapshot.rs new file mode 100644 index 000000000..3be1172ad --- /dev/null +++ b/crates/evm/core/src/state_snapshot.rs @@ -0,0 +1,74 @@ +//! Support for snapshotting different states + +use alloy_primitives::{map::HashMap, U256}; +use std::ops::Add; + +/// Represents all state snapshots +#[derive(Clone, Debug)] +pub struct StateSnapshots { + id: U256, + state_snapshots: HashMap, +} + +impl StateSnapshots { + fn next_id(&mut self) -> U256 { + let id = self.id; + self.id = id.saturating_add(U256::from(1)); + id + } + + /// Returns the state snapshot with the given id `id` + pub fn get(&self, id: U256) -> Option<&T> { + self.state_snapshots.get(&id) + } + + /// Removes the state snapshot with the given `id`. + /// + /// This will also remove any state snapshots taken after the state snapshot with the `id`. + /// e.g.: reverting to id 1 will delete snapshots with ids 1, 2, 3, etc.) + pub fn remove(&mut self, id: U256) -> Option { + let snapshot_state = self.state_snapshots.remove(&id); + + // Revert all state snapshots taken after the state snapshot with the `id` + let mut to_revert = id.add(U256::from(1)); + while to_revert < self.id { + self.state_snapshots.remove(&to_revert); + to_revert += U256::from(1); + } + + snapshot_state + } + + /// Removes all state snapshots. + pub fn clear(&mut self) { + self.state_snapshots.clear(); + } + + /// Removes the state snapshot with the given `id`. + /// + /// Does not remove state snapshots after it. + pub fn remove_at(&mut self, id: U256) -> Option { + self.state_snapshots.remove(&id) + } + + /// Inserts the new state snapshot and returns the id. + pub fn insert(&mut self, state_snapshot: T) -> U256 { + let id = self.next_id(); + self.state_snapshots.insert(id, state_snapshot); + id + } + + /// Inserts the new state snapshot at the given `id`. + /// + /// Does not auto-increment the next `id`. + pub fn insert_at(&mut self, state_snapshot: T, id: U256) -> U256 { + self.state_snapshots.insert(id, state_snapshot); + id + } +} + +impl Default for StateSnapshots { + fn default() -> Self { + Self { id: U256::ZERO, state_snapshots: HashMap::default() } + } +} diff --git a/crates/evm/core/src/utils.rs b/crates/evm/core/src/utils.rs index f39c65964..b3a50fe1f 100644 --- a/crates/evm/core/src/utils.rs +++ b/crates/evm/core/src/utils.rs @@ -1,16 +1,19 @@ pub use crate::ic::*; -use crate::{constants::DEFAULT_CREATE2_DEPLOYER, precompiles::ALPHANET_P256, InspectorExt}; +use crate::{ + backend::DatabaseExt, constants::DEFAULT_CREATE2_DEPLOYER, precompiles::ALPHANET_P256, + InspectorExt, +}; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector, TxKind, U256}; use alloy_provider::{ network::{BlockResponse, HeaderResponse}, Network, }; -use alloy_rpc_types::Transaction; +use alloy_rpc_types::{Transaction, TransactionRequest}; use foundry_config::NamedChain; +use foundry_fork_db::DatabaseError; use foundry_zksync_core::DEFAULT_CREATE2_DEPLOYER_ZKSYNC; use revm::{ - db::WrapDatabaseRef, handler::register::EvmHandler, interpreter::{ return_ok, CallInputs, CallOutcome, CallScheme, CallValue, CreateInputs, CreateOutcome, @@ -82,17 +85,62 @@ pub fn get_function<'a>( .ok_or_else(|| eyre::eyre!("{contract_name} does not have the selector {selector}")) } -/// Configures the env for the transaction +/// Configures the env for the given RPC transaction. pub fn configure_tx_env(env: &mut revm::primitives::Env, tx: &Transaction) { - env.tx.caller = tx.from; - env.tx.gas_limit = tx.gas as u64; - env.tx.gas_price = U256::from(tx.gas_price.unwrap_or_default()); - env.tx.gas_priority_fee = tx.max_priority_fee_per_gas.map(U256::from); - env.tx.nonce = Some(tx.nonce); - env.tx.access_list = tx.access_list.clone().unwrap_or_default().0.into_iter().collect(); - env.tx.value = tx.value.to(); - env.tx.data = alloy_primitives::Bytes(tx.input.0.clone()); - env.tx.transact_to = tx.to.map(TxKind::Call).unwrap_or(TxKind::Create) + configure_tx_req_env(env, &tx.clone().into()).expect("cannot fail"); +} + +/// Configures the env for the given RPC transaction request. +pub fn configure_tx_req_env( + env: &mut revm::primitives::Env, + tx: &TransactionRequest, +) -> eyre::Result<()> { + let TransactionRequest { + nonce, + from, + to, + value, + gas_price, + gas, + max_fee_per_gas, + max_priority_fee_per_gas, + max_fee_per_blob_gas, + ref input, + chain_id, + ref blob_versioned_hashes, + ref access_list, + transaction_type: _, + ref authorization_list, + sidecar: _, + } = *tx; + + // If no `to` field then set create kind: https://eips.ethereum.org/EIPS/eip-2470#deployment-transaction + env.tx.transact_to = to.unwrap_or(TxKind::Create); + env.tx.caller = from.ok_or_else(|| eyre::eyre!("missing `from` field"))?; + env.tx.gas_limit = gas.ok_or_else(|| eyre::eyre!("missing `gas` field"))?; + env.tx.nonce = nonce; + env.tx.value = value.unwrap_or_default(); + env.tx.data = input.input().cloned().unwrap_or_default(); + env.tx.chain_id = chain_id; + + // Type 1, EIP-2930 + env.tx.access_list = access_list.clone().unwrap_or_default().0.into_iter().collect(); + + // Type 2, EIP-1559 + env.tx.gas_price = U256::from(gas_price.or(max_fee_per_gas).unwrap_or_default()); + env.tx.gas_priority_fee = max_priority_fee_per_gas.map(U256::from); + + // Type 3, EIP-4844 + env.tx.blob_hashes = blob_versioned_hashes.clone().unwrap_or_default(); + env.tx.max_fee_per_blob_gas = max_fee_per_blob_gas.map(U256::from); + + // Type 4, EIP-7702 + if let Some(authorization_list) = authorization_list { + env.tx.authorization_list = + Some(revm::primitives::AuthorizationList::Signed(authorization_list.clone())); + } + + Ok(()) } /// Get the gas used, accounting for refunds @@ -120,19 +168,19 @@ fn get_create2_factory_call_inputs(salt: U256, inputs: CreateInputs) -> CallInpu /// Used for routing certain CREATE2 invocations through [DEFAULT_CREATE2_DEPLOYER]. /// /// Overrides create hook with CALL frame if [InspectorExt::should_use_create2_factory] returns -/// true. Keeps track of overriden frames and handles outcome in the overriden insert_call_outcome +/// true. Keeps track of overridden frames and handles outcome in the overridden insert_call_outcome /// hook by inserting decoded address directly into interpreter. /// /// Should be installed after [revm::inspector_handle_register] and before any other registers. -pub fn create2_handler_register>( - handler: &mut EvmHandler<'_, I, DB>, +pub fn create2_handler_register( + handler: &mut EvmHandler<'_, I, &mut dyn DatabaseExt>, ) { let create2_overrides = Rc::>>::new(RefCell::new(Vec::new())); let create2_overrides_inner = create2_overrides.clone(); let old_handle = handler.execution.create.clone(); handler.execution.create = - Arc::new(move |ctx, mut inputs| -> Result> { + Arc::new(move |ctx, mut inputs| -> Result> { let CreateScheme::Create2 { salt } = inputs.scheme else { return old_handle(ctx, inputs); }; @@ -232,9 +280,7 @@ pub fn create2_handler_register>( } /// Adds Alphanet P256 precompile to the list of loaded precompiles. -pub fn alphanet_handler_register>( - handler: &mut EvmHandler<'_, I, DB>, -) { +pub fn alphanet_handler_register(handler: &mut EvmHandler<'_, EXT, DB>) { let prev = handler.pre_execution.load_precompiles.clone(); handler.pre_execution.load_precompiles = Arc::new(move || { let mut loaded_precompiles = prev(); @@ -246,15 +292,11 @@ pub fn alphanet_handler_register>( } /// Creates a new EVM with the given inspector. -pub fn new_evm_with_inspector<'a, DB, I>( - db: DB, +pub fn new_evm_with_inspector<'evm, 'i, 'db, I: InspectorExt + ?Sized>( + db: &'db mut dyn DatabaseExt, env: revm::primitives::EnvWithHandlerCfg, - inspector: I, -) -> revm::Evm<'a, I, DB> -where - DB: revm::Database, - I: InspectorExt, -{ + inspector: &'i mut I, +) -> revm::Evm<'evm, &'i mut I, &'db mut dyn DatabaseExt> { let revm::primitives::EnvWithHandlerCfg { env, handler_cfg } = env; // NOTE: We could use `revm::Evm::builder()` here, but on the current patch it has some @@ -282,27 +324,10 @@ where revm::Evm::new(context, handler) } -/// Creates a new EVM with the given inspector and wraps the database in a `WrapDatabaseRef`. -pub fn new_evm_with_inspector_ref<'a, DB, I>( - db: DB, - env: revm::primitives::EnvWithHandlerCfg, - inspector: I, -) -> revm::Evm<'a, I, WrapDatabaseRef> -where - DB: revm::DatabaseRef, - I: InspectorExt>, -{ - new_evm_with_inspector(WrapDatabaseRef(db), env, inspector) -} - -pub fn new_evm_with_existing_context<'a, DB, I>( - inner: revm::InnerEvmContext, - inspector: I, -) -> revm::Evm<'a, I, DB> -where - DB: revm::Database, - I: InspectorExt, -{ +pub fn new_evm_with_existing_context<'a>( + inner: revm::InnerEvmContext<&'a mut dyn DatabaseExt>, + inspector: &'a mut dyn InspectorExt, +) -> revm::Evm<'a, &'a mut dyn InspectorExt, &'a mut dyn DatabaseExt> { let handler_cfg = HandlerCfg::new(inner.spec_id()); let mut handler = revm::Handler::new(handler_cfg); @@ -316,24 +341,3 @@ where revm::Context::new(revm::EvmContext { inner, precompiles: Default::default() }, inspector); revm::Evm::new(context, handler) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_evm() { - let mut db = revm::db::EmptyDB::default(); - - let env = Box::::default(); - let spec = SpecId::LATEST; - let handler_cfg = revm::primitives::HandlerCfg::new(spec); - let cfg = revm::primitives::EnvWithHandlerCfg::new(env, handler_cfg); - - let mut inspector = revm::inspectors::NoOpInspector; - - let mut evm = new_evm_with_inspector(&mut db, cfg, &mut inspector); - let result = evm.transact().unwrap(); - assert!(result.result.is_success()); - } -} diff --git a/crates/evm/coverage/Cargo.toml b/crates/evm/coverage/Cargo.toml index 777c37d99..e38d33e2a 100644 --- a/crates/evm/coverage/Cargo.toml +++ b/crates/evm/coverage/Cargo.toml @@ -23,5 +23,4 @@ eyre.workspace = true revm.workspace = true semver.workspace = true tracing.workspace = true -rustc-hash.workspace = true rayon.workspace = true diff --git a/crates/evm/coverage/src/analysis.rs b/crates/evm/coverage/src/analysis.rs index daf676c2c..e5eda5262 100644 --- a/crates/evm/coverage/src/analysis.rs +++ b/crates/evm/coverage/src/analysis.rs @@ -1,8 +1,8 @@ use super::{CoverageItem, CoverageItemKind, SourceLocation}; +use alloy_primitives::map::HashMap; use foundry_common::TestFunctionExt; use foundry_compilers::artifacts::ast::{self, Ast, Node, NodeType}; use rayon::prelude::*; -use rustc_hash::FxHashMap; use std::sync::Arc; /// A visitor that walks the AST of a single contract and finds coverage items. @@ -470,12 +470,14 @@ impl<'a> ContractVisitor<'a> { } fn source_location_for(&self, loc: &ast::LowFidelitySourceLocation) -> SourceLocation { + let loc_start = + self.source.char_indices().map(|(i, _)| i).nth(loc.start).unwrap_or_default(); SourceLocation { source_id: self.source_id, contract_name: self.contract_name.clone(), start: loc.start as u32, length: loc.length.map(|x| x as u32), - line: self.source[..loc.start].lines().count(), + line: self.source[..loc_start].lines().count(), } } } @@ -581,7 +583,7 @@ impl<'a> SourceAnalyzer<'a> { #[derive(Debug, Default)] pub struct SourceFiles<'a> { /// The versioned sources. - pub sources: FxHashMap>, + pub sources: HashMap>, } /// The source code and AST of a file. diff --git a/crates/evm/coverage/src/anchors.rs b/crates/evm/coverage/src/anchors.rs index 9e82ec1cb..f42fd1a62 100644 --- a/crates/evm/coverage/src/anchors.rs +++ b/crates/evm/coverage/src/anchors.rs @@ -1,9 +1,9 @@ use super::{CoverageItem, CoverageItemKind, ItemAnchor, SourceLocation}; +use alloy_primitives::map::{DefaultHashBuilder, HashMap, HashSet}; use eyre::ensure; use foundry_compilers::artifacts::sourcemap::{SourceElement, SourceMap}; use foundry_evm_core::utils::IcPcMap; use revm::interpreter::opcode; -use rustc_hash::{FxHashMap, FxHashSet}; /// Attempts to find anchors for the given items using the given source map and bytecode. pub fn find_anchors( @@ -11,9 +11,9 @@ pub fn find_anchors( source_map: &SourceMap, ic_pc_map: &IcPcMap, items: &[CoverageItem], - items_by_source_id: &FxHashMap>, + items_by_source_id: &HashMap>, ) -> Vec { - let mut seen = FxHashSet::default(); + let mut seen = HashSet::with_hasher(DefaultHashBuilder::default()); source_map .iter() .filter_map(|element| items_by_source_id.get(&(element.index()? as usize))) @@ -71,7 +71,7 @@ pub fn find_anchor_simple( Ok(ItemAnchor { instruction: ic_pc_map.get(instruction).ok_or_else(|| { - eyre::eyre!("We found an anchor, but we cant translate it to a program counter") + eyre::eyre!("We found an anchor, but we can't translate it to a program counter") })?, item_id, }) diff --git a/crates/evm/coverage/src/lib.rs b/crates/evm/coverage/src/lib.rs index 4481813ac..220ab6b41 100644 --- a/crates/evm/coverage/src/lib.rs +++ b/crates/evm/coverage/src/lib.rs @@ -8,12 +8,12 @@ #[macro_use] extern crate tracing; -use alloy_primitives::{Bytes, B256}; +use alloy_primitives::{map::HashMap, Bytes, B256}; use eyre::{Context, Result}; use foundry_compilers::artifacts::sourcemap::SourceMap; use semver::Version; use std::{ - collections::{BTreeMap, HashMap}, + collections::BTreeMap, fmt::Display, ops::{AddAssign, Deref, DerefMut}, path::{Path, PathBuf}, diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 268f9689e..8c8d7ff69 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -1,7 +1,7 @@ use crate::executors::{Executor, RawCallResult}; use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::Function; -use alloy_primitives::{Address, Bytes, Log, U256}; +use alloy_primitives::{map::HashMap, Address, Bytes, Log, U256}; use eyre::Result; use foundry_common::evm::Breakpoints; use foundry_config::FuzzConfig; @@ -17,7 +17,7 @@ use foundry_evm_fuzz::{ use foundry_evm_traces::SparsedTraceArena; use indicatif::ProgressBar; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; -use std::cell::RefCell; +use std::{cell::RefCell, collections::BTreeMap}; mod types; pub use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome}; @@ -39,6 +39,10 @@ pub struct FuzzTestData { pub coverage: Option, // Stores logs for all fuzz cases pub logs: Vec, + // Stores gas snapshots for all fuzz cases + pub gas_snapshots: BTreeMap>, + // Deprecated cheatcodes mapped to their replacements. + pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>, } /// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`]. @@ -87,7 +91,7 @@ impl FuzzedExecutor { let state = self.build_fuzz_state(); let no_zksync_reserved_addresses = state.dictionary_read().no_zksync_reserved_addresses(); let dictionary_weight = self.config.dictionary.dictionary_weight.min(100); - let strat = proptest::prop_oneof![ + let strategy = proptest::prop_oneof![ 100 - dictionary_weight => fuzz_calldata(func.clone(), fuzz_fixtures, no_zksync_reserved_addresses), dictionary_weight => fuzz_calldata_from_state(func.clone(), &state), ]; @@ -95,7 +99,7 @@ impl FuzzedExecutor { let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize; let show_logs = self.config.show_logs; - let run_result = self.runner.clone().run(&strat, |calldata| { + let run_result = self.runner.clone().run(&strategy, |calldata| { let fuzz_res = self.single_fuzz(address, should_fail, calldata)?; // If running with progress then increment current run. @@ -107,9 +111,11 @@ impl FuzzedExecutor { FuzzOutcome::Case(case) => { let mut data = execution_data.borrow_mut(); data.gas_by_case.push((case.case.gas, case.case.stipend)); + if data.first_case.is_none() { data.first_case.replace(case.case); } + if let Some(call_traces) = case.traces { if data.traces.len() == max_traces_to_collect { data.traces.pop(); @@ -117,15 +123,19 @@ impl FuzzedExecutor { data.traces.push(call_traces); data.breakpoints.replace(case.breakpoints); } + if show_logs { data.logs.extend(case.logs); } + // Collect and merge coverage if `forge snapshot` context. match &mut data.coverage { Some(prev) => prev.merge(case.coverage.unwrap()), opt => *opt = case.coverage, } + data.deprecated_cheatcodes = case.deprecated_cheatcodes; + Ok(()) } FuzzOutcome::CounterExample(CounterExampleOutcome { @@ -169,6 +179,7 @@ impl FuzzedExecutor { breakpoints: last_run_breakpoints, gas_report_traces: traces.into_iter().map(|a| a.arena).collect(), coverage: fuzz_result.coverage, + deprecated_cheatcodes: fuzz_result.deprecated_cheatcodes, }; match run_result { @@ -231,10 +242,10 @@ impl FuzzedExecutor { return Err(TestCaseError::reject(FuzzError::AssumeReject)) } - let breakpoints = call - .cheatcodes - .as_ref() - .map_or_else(Default::default, |cheats| cheats.breakpoints.clone()); + let (breakpoints, deprecated_cheatcodes) = + call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| { + (cheats.breakpoints.clone(), cheats.deprecated.clone()) + }); let success = self.executor.is_raw_call_mut_success(address, &mut call, should_fail); if success { @@ -244,6 +255,7 @@ impl FuzzedExecutor { coverage: call.coverage, breakpoints, logs: call.logs, + deprecated_cheatcodes, })) } else { Ok(FuzzOutcome::CounterExample(CounterExampleOutcome { diff --git a/crates/evm/evm/src/executors/fuzz/types.rs b/crates/evm/evm/src/executors/fuzz/types.rs index ac7c14373..f5399c5c3 100644 --- a/crates/evm/evm/src/executors/fuzz/types.rs +++ b/crates/evm/evm/src/executors/fuzz/types.rs @@ -1,5 +1,5 @@ use crate::executors::RawCallResult; -use alloy_primitives::{Bytes, Log}; +use alloy_primitives::{map::HashMap, Bytes, Log}; use foundry_common::evm::Breakpoints; use foundry_evm_coverage::HitMaps; use foundry_evm_fuzz::FuzzCase; @@ -9,26 +9,28 @@ use revm::interpreter::InstructionResult; /// Returned by a single fuzz in the case of a successful run #[derive(Debug)] pub struct CaseOutcome { - /// Data of a single fuzz test case + /// Data of a single fuzz test case. pub case: FuzzCase, - /// The traces of the call + /// The traces of the call. pub traces: Option, - /// The coverage info collected during the call + /// The coverage info collected during the call. pub coverage: Option, - /// Breakpoints char pc map + /// Breakpoints char pc map. pub breakpoints: Breakpoints, - /// logs of a single fuzz test case + /// logs of a single fuzz test case. pub logs: Vec, + // Deprecated cheatcodes mapped to their replacements. + pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>, } /// Returned by a single fuzz when a counterexample has been discovered #[derive(Debug)] pub struct CounterExampleOutcome { - /// Minimal reproduction test case for failing test + /// Minimal reproduction test case for failing test. pub counterexample: (Bytes, RawCallResult), - /// The status of the call + /// The status of the call. pub exit_reason: InstructionResult, - /// Breakpoints char pc map + /// Breakpoints char pc map. pub breakpoints: Breakpoints, } diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index a931c8c76..e8dcea10b 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -242,7 +242,7 @@ impl InvariantTestRun { } } -/// Wrapper around any [`Executor`] implementor which provides fuzzing support using [`proptest`]. +/// Wrapper around any [`Executor`] implementer which provides fuzzing support using [`proptest`]. /// /// After instantiation, calling `invariant_fuzz` will proceed to hammer the deployed smart /// contracts with inputs, until it finds a counterexample sequence. The provided [`TestRunner`] @@ -316,10 +316,11 @@ impl<'a> InvariantExecutor<'a> { TestCaseError::fail("No input generated to call fuzzed target.") })?; - // Execute call from the randomly generated sequence and commit state changes. - let call_result = current_run + // Execute call from the randomly generated sequence without committing state. + // State is committed only if call is not a magic assume. + let mut call_result = current_run .executor - .transact_raw( + .call_raw( tx.sender, tx.call_details.target, tx.call_details.calldata.clone(), @@ -342,9 +343,11 @@ impl<'a> InvariantExecutor<'a> { return Err(TestCaseError::fail("Max number of vm.assume rejects reached.")) } } else { + // Commit executed call result. + current_run.executor.commit(&mut call_result); + // Collect data for fuzzing from the state changeset. let mut state_changeset = call_result.state_changeset.clone(); - if !call_result.reverted { collect_data( &invariant_test, @@ -368,13 +371,13 @@ impl<'a> InvariantExecutor<'a> { { warn!(target: "forge::test", "{error}"); } - current_run.fuzz_runs.push(FuzzCase { calldata: tx.call_details.calldata.clone(), gas: call_result.gas_used, stipend: call_result.stipend, }); + // Determine if test can continue or should exit. let result = can_continue( &invariant_contract, &invariant_test, @@ -384,11 +387,9 @@ impl<'a> InvariantExecutor<'a> { &state_changeset, ) .map_err(|e| TestCaseError::fail(e.to_string()))?; - if !result.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 { return Err(TestCaseError::fail("Test cannot continue.")) @@ -465,7 +466,7 @@ impl<'a> InvariantExecutor<'a> { ); // Creates the invariant strategy. - let strat = invariant_strat( + let strategy = invariant_strat( fuzz_state.clone(), targeted_senders, targeted_contracts.clone(), @@ -521,7 +522,7 @@ impl<'a> InvariantExecutor<'a> { last_call_results, self.runner.clone(), ), - strat, + strategy, )) } @@ -743,9 +744,6 @@ impl<'a> InvariantExecutor<'a> { ) -> Result<()> { for (address, (identifier, _)) in self.setup_contracts.iter() { if let Some(selectors) = self.artifact_filters.targeted.get(identifier) { - if selectors.is_empty() { - continue; - } self.add_address_with_functions(*address, selectors, false, targeted_contracts)?; } } @@ -775,6 +773,11 @@ impl<'a> InvariantExecutor<'a> { should_exclude: bool, targeted_contracts: &mut TargetedContracts, ) -> eyre::Result<()> { + // Do not add address in target contracts if no function selected. + if selectors.is_empty() { + return Ok(()) + } + let contract = match targeted_contracts.entry(address) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index 34a525a47..969f1587f 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::executors::Executor; use alloy_dyn_abi::JsonAbiExt; -use alloy_primitives::Log; +use alloy_primitives::{map::HashMap, Log}; use eyre::Result; use foundry_common::{ContractsByAddress, ContractsByArtifact}; use foundry_evm_coverage::HitMaps; @@ -17,7 +17,7 @@ use indicatif::ProgressBar; use parking_lot::RwLock; use proptest::test_runner::TestError; use revm::primitives::U256; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; /// Replays a call sequence for collecting logs and traces. /// Returns counterexample to be used when the call sequence is a failed scenario. @@ -30,6 +30,7 @@ pub fn replay_run( logs: &mut Vec, traces: &mut Traces, coverage: &mut Option, + deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>, inputs: &[BasicTxDetails], ) -> Result> { // We want traces for a failed case. @@ -59,11 +60,8 @@ pub fn replay_run( } // Identify newly generated contracts, if they exist. - ided_contracts.extend(load_contracts( - call_result.traces.iter().map(|a| &a.arena), - known_contracts, - &HashMap::new(), - )); + 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( @@ -87,6 +85,12 @@ pub fn replay_run( )?; traces.push((TraceKind::Execution, invariant_result.traces.clone().unwrap())); logs.extend(invariant_result.logs); + deprecated_cheatcodes.extend( + invariant_result + .cheatcodes + .as_ref() + .map_or_else(Default::default, |cheats| cheats.deprecated.clone()), + ); // Collect after invariant logs and traces. if invariant_contract.call_after_invariant && invariant_success { @@ -110,6 +114,7 @@ pub fn replay_error( logs: &mut Vec, traces: &mut Traces, coverage: &mut Option, + deprecated_cheatcodes: &mut HashMap<&'static str, Option<&'static str>>, progress: Option<&ProgressBar>, ) -> Result> { match failed_case.test_error { @@ -136,6 +141,7 @@ pub fn replay_error( logs, traces, coverage, + deprecated_cheatcodes, &calls, ) } diff --git a/crates/evm/evm/src/executors/invariant/shrink.rs b/crates/evm/evm/src/executors/invariant/shrink.rs index 318163534..c468c58ee 100644 --- a/crates/evm/evm/src/executors/invariant/shrink.rs +++ b/crates/evm/evm/src/executors/invariant/shrink.rs @@ -5,6 +5,7 @@ use crate::executors::{ Executor, }; use alloy_primitives::{Address, Bytes, U256}; +use foundry_evm_core::constants::MAGIC_ASSUME; use foundry_evm_fuzz::invariant::BasicTxDetails; use indicatif::ProgressBar; use proptest::bits::{BitSetLike, VarBitSet}; @@ -21,13 +22,13 @@ struct Shrink { /// If the failure is not reproducible then restore removed call and moves to next one. #[derive(Debug)] struct CallSequenceShrinker { - /// Length of call sequence to be shrinked. + /// Length of call sequence to be shrunk. call_sequence_len: usize, - /// Call ids contained in current shrinked sequence. + /// Call ids contained in current shrunk sequence. included_calls: VarBitSet, - /// Current shrinked call id. + /// Current shrunk call id. shrink: Shrink, - /// Previous shrinked call id. + /// Previous shrunk call id. prev_shrink: Option, } @@ -82,7 +83,7 @@ impl CallSequenceShrinker { /// Maximal shrinkage is guaranteed if the shrink_run_limit is not set to a value lower than the /// length of failed call sequence. /// -/// The shrinked call sequence always respect the order failure is reproduced as it is tested +/// The shrunk call sequence always respect the order failure is reproduced as it is tested /// top-down. pub(crate) fn shrink_sequence( failed_case: &FailedInvariantCaseData, @@ -160,7 +161,10 @@ pub fn check_sequence( tx.call_details.calldata.clone(), U256::ZERO, )?; - if call_result.reverted && fail_on_revert { + // Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed scenarios that + // are replayed with a modified version of test driver (that use new `vm.assume` + // cheatcodes). + if call_result.reverted && fail_on_revert && call_result.result.as_ref() != MAGIC_ASSUME { // Candidate sequence fails test. // We don't have to apply remaining calls to check sequence. return Ok((false, false)); diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index ba8c50360..a3c14b992 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -11,7 +11,10 @@ use crate::inspectors::{ }; use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; -use alloy_primitives::{Address, Bytes, Log, U256}; +use alloy_primitives::{ + map::{AddressHashMap, HashMap}, + Address, Bytes, Log, U256, +}; use alloy_sol_types::{sol, SolCall}; use foundry_evm_core::{ backend::{Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT}, @@ -34,7 +37,7 @@ use revm::{ }, Database, }; -use std::{borrow::Cow, collections::HashMap}; +use std::borrow::Cow; mod builder; pub use builder::ExecutorBuilder; @@ -440,7 +443,7 @@ impl Executor { )? } }; - convert_executed_result(env, inspector, result, backend.has_snapshot_failure()) + convert_executed_result(env, inspector, result, backend.has_state_snapshot_failure()) } /// Execute the transaction configured in `env.tx`. @@ -467,20 +470,9 @@ impl Executor { let mut result = convert_executed_result( env, inspector, - result_and_state.clone(), - backend.has_snapshot_failure(), + result_and_state, + backend.has_state_snapshot_failure(), )?; - let state = result_and_state.state; - if let Some(traces) = &mut result.traces { - for trace_node in traces.nodes() { - if let Some(account_info) = state.get(&trace_node.trace.address) { - result.deployments.insert( - trace_node.trace.address, - account_info.info.code.clone().unwrap_or_default().bytes(), - ); - } - } - } self.commit(&mut result); Ok(result) @@ -541,7 +533,7 @@ impl Executor { call_result: &RawCallResult, should_fail: bool, ) -> bool { - if call_result.has_snapshot_failure { + if call_result.has_state_snapshot_failure { // a failure occurred in a reverted snapshot, which is considered a failed test return should_fail; } @@ -593,7 +585,7 @@ impl Executor { } // A failure occurred in a reverted snapshot, which is considered a failed test. - if self.backend().has_snapshot_failure() { + if self.backend().has_state_snapshot_failure() { return false; } @@ -656,7 +648,7 @@ impl Executor { /// Creates the environment to use when executing a transaction in a test context /// /// If using a backend with cheatcodes, `tx.gas_price` and `block.number` will be overwritten by - /// the cheatcode state inbetween calls. + /// the cheatcode state in between calls. fn build_test_env( &self, caller: Address, @@ -791,7 +783,7 @@ pub struct RawCallResult { /// /// This is tracked separately from revert because a snapshot failure can occur without a /// revert, since assert failures are stored in a global variable (ds-test legacy) - pub has_snapshot_failure: bool, + pub has_state_snapshot_failure: bool, /// The raw result of the call. pub result: Bytes, /// The gas used for the call @@ -803,7 +795,7 @@ pub struct RawCallResult { /// The logs emitted during the call pub logs: Vec, /// The labels assigned to addresses during the call - pub labels: HashMap, + pub labels: AddressHashMap, /// The traces of the call pub traces: Option, /// The coverage info collected during the call @@ -820,23 +812,20 @@ pub struct RawCallResult { pub out: Option, /// The chisel state pub chisel_state: Option<(Vec, Vec, InstructionResult)>, - /// The deployments generated during the call - pub deployments: HashMap, } impl Default for RawCallResult { fn default() -> Self { Self { - deployments: HashMap::new(), exit_reason: InstructionResult::Continue, reverted: false, - has_snapshot_failure: false, + has_state_snapshot_failure: false, result: Bytes::new(), gas_used: 0, gas_refunded: 0, stipend: 0, logs: Vec::new(), - labels: HashMap::new(), + labels: HashMap::default(), traces: None, coverage: None, transactions: None, @@ -925,7 +914,7 @@ fn convert_executed_result( env: EnvWithHandlerCfg, inspector: InspectorStack, ResultAndState { result, state: state_changeset }: ResultAndState, - has_snapshot_failure: bool, + has_state_snapshot_failure: bool, ) -> eyre::Result { let (exit_reason, gas_refunded, gas_used, out, _exec_logs) = match result { ExecutionResult::Success { reason, gas_used, gas_refunded, output, logs, .. } => { @@ -961,10 +950,9 @@ fn convert_executed_result( .filter(|txs| !txs.is_empty()); Ok(RawCallResult { - deployments: HashMap::new(), exit_reason, reverted: !matches!(exit_reason, return_ok!()), - has_snapshot_failure, + has_state_snapshot_failure, result, gas_used, gas_refunded, diff --git a/crates/evm/evm/src/inspectors/logs.rs b/crates/evm/evm/src/inspectors/logs.rs index 631c4af81..4abfdcb4e 100644 --- a/crates/evm/evm/src/inspectors/logs.rs +++ b/crates/evm/evm/src/inspectors/logs.rs @@ -60,7 +60,7 @@ impl Inspector for LogCollector { gas: Gas::new(inputs.gas_limit), }, memory_offset: inputs.return_memory_offset.clone(), - }) + }); } } @@ -68,7 +68,7 @@ impl Inspector for LogCollector { } } -impl InspectorExt for LogCollector { +impl InspectorExt for LogCollector { fn console_log(&mut self, input: String) { self.logs.push(Log::new_unchecked( HARDHAT_CONSOLE_ADDRESS, diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index b96ce1d52..f312d123a 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -1,12 +1,9 @@ use super::{ Cheatcodes, CheatsConfig, ChiselState, CoverageCollector, Fuzzer, LogCollector, TraceCollector, }; -use alloy_primitives::{Address, Bytes, Log, TxKind, U256}; -use foundry_cheatcodes::CheatcodesExecutor; -use foundry_evm_core::{ - backend::{update_state, DatabaseExt}, - InspectorExt, -}; +use alloy_primitives::{map::AddressHashMap, Address, Bytes, Log, TxKind, U256}; +use foundry_cheatcodes::{CheatcodesExecutor, Wallets}; +use foundry_evm_core::{backend::DatabaseExt, InspectorExt}; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::{SparsedTraceArena, TraceMode}; use foundry_zksync_core::Call; @@ -17,12 +14,12 @@ use revm::{ EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterResult, }, primitives::{ - BlockEnv, CreateScheme, Env, EnvWithHandlerCfg, ExecutionResult, Output, TransactTo, + Account, AccountStatus, BlockEnv, CreateScheme, Env, EnvWithHandlerCfg, ExecutionResult, + HashMap, Output, TransactTo, }, EvmContext, Inspector, }; use std::{ - collections::HashMap, ops::{Deref, DerefMut}, sync::Arc, }; @@ -60,6 +57,8 @@ pub struct InspectorStackBuilder { pub enable_isolation: bool, /// Whether to enable Alphanet features. pub alphanet: bool, + /// The wallets to set in the cheatcodes context. + pub wallets: Option, } impl InspectorStackBuilder { @@ -90,6 +89,13 @@ impl InspectorStackBuilder { self } + /// Set the wallets. + #[inline] + pub fn wallets(mut self, wallets: Wallets) -> Self { + self.wallets = Some(wallets); + self + } + /// Set the fuzzer inspector. #[inline] pub fn fuzzer(mut self, fuzzer: Fuzzer) -> Self { @@ -164,13 +170,20 @@ impl InspectorStackBuilder { chisel_state, enable_isolation, alphanet, + wallets, } = self; let mut stack = InspectorStack::new(); // inspectors if let Some(config) = cheatcodes { - stack.set_cheatcodes(Cheatcodes::new(config)); + let mut cheatcodes = Cheatcodes::new(config); + // Set wallets if they are provided + if let Some(wallets) = wallets { + cheatcodes.set_wallets(wallets); + } + stack.set_cheatcodes(cheatcodes); } + if let Some(fuzzer) = fuzzer { stack.set_fuzzer(fuzzer); } @@ -219,31 +232,10 @@ macro_rules! call_inspectors { }; } -/// Same as [`call_inspectors!`], but with depth adjustment for isolated execution. -macro_rules! call_inspectors_adjust_depth { - ([$($inspector:expr),+ $(,)?], |$id:ident $(,)?| $call:expr, $self:ident, $data:ident $(,)?) => { - $data.journaled_state.depth += $self.in_inner_context as usize; - call_inspectors!([$($inspector),+], |$id| $call); - $data.journaled_state.depth -= $self.in_inner_context as usize; - }; - (#[ret] [$($inspector:expr),+ $(,)?], |$id:ident $(,)?| $call:expr, $self:ident, $data:ident $(,)?) => { - $data.journaled_state.depth += $self.in_inner_context as usize; - $( - if let Some($id) = $inspector { - if let Some(result) = ({ #[inline(always)] #[cold] || $call })() { - $data.journaled_state.depth -= $self.in_inner_context as usize; - return result; - } - } - )+ - $data.journaled_state.depth -= $self.in_inner_context as usize; - }; -} - /// The collected results of [`InspectorStack`]. pub struct InspectorData { pub logs: Vec, - pub labels: HashMap, + pub labels: AddressHashMap, pub traces: Option, pub coverage: Option, pub cheatcodes: Option, @@ -294,6 +286,7 @@ pub struct InspectorStackInner { /// Flag marking if we are in the inner EVM context. pub in_inner_context: bool, pub inner_context_data: Option, + pub top_frame_journal: HashMap, } /// Struct keeping mutable references to both parts of [InspectorStack] and implementing @@ -305,11 +298,8 @@ pub struct InspectorStackRefMut<'a> { } impl CheatcodesExecutor for InspectorStackInner { - fn get_inspector<'a, DB: DatabaseExt>( - &'a mut self, - cheats: &'a mut Cheatcodes, - ) -> impl InspectorExt + 'a { - InspectorStackRefMut { cheatcodes: Some(cheats), inner: self } + fn get_inspector<'a>(&'a mut self, cheats: &'a mut Cheatcodes) -> Box { + Box::new(InspectorStackRefMut { cheatcodes: Some(cheats), inner: self }) } fn tracing_inspector(&mut self) -> Option<&mut Option> { @@ -475,25 +465,25 @@ impl InspectorStack { } } -impl<'a> InspectorStackRefMut<'a> { +impl InspectorStackRefMut<'_> { /// Adjusts the EVM data for the inner EVM context. /// Should be called on the top-level call of inner context (depth == 0 && /// self.in_inner_context) Decreases sender nonce for CALLs to keep backwards compatibility /// Updates tx.origin to the value before entering inner context - fn adjust_evm_data_for_inner_context(&mut self, ecx: &mut EvmContext) { + fn adjust_evm_data_for_inner_context(&mut self, ecx: &mut EvmContext<&mut dyn DatabaseExt>) { let inner_context_data = self.inner_context_data.as_ref().expect("should be called in inner context"); ecx.env.tx.caller = inner_context_data.original_origin; } - fn do_call_end( + fn do_call_end( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &CallInputs, outcome: CallOutcome, ) -> CallOutcome { let result = outcome.result.result; - call_inspectors_adjust_depth!( + call_inspectors!( #[ret] [&mut self.fuzzer, &mut self.tracer, &mut self.cheatcodes, &mut self.printer], |inspector| { @@ -506,16 +496,64 @@ impl<'a> InspectorStackRefMut<'a> { new_outcome.output() != outcome.output()); different.then_some(new_outcome) }, - self, - ecx ); outcome } - fn transact_inner( + fn do_create_end( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + call: &CreateInputs, + outcome: CreateOutcome, + ) -> CreateOutcome { + let result = outcome.result.result; + call_inspectors!( + #[ret] + [&mut self.tracer, &mut self.cheatcodes, &mut self.printer], + |inspector| { + let new_outcome = inspector.create_end(ecx, call, outcome.clone()); + + // If the inspector returns a different status or a revert with a non-empty message, + // we assume it wants to tell us something + let different = new_outcome.result.result != result || + (new_outcome.result.result == InstructionResult::Revert && + new_outcome.output() != outcome.output()); + different.then_some(new_outcome) + }, + ); + + outcome + } + + fn do_eofcreate_end( + &mut self, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + call: &EOFCreateInputs, + outcome: CreateOutcome, + ) -> CreateOutcome { + let result = outcome.result.result; + call_inspectors!( + #[ret] + [&mut self.tracer, &mut self.cheatcodes, &mut self.printer], + |inspector| { + let new_outcome = inspector.eofcreate_end(ecx, call, outcome.clone()); + + // If the inspector returns a different status or a revert with a non-empty message, + // we assume it wants to tell us something + let different = new_outcome.result.result != result || + (new_outcome.result.result == InstructionResult::Revert && + new_outcome.output() != outcome.output()); + different.then_some(new_outcome) + }, + ); + + outcome + } + + fn transact_inner( + &mut self, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, transact_to: TransactTo, caller: Address, input: Bytes, @@ -524,8 +562,6 @@ impl<'a> InspectorStackRefMut<'a> { ) -> (InterpreterResult, Option
) { let ecx = &mut ecx.inner; - ecx.db.commit(ecx.journaled_state.state.clone()); - let cached_env = ecx.env.clone(); ecx.env.block.basefee = U256::ZERO; @@ -547,18 +583,37 @@ impl<'a> InspectorStackRefMut<'a> { self.in_inner_context = true; let env = EnvWithHandlerCfg::new_with_spec_id(ecx.env.clone(), ecx.spec_id()); - let res = { - let mut evm = crate::utils::new_evm_with_inspector( - &mut ecx.db as &mut dyn DatabaseExt, - env, - &mut *self, - ); + let res = self.with_stack(|inspector| { + let mut evm = crate::utils::new_evm_with_inspector(&mut ecx.db, env, inspector); + + evm.context.evm.inner.journaled_state.state = { + let mut state = ecx.journaled_state.state.clone(); + + for (addr, acc_mut) in &mut state { + // mark all accounts cold, besides preloaded addresses + if !ecx.journaled_state.warm_preloaded_addresses.contains(addr) { + acc_mut.mark_cold(); + } + + // mark all slots 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 to make sure traces are collected correctly + evm.context.evm.inner.journaled_state.depth = 1; + let res = evm.transact(); // need to reset the env in case it was modified via cheatcodes during execution ecx.env = evm.context.evm.inner.env; res - }; + }); self.in_inner_context = false; self.inner_context_data = None; @@ -568,43 +623,35 @@ impl<'a> InspectorStackRefMut<'a> { let mut gas = Gas::new(gas_limit); - let Ok(mut res) = res else { + let Ok(res) = res else { // Should we match, encode and propagate error as a revert reason? let result = InterpreterResult { result: InstructionResult::Revert, output: Bytes::new(), gas }; return (result, None); }; - // Commit changes after transaction - ecx.db.commit(res.state.clone()); - - // Update both states with new DB data after commit. - if let Err(e) = update_state(&mut ecx.journaled_state.state, &mut ecx.db, None) { - let res = InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from(e.to_string()), - gas, - }; - return (res, None); - } - if let Err(e) = update_state(&mut res.state, &mut ecx.db, None) { - let res = InterpreterResult { - result: InstructionResult::Revert, - output: Bytes::from(e.to_string()), - gas, + for (addr, mut acc) in res.state { + let Some(acc_mut) = ecx.journaled_state.state.get_mut(&addr) else { + ecx.journaled_state.state.insert(addr, acc); + continue }; - return (res, None); - } - // Merge transaction journal into the active journal. - for (addr, acc) in res.state { - if let Some(acc_mut) = ecx.journaled_state.state.get_mut(&addr) { - acc_mut.status |= acc.status; - for (key, val) in acc.storage { - acc_mut.storage.entry(key).or_insert(val); - } - } else { - ecx.journaled_state.state.insert(addr, acc); + // make sure accounts that were warmed earlier do not become cold + 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; + + 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; } } @@ -629,20 +676,74 @@ impl<'a> InspectorStackRefMut<'a> { }; (InterpreterResult { result, output, gas }, address) } + + /// Moves out of references, constructs an [`InspectorStack`] and runs the given closure with + /// it. + fn with_stack(&mut self, f: impl FnOnce(&mut InspectorStack) -> O) -> O { + let mut stack = InspectorStack { + cheatcodes: self.cheatcodes.as_deref_mut().map(std::mem::take), + inner: std::mem::take(self.inner), + }; + + let out = f(&mut stack); + + if let Some(cheats) = self.cheatcodes.as_deref_mut() { + *cheats = stack.cheatcodes.take().unwrap(); + } + + *self.inner = stack.inner; + + out + } + + /// Invoked at the beginning of a new top-level (0 depth) frame. + fn top_level_frame_start(&mut self, ecx: &mut EvmContext<&mut dyn DatabaseExt>) { + if self.enable_isolation { + // If we're in isolation mode, we need to keep track of the state at the beginning of + // the frame to be able to roll back on revert + self.top_frame_journal = ecx.journaled_state.state.clone(); + } + } + + /// Invoked at the end of root frame. + fn top_level_frame_end( + &mut self, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + result: InstructionResult, + ) { + if !result.is_revert() { + return; + } + // Encountered a revert, since cheatcodes may have altered the evm state in such a way + // that violates some constraints, e.g. `deal`, we need to manually roll back on revert + // before revm reverts the state itself + if let Some(cheats) = self.cheatcodes.as_mut() { + cheats.on_revert(ecx); + } + + // If we're in isolation mode, we need to rollback to state before the root frame was + // created We can't rely on revm's journal because it doesn't account for changes + // made by isolated calls + if self.enable_isolation { + ecx.journaled_state.state = std::mem::take(&mut self.top_frame_journal); + } + } } -impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { - fn initialize_interp(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { - call_inspectors_adjust_depth!( +impl Inspector<&mut dyn DatabaseExt> for InspectorStackRefMut<'_> { + fn initialize_interp( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + ) { + call_inspectors!( [&mut self.coverage, &mut self.tracer, &mut self.cheatcodes, &mut self.printer], |inspector| inspector.initialize_interp(interpreter, ecx), - self, - ecx ); } - fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { - call_inspectors_adjust_depth!( + fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<&mut dyn DatabaseExt>) { + call_inspectors!( [ &mut self.fuzzer, &mut self.tracer, @@ -651,36 +752,47 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { &mut self.printer, ], |inspector| inspector.step(interpreter, ecx), - self, - ecx ); } - fn step_end(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { - call_inspectors_adjust_depth!( + fn step_end( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + ) { + call_inspectors!( [&mut self.tracer, &mut self.cheatcodes, &mut self.chisel_state, &mut self.printer], |inspector| inspector.step_end(interpreter, ecx), - self, - ecx ); } - fn log(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext, log: &Log) { - call_inspectors_adjust_depth!( + fn log( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + log: &Log, + ) { + call_inspectors!( [&mut self.tracer, &mut self.log_collector, &mut self.cheatcodes, &mut self.printer], |inspector| inspector.log(interpreter, ecx, log), - self, - ecx ); } - fn call(&mut self, ecx: &mut EvmContext, call: &mut CallInputs) -> Option { - if self.in_inner_context && ecx.journaled_state.depth == 0 { + fn call( + &mut self, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + call: &mut CallInputs, + ) -> Option { + if self.in_inner_context && ecx.journaled_state.depth == 1 { self.adjust_evm_data_for_inner_context(ecx); return None; } - call_inspectors_adjust_depth!( + if ecx.journaled_state.depth == 0 { + self.top_level_frame_start(ecx); + } + + call_inspectors!( #[ret] [&mut self.fuzzer, &mut self.tracer, &mut self.log_collector, &mut self.printer], |inspector| { @@ -692,11 +804,8 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { } out }, - self, - ecx ); - ecx.journaled_state.depth += self.in_inner_context as usize; if let Some(cheatcodes) = self.cheatcodes.as_deref_mut() { // Handle mocked functions, replace bytecode address with mock if matched. if let Some(mocks) = cheatcodes.mocked_functions.get(&call.target_address) { @@ -712,12 +821,10 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { if let Some(output) = cheatcodes.call_with_executor(ecx, call, self.inner) { if output.result.result != InstructionResult::Continue { - ecx.journaled_state.depth -= self.in_inner_context as usize; return Some(output); } } } - ecx.journaled_state.depth -= self.in_inner_context as usize; if self.enable_isolation && call.scheme == CallScheme::Call && @@ -740,24 +847,20 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { fn call_end( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &CallInputs, outcome: CallOutcome, ) -> CallOutcome { - // Inner context calls with depth 0 are being dispatched as top-level calls with depth 1. - // Avoid processing twice. - if self.in_inner_context && ecx.journaled_state.depth == 0 { + // We are processing inner context outputs in the outer context, so need to avoid processing + // twice. + if self.in_inner_context && ecx.journaled_state.depth == 1 { return outcome; } let outcome = self.do_call_end(ecx, inputs, outcome); - if outcome.result.is_revert() { - // Encountered a revert, since cheatcodes may have altered the evm state in such a way - // that violates some constraints, e.g. `deal`, we need to manually roll back on revert - // before revm reverts the state itself - if let Some(cheats) = self.cheatcodes.as_mut() { - cheats.on_revert(ecx); - } + + if ecx.journaled_state.depth == 0 { + self.top_level_frame_end(ecx, outcome.result.result); } outcome @@ -765,20 +868,22 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { fn create( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, create: &mut CreateInputs, ) -> Option { - if self.in_inner_context && ecx.journaled_state.depth == 0 { + if self.in_inner_context && ecx.journaled_state.depth == 1 { self.adjust_evm_data_for_inner_context(ecx); return None; } - call_inspectors_adjust_depth!( + if ecx.journaled_state.depth == 0 { + self.top_level_frame_start(ecx); + } + + call_inspectors!( #[ret] [&mut self.tracer, &mut self.coverage], |inspector| inspector.create(ecx, create).map(Some), - self, - ecx ); ecx.journaled_state.depth += self.in_inner_context as usize; @@ -813,54 +918,43 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { fn create_end( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, call: &CreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { - // Inner context calls with depth 0 are being dispatched as top-level calls with depth 1. - // Avoid processing twice. - if self.in_inner_context && ecx.journaled_state.depth == 0 { + // We are processing inner context outputs in the outer context, so need to avoid processing + // twice. + if self.in_inner_context && ecx.journaled_state.depth == 1 { return outcome; } - let result = outcome.result.result; - - call_inspectors_adjust_depth!( - #[ret] - [&mut self.tracer, &mut self.cheatcodes, &mut self.printer], - |inspector| { - let new_outcome = inspector.create_end(ecx, call, outcome.clone()); + let outcome = self.do_create_end(ecx, call, outcome); - // If the inspector returns a different status or a revert with a non-empty message, - // we assume it wants to tell us something - let different = new_outcome.result.result != result || - (new_outcome.result.result == InstructionResult::Revert && - new_outcome.output() != outcome.output()); - different.then_some(new_outcome) - }, - self, - ecx - ); + if ecx.journaled_state.depth == 0 { + self.top_level_frame_end(ecx, outcome.result.result); + } outcome } fn eofcreate( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, create: &mut EOFCreateInputs, ) -> Option { - if self.in_inner_context && ecx.journaled_state.depth == 0 { + if self.in_inner_context && ecx.journaled_state.depth == 1 { self.adjust_evm_data_for_inner_context(ecx); return None; } - call_inspectors_adjust_depth!( + if ecx.journaled_state.depth == 0 { + self.top_level_frame_start(ecx); + } + + call_inspectors!( #[ret] [&mut self.tracer, &mut self.coverage, &mut self.cheatcodes], |inspector| inspector.eofcreate(ecx, create).map(Some), - self, - ecx ); if matches!(create.kind, EOFCreateKind::Tx { .. }) && @@ -889,70 +983,55 @@ impl<'a, DB: DatabaseExt> Inspector for InspectorStackRefMut<'a> { fn eofcreate_end( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, call: &EOFCreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { - // Inner context calls with depth 0 are being dispatched as top-level calls with depth 1. - // Avoid processing twice. - if self.in_inner_context && ecx.journaled_state.depth == 0 { + // We are processing inner context outputs in the outer context, so need to avoid processing + // twice. + if self.in_inner_context && ecx.journaled_state.depth == 1 { return outcome; } - let result = outcome.result.result; - - call_inspectors_adjust_depth!( - #[ret] - [&mut self.tracer, &mut self.cheatcodes, &mut self.printer], - |inspector| { - let new_outcome = inspector.eofcreate_end(ecx, call, outcome.clone()); + let outcome = self.do_eofcreate_end(ecx, call, outcome); - // If the inspector returns a different status or a revert with a non-empty message, - // we assume it wants to tell us something - let different = new_outcome.result.result != result || - (new_outcome.result.result == InstructionResult::Revert && - new_outcome.output() != outcome.output()); - different.then_some(new_outcome) - }, - self, - ecx - ); + if ecx.journaled_state.depth == 0 { + self.top_level_frame_end(ecx, outcome.result.result); + } outcome } fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { call_inspectors!([&mut self.tracer, &mut self.printer], |inspector| { - Inspector::::selfdestruct(inspector, contract, target, value) + Inspector::<&mut dyn DatabaseExt>::selfdestruct(inspector, contract, target, value) }); } } -impl<'a, DB: DatabaseExt> InspectorExt for InspectorStackRefMut<'a> { +impl InspectorExt for InspectorStackRefMut<'_> { fn should_use_create2_factory( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &mut CreateInputs, ) -> bool { - call_inspectors_adjust_depth!( + call_inspectors!( #[ret] [&mut self.cheatcodes], |inspector| { inspector.should_use_create2_factory(ecx, inputs).then_some(true) }, - self, - ecx ); false } fn console_log(&mut self, input: String) { - call_inspectors!([&mut self.log_collector], |inspector| InspectorExt::::console_log( + call_inspectors!([&mut self.log_collector], |inspector| InspectorExt::console_log( inspector, input )); } - fn trace_zksync(&mut self, ecx: &mut EvmContext, call_traces: Vec) { - call_inspectors!([&mut self.tracer], |inspector| InspectorExt::::trace_zksync( + fn trace_zksync(&mut self, ecx: &mut EvmContext<&mut dyn DatabaseExt>, call_traces: Vec) { + call_inspectors!([&mut self.tracer], |inspector| InspectorExt::trace_zksync( inspector, ecx, call_traces @@ -964,20 +1043,24 @@ impl<'a, DB: DatabaseExt> InspectorExt for InspectorStackRefMut<'a> { } } -impl Inspector for InspectorStack { +impl Inspector<&mut dyn DatabaseExt> for InspectorStack { #[inline] - fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<&mut dyn DatabaseExt>) { self.as_mut().step(interpreter, ecx) } #[inline] - fn step_end(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn step_end( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + ) { self.as_mut().step_end(interpreter, ecx) } fn call( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &mut CallInputs, ) -> Option { self.as_mut().call(context, inputs) @@ -985,7 +1068,7 @@ impl Inspector for InspectorStack { fn call_end( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &CallInputs, outcome: CallOutcome, ) -> CallOutcome { @@ -994,7 +1077,7 @@ impl Inspector for InspectorStack { fn create( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, create: &mut CreateInputs, ) -> Option { self.as_mut().create(context, create) @@ -1002,7 +1085,7 @@ impl Inspector for InspectorStack { fn create_end( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, call: &CreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { @@ -1011,7 +1094,7 @@ impl Inspector for InspectorStack { fn eofcreate( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, create: &mut EOFCreateInputs, ) -> Option { self.as_mut().eofcreate(context, create) @@ -1019,30 +1102,39 @@ impl Inspector for InspectorStack { fn eofcreate_end( &mut self, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, call: &EOFCreateInputs, outcome: CreateOutcome, ) -> CreateOutcome { self.as_mut().eofcreate_end(context, call, outcome) } - fn initialize_interp(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext) { + fn initialize_interp( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + ) { self.as_mut().initialize_interp(interpreter, ecx) } - fn log(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext, log: &Log) { + fn log( + &mut self, + interpreter: &mut Interpreter, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, + log: &Log, + ) { self.as_mut().log(interpreter, ecx, log) } fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - Inspector::::selfdestruct(&mut self.as_mut(), contract, target, value) + Inspector::<&mut dyn DatabaseExt>::selfdestruct(&mut self.as_mut(), contract, target, value) } } -impl InspectorExt for InspectorStack { +impl InspectorExt for InspectorStack { fn should_use_create2_factory( &mut self, - ecx: &mut EvmContext, + ecx: &mut EvmContext<&mut dyn DatabaseExt>, inputs: &mut CreateInputs, ) -> bool { self.as_mut().should_use_create2_factory(ecx, inputs) diff --git a/crates/evm/evm/src/lib.rs b/crates/evm/evm/src/lib.rs index 8bbd7f141..15858c0f3 100644 --- a/crates/evm/evm/src/lib.rs +++ b/crates/evm/evm/src/lib.rs @@ -19,9 +19,3 @@ pub use foundry_evm_traces as traces; // TODO: We should probably remove these, but it's a pretty big breaking change. #[doc(hidden)] pub use revm; - -#[doc(hidden)] -#[deprecated = "use `{hash_map, hash_set, HashMap, HashSet}` in `std::collections` or `revm::primitives` instead"] -pub mod hashbrown { - pub use revm::primitives::{hash_map, hash_set, HashMap, HashSet}; -} diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index fe1cb38d0..276958ecf 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -9,13 +9,16 @@ extern crate tracing; use alloy_dyn_abi::{DynSolValue, JsonAbiExt}; -use alloy_primitives::{Address, Bytes, Log}; +use alloy_primitives::{ + map::{AddressHashMap, HashMap}, + Address, Bytes, Log, +}; use foundry_common::{calc, contracts::ContractsByAddress, evm::Breakpoints}; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::{CallTraceArena, SparsedTraceArena}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, sync::Arc}; +use std::{fmt, sync::Arc}; pub use proptest::test_runner::{Config as FuzzConfig, Reason}; @@ -166,7 +169,7 @@ pub struct FuzzTestResult { pub logs: Vec, /// Labeled addresses - pub labeled_addresses: HashMap, + pub labeled_addresses: AddressHashMap, /// Exemplary traces for a fuzz run of the test function /// @@ -183,6 +186,9 @@ pub struct FuzzTestResult { /// Breakpoints for debugger. Correspond to the same fuzz case as `traces`. pub breakpoints: Option, + + // Deprecated cheatcodes mapped to their replacements. + pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>, } impl FuzzTestResult { diff --git a/crates/evm/fuzz/src/strategies/calldata.rs b/crates/evm/fuzz/src/strategies/calldata.rs index 5697be1f6..b6a9f0f26 100644 --- a/crates/evm/fuzz/src/strategies/calldata.rs +++ b/crates/evm/fuzz/src/strategies/calldata.rs @@ -70,16 +70,15 @@ mod tests { use crate::{strategies::fuzz_calldata, FuzzFixtures}; use alloy_dyn_abi::{DynSolValue, JsonAbiExt}; use alloy_json_abi::Function; - use alloy_primitives::Address; + use alloy_primitives::{map::HashMap, Address}; use proptest::prelude::Strategy; - use std::collections::HashMap; #[test] fn can_fuzz_with_fixtures() { let function = Function::parse("test_fuzzed_address(address addressFixture)").unwrap(); let address_fixture = DynSolValue::Address(Address::random()); - let mut fixtures = HashMap::new(); + let mut fixtures = HashMap::default(); fixtures.insert( "addressFixture".to_string(), DynSolValue::Array(vec![address_fixture.clone()]), diff --git a/crates/evm/fuzz/src/strategies/param.rs b/crates/evm/fuzz/src/strategies/param.rs index fb5572f2f..6efd4b48e 100644 --- a/crates/evm/fuzz/src/strategies/param.rs +++ b/crates/evm/fuzz/src/strategies/param.rs @@ -245,12 +245,12 @@ mod tests { let func = get_func(f).unwrap(); let db = CacheDB::new(EmptyDB::default()); let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), false); - let strat = proptest::prop_oneof![ + let strategy = proptest::prop_oneof![ 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default(), false), 40 => fuzz_calldata_from_state(func, &state), ]; let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() }; let mut runner = proptest::test_runner::TestRunner::new(cfg); - let _ = runner.run(&strat, |_| Ok(())); + let _ = runner.run(&strategy, |_| Ok(())); } } diff --git a/crates/evm/fuzz/src/strategies/state.rs b/crates/evm/fuzz/src/strategies/state.rs index 782e8a27b..03ca2559b 100644 --- a/crates/evm/fuzz/src/strategies/state.rs +++ b/crates/evm/fuzz/src/strategies/state.rs @@ -1,7 +1,7 @@ use crate::invariant::{BasicTxDetails, FuzzRunIdentifiedContracts}; use alloy_dyn_abi::{DynSolType, DynSolValue, EventExt, FunctionExt}; use alloy_json_abi::{Function, JsonAbi}; -use alloy_primitives::{Address, Bytes, Log, B256, U256}; +use alloy_primitives::{map::HashMap, Address, Bytes, Log, B256, U256}; use foundry_config::FuzzDictionaryConfig; use foundry_evm_core::utils::StateChangeset; use indexmap::IndexSet; @@ -11,11 +11,7 @@ use revm::{ interpreter::opcode, primitives::AccountInfo, }; -use std::{ - collections::{BTreeMap, HashMap}, - fmt, - sync::Arc, -}; +use std::{collections::BTreeMap, fmt, sync::Arc}; type AIndexSet = IndexSet>; diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index ebb398f2c..90eede863 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -40,7 +40,6 @@ itertools.workspace = true serde.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true -rustc-hash.workspace = true tempfile.workspace = true rayon.workspace = true solang-parser.workspace = true diff --git a/crates/evm/traces/src/debug/mod.rs b/crates/evm/traces/src/debug/mod.rs index a651a3acc..a56f4ab2b 100644 --- a/crates/evm/traces/src/debug/mod.rs +++ b/crates/evm/traces/src/debug/mod.rs @@ -110,7 +110,7 @@ impl<'a> DebugStepsWalker<'a> { loc.index() == other_loc.index() } - /// Invoked when current step is a JUMPDEST preceeded by a JUMP marked as [Jump::In]. + /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::In]. fn jump_in(&mut self) { // This usually means that this is a jump into the external function which is an // entrypoint for the current frame. We don't want to include this to avoid @@ -128,7 +128,7 @@ impl<'a> DebugStepsWalker<'a> { } } - /// Invoked when current step is a JUMPDEST preceeded by a JUMP marked as [Jump::Out]. + /// Invoked when current step is a JUMPDEST preceded by a JUMP marked as [Jump::Out]. fn jump_out(&mut self) { let Some((i, _)) = self.stack.iter().enumerate().rfind(|(_, (_, step_idx))| { self.is_same_loc(*step_idx, self.current_step) || diff --git a/crates/evm/traces/src/debug/sources.rs b/crates/evm/traces/src/debug/sources.rs index 5087a06f3..40e540a97 100644 --- a/crates/evm/traces/src/debug/sources.rs +++ b/crates/evm/traces/src/debug/sources.rs @@ -11,7 +11,6 @@ use foundry_compilers::{ use foundry_evm_core::utils::PcIcMap; use foundry_linking::Linker; use rayon::prelude::*; -use rustc_hash::FxHashMap; use solang_parser::pt::SourceUnitPart; use std::{ collections::{BTreeMap, HashMap}, @@ -83,12 +82,14 @@ pub struct ArtifactData { impl ArtifactData { fn new(bytecode: ContractBytecodeSome, build_id: String, file_id: u32) -> Result { - let parse = |b: &Bytecode| { + let parse = |b: &Bytecode, name: &str| { // Only parse source map if it's not empty. let source_map = if b.source_map.as_ref().map_or(true, |s| s.is_empty()) { Ok(None) } else { - b.source_map().transpose() + b.source_map().transpose().wrap_err_with(|| { + format!("failed to parse {name} source map of file {file_id} in {build_id}") + }) }; // Only parse bytecode if it's not empty. @@ -100,11 +101,11 @@ impl ArtifactData { source_map.map(|source_map| (source_map, pc_ic_map)) }; - let (source_map, pc_ic_map) = parse(&bytecode.bytecode)?; + let (source_map, pc_ic_map) = parse(&bytecode.bytecode, "creation")?; let (source_map_runtime, pc_ic_map_runtime) = bytecode .deployed_bytecode .bytecode - .map(|b| parse(&b)) + .map(|b| parse(&b, "runtime")) .unwrap_or_else(|| Ok((None, None)))?; Ok(Self { source_map, source_map_runtime, pc_ic_map, pc_ic_map_runtime, build_id, file_id }) @@ -115,7 +116,7 @@ impl ArtifactData { #[derive(Clone, Debug, Default)] pub struct ContractSources { /// Map over build_id -> file_id -> (source code, language) - pub sources_by_id: HashMap>>, + pub sources_by_id: HashMap>>, /// Map over contract name -> Vec<(bytecode, build_id, file_id)> pub artifacts_by_name: HashMap>, } diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index da4adb555..e44ec61b9 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -7,7 +7,10 @@ use crate::{ }; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt, FunctionExt, JsonAbiExt}; use alloy_json_abi::{Error, Event, Function, JsonAbi}; -use alloy_primitives::{Address, LogData, Selector, B256}; +use alloy_primitives::{ + map::{hash_map::Entry, HashMap}, + Address, LogData, Selector, B256, +}; use foundry_cheatcodes_spec::Vm; use foundry_common::{ abi::get_indexed_event, fmt::format_token, get_contract_name, ContractsByArtifact, SELECTOR_LEN, @@ -24,9 +27,8 @@ use foundry_evm_core::{ use foundry_zksync_compiler::ZKSYNC_ARTIFACTS_DIR; use itertools::Itertools; use revm_inspectors::tracing::types::{DecodedCallLog, DecodedCallTrace}; -use rustc_hash::FxHashMap; use std::{ - collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, sync::OnceLock, }; @@ -123,7 +125,7 @@ pub struct CallTraceDecoder { pub receive_contracts: Vec
, /// All known functions. - pub functions: FxHashMap>, + pub functions: HashMap>, /// All known events. pub events: BTreeMap<(B256, usize), Vec>, /// Revert decoder. Contains all known custom errors. @@ -173,7 +175,7 @@ impl CallTraceDecoder { Self { contracts: Default::default(), - labels: [ + labels: HashMap::from_iter([ (CHEATCODE_ADDRESS, "VM".to_string()), (HARDHAT_CONSOLE_ADDRESS, "console".to_string()), (DEFAULT_CREATE2_DEPLOYER, "Create2Deployer".to_string()), @@ -189,8 +191,7 @@ impl CallTraceDecoder { (EC_PAIRING, "ECPairing".to_string()), (BLAKE_2F, "Blake2F".to_string()), (POINT_EVALUATION, "PointEvaluation".to_string()), - ] - .into(), + ]), receive_contracts: Default::default(), functions: hh_funcs() @@ -268,16 +269,18 @@ impl CallTraceDecoder { pub fn trace_addresses<'a>( &'a self, arena: &'a CallTraceArena, - ) -> impl Iterator)> + Clone + 'a { + ) -> impl Iterator, Option<&'a [u8]>)> + Clone + 'a { arena .nodes() .iter() .map(|node| { - let address = &node.trace.address; - let output = node.trace.kind.is_any_create().then_some(&node.trace.output[..]); - (address, output) + ( + &node.trace.address, + node.trace.kind.is_any_create().then_some(&node.trace.output[..]), + node.trace.kind.is_any_create().then_some(&node.trace.data[..]), + ) }) - .filter(|&(address, _)| { + .filter(|&(address, _, _)| { !self.labels.contains_key(address) || !self.contracts.contains_key(address) }) } @@ -527,6 +530,24 @@ impl CallTraceDecoder { Some(decoded.iter().map(format_token).collect()) } } + "createFork" | + "createSelectFork" | + "rpc" => { + let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..], false).ok()?; + + // Redact RPC URL except if referenced by an alias + if !decoded.is_empty() && func.inputs[0].ty == "string" { + let url_or_alias = decoded[0].as_str().unwrap_or_default(); + + if url_or_alias.starts_with("http") || url_or_alias.starts_with("ws") { + decoded[0] = DynSolValue::String("".to_string()); + } + } else { + return None; + } + + Some(decoded.iter().map(format_token).collect()) + } _ => None, } } @@ -569,6 +590,7 @@ impl CallTraceDecoder { "promptSecret" | "promptSecretUint" => Some(""), "parseJson" if self.verbosity < 5 => Some(""), "readFile" if self.verbosity < 5 => Some(""), + "rpcUrl" | "rpcUrls" | "rpcUrlStructs" => Some(""), _ => None, } .map(Into::into) @@ -681,7 +703,7 @@ mod tests { use alloy_primitives::hex; #[test] - fn test_should_redact_pk() { + fn test_should_redact() { let decoder = CallTraceDecoder::new(); // [function_signature, data, expected] @@ -737,6 +759,275 @@ mod tests { .to_string(), ]), ), + ( + // cast calldata "createFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" + "createFork(string)", + hex!( + " + 31ba3498 + 0000000000000000000000000000000000000000000000000000000000000020 + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"\"".to_string()]), + ), + ( + // cast calldata "createFork(string)" "wss://eth-mainnet.g.alchemy.com/v2/api_key" + "createFork(string)", + hex!( + " + 31ba3498 + 0000000000000000000000000000000000000000000000000000000000000020 + 000000000000000000000000000000000000000000000000000000000000002a + 7773733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f6d2f + 76322f6170695f6b657900000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"\"".to_string()]), + ), + ( + // cast calldata "createFork(string)" "mainnet" + "createFork(string)", + hex!( + " + 31ba3498 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"mainnet\"".to_string()]), + ), + ( + // cast calldata "createFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1 + "createFork(string,uint256)", + hex!( + " + 6ba3ba2b + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000001 + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"\"".to_string(), "1".to_string()]), + ), + ( + // cast calldata "createFork(string,uint256)" "mainnet" 1 + "createFork(string,uint256)", + hex!( + " + 6ba3ba2b + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"mainnet\"".to_string(), "1".to_string()]), + ), + ( + // cast calldata "createFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + "createFork(string,bytes32)", + hex!( + " + 7ca29682 + 0000000000000000000000000000000000000000000000000000000000000040 + ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"\"".to_string(), + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + .to_string(), + ]), + ), + ( + // cast calldata "createFork(string,bytes32)" "mainnet" + // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + "createFork(string,bytes32)", + hex!( + " + 7ca29682 + 0000000000000000000000000000000000000000000000000000000000000040 + ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"mainnet\"".to_string(), + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + .to_string(), + ]), + ), + ( + // cast calldata "createSelectFork(string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" + "createSelectFork(string)", + hex!( + " + 98680034 + 0000000000000000000000000000000000000000000000000000000000000020 + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"\"".to_string()]), + ), + ( + // cast calldata "createSelectFork(string)" "mainnet" + "createSelectFork(string)", + hex!( + " + 98680034 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"mainnet\"".to_string()]), + ), + ( + // cast calldata "createSelectFork(string,uint256)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 1 + "createSelectFork(string,uint256)", + hex!( + " + 71ee464d + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000001 + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"\"".to_string(), "1".to_string()]), + ), + ( + // cast calldata "createSelectFork(string,uint256)" "mainnet" 1 + "createSelectFork(string,uint256)", + hex!( + " + 71ee464d + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec!["\"mainnet\"".to_string(), "1".to_string()]), + ), + ( + // cast calldata "createSelectFork(string,bytes32)" "https://eth-mainnet.g.alchemy.com/v2/api_key" 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + "createSelectFork(string,bytes32)", + hex!( + " + 84d52b7a + 0000000000000000000000000000000000000000000000000000000000000040 + ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"\"".to_string(), + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + .to_string(), + ]), + ), + ( + // cast calldata "createSelectFork(string,bytes32)" "mainnet" + // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + "createSelectFork(string,bytes32)", + hex!( + " + 84d52b7a + 0000000000000000000000000000000000000000000000000000000000000040 + ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"mainnet\"".to_string(), + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + .to_string(), + ]), + ), + ( + // cast calldata "rpc(string,string,string)" "https://eth-mainnet.g.alchemy.com/v2/api_key" "eth_getBalance" "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]" + "rpc(string,string,string)", + hex!( + " + 0199a220 + 0000000000000000000000000000000000000000000000000000000000000060 + 00000000000000000000000000000000000000000000000000000000000000c0 + 0000000000000000000000000000000000000000000000000000000000000100 + 000000000000000000000000000000000000000000000000000000000000002c + 68747470733a2f2f6574682d6d61696e6e65742e672e616c6368656d792e636f + 6d2f76322f6170695f6b65790000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000000e + 6574685f67657442616c616e6365000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000034 + 5b22307835353165373738343737386566386530343865343935646634396632 + 363134663834613466316463222c22307830225d000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"\"".to_string(), + "\"eth_getBalance\"".to_string(), + "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\"" + .to_string(), + ]), + ), + ( + // cast calldata "rpc(string,string,string)" "mainnet" "eth_getBalance" + // "[\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\",\"0x0\"]" + "rpc(string,string,string)", + hex!( + " + 0199a220 + 0000000000000000000000000000000000000000000000000000000000000060 + 00000000000000000000000000000000000000000000000000000000000000a0 + 00000000000000000000000000000000000000000000000000000000000000e0 + 0000000000000000000000000000000000000000000000000000000000000007 + 6d61696e6e657400000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000000e + 6574685f67657442616c616e6365000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000034 + 5b22307835353165373738343737386566386530343865343935646634396632 + 363134663834613466316463222c22307830225d000000000000000000000000 + " + ) + .to_vec(), + Some(vec![ + "\"mainnet\"".to_string(), + "\"eth_getBalance\"".to_string(), + "\"[\\\"0x551e7784778ef8e048e495df49f2614f84a4f1dc\\\",\\\"0x0\\\"]\"" + .to_string(), + ]), + ), ]; // [function_signature, expected] @@ -744,6 +1035,10 @@ mod tests { // Should redact private key on output in all cases: ("createWallet(string)", Some("".to_string())), ("deriveKey(string,uint32)", Some("".to_string())), + // Should redact RPC URL if defined, except if referenced by an alias: + ("rpcUrl(string)", Some("".to_string())), + ("rpcUrls()", Some("".to_string())), + ("rpcUrlStructs()", Some("".to_string())), ]; for (function_signature, data, expected) in cheatcode_input_test_cases { diff --git a/crates/evm/traces/src/folded_stack_trace.rs b/crates/evm/traces/src/folded_stack_trace.rs index de76f8e80..ee3a05afa 100644 --- a/crates/evm/traces/src/folded_stack_trace.rs +++ b/crates/evm/traces/src/folded_stack_trace.rs @@ -30,14 +30,13 @@ impl EvmFoldedStackTraceBuilder { let node = &nodes[idx]; let func_name = if node.trace.kind.is_any_create() { - let default_contract_name = "Contract".to_string(); - let contract_name = node.trace.decoded.label.as_ref().unwrap_or(&default_contract_name); + let contract_name = node.trace.decoded.label.as_deref().unwrap_or("Contract"); format!("new {contract_name}") } else { let selector = node .selector() .map(|selector| selector.encode_hex_with_prefix()) - .unwrap_or("fallback".to_string()); + .unwrap_or_else(|| "fallback".to_string()); let signature = node.trace.decoded.call_data.as_ref().map(|dc| &dc.signature).unwrap_or(&selector); @@ -114,9 +113,11 @@ impl EvmFoldedStackTraceBuilder { /// Helps to translate a function enter-exit flow into a folded stack trace. /// /// Example: -/// fn top() { child_a(); child_b() } // consumes 500 gas -/// fn child_a() {} // consumes 100 gas -/// fn child_b() {} // consumes 200 gas +/// ```solidity +/// function top() { child_a(); child_b() } // consumes 500 gas +/// function child_a() {} // consumes 100 gas +/// function child_b() {} // consumes 200 gas +/// ``` /// /// For execution of the `top` function looks like: /// 1. enter `top` diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index 4a34b31b3..2af6fbb59 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -97,7 +97,7 @@ impl EtherscanIdentifier { impl TraceIdentifier for EtherscanIdentifier { fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> where - A: Iterator)>, + A: Iterator, Option<&'a [u8]>)>, { trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1); @@ -114,7 +114,7 @@ impl TraceIdentifier for EtherscanIdentifier { Arc::clone(&self.invalid_api_key), ); - for (addr, _) in addresses { + for (addr, _, _) in addresses { if let Some(metadata) = self.contracts.get(addr) { let label = metadata.contract_name.clone(); let abi = metadata.abi().ok().map(Cow::Owned); diff --git a/crates/evm/traces/src/identifier/local.rs b/crates/evm/traces/src/identifier/local.rs index d21022d96..fec31e4ae 100644 --- a/crates/evm/traces/src/identifier/local.rs +++ b/crates/evm/traces/src/identifier/local.rs @@ -1,9 +1,9 @@ use super::{AddressIdentity, TraceIdentifier}; use alloy_json_abi::JsonAbi; -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::Address; use foundry_common::contracts::{bytecode_diff_score, ContractsByArtifact}; use foundry_compilers::ArtifactId; -use std::{borrow::Cow, collections::HashMap}; +use std::borrow::Cow; /// A trace identifier that tries to identify addresses using local contracts. pub struct LocalTraceIdentifier<'a> { @@ -11,8 +11,6 @@ pub struct LocalTraceIdentifier<'a> { known_contracts: &'a ContractsByArtifact, /// Vector of pairs of artifact ID and the runtime code length of the given artifact. ordered_ids: Vec<(&'a ArtifactId, usize)>, - /// Deployments generated during the setup - pub deployments: HashMap, } impl<'a> LocalTraceIdentifier<'a> { @@ -25,7 +23,7 @@ impl<'a> LocalTraceIdentifier<'a> { .map(|(id, bytecode)| (id, bytecode.len())) .collect::>(); ordered_ids.sort_by_key(|(_, len)| *len); - Self { known_contracts, ordered_ids, deployments: HashMap::new() } + Self { known_contracts, ordered_ids } } /// Returns the known contracts. @@ -34,23 +32,34 @@ impl<'a> LocalTraceIdentifier<'a> { self.known_contracts } - /// Tries to the bytecode most similar to the given one. - pub fn identify_code(&self, code: &[u8]) -> Option<(&'a ArtifactId, &'a JsonAbi)> { - let len = code.len(); + /// Identifies the artifact based on score computed for both creation and deployed bytecodes. + pub fn identify_code( + &self, + runtime_code: &[u8], + creation_code: &[u8], + ) -> Option<(&'a ArtifactId, &'a JsonAbi)> { + let len = runtime_code.len(); let mut min_score = f64::MAX; let mut min_score_id = None; - let mut check = |id| { + let mut check = |id, is_creation, min_score: &mut f64| { let contract = self.known_contracts.get(id)?; - if let Some(deployed_bytecode) = contract.deployed_bytecode() { - let score = bytecode_diff_score(deployed_bytecode, code); + // Select bytecodes to compare based on `is_creation` flag. + let (contract_bytecode, current_bytecode) = if is_creation { + (contract.bytecode(), creation_code) + } else { + (contract.deployed_bytecode(), runtime_code) + }; + + if let Some(bytecode) = contract_bytecode { + let score = bytecode_diff_score(bytecode, current_bytecode); if score == 0.0 { trace!(target: "evm::traces", "found exact match"); return Some((id, &contract.abi)); } - if score < min_score { - min_score = score; + if score < *min_score { + *min_score = score; min_score_id = Some((id, &contract.abi)); } } @@ -67,7 +76,7 @@ impl<'a> LocalTraceIdentifier<'a> { if len > max_len { break; } - if let found @ Some(_) = check(id) { + if let found @ Some(_) = check(id, true, &mut min_score) { return found; } } @@ -77,11 +86,20 @@ impl<'a> LocalTraceIdentifier<'a> { let idx = self.find_index(min_len); for i in idx..same_length_idx { let (id, _) = self.ordered_ids[i]; - if let found @ Some(_) = check(id) { + if let found @ Some(_) = check(id, true, &mut min_score) { return found; } } + // Fallback to comparing deployed code if min score greater than threshold. + if min_score >= 0.85 { + for (artifact, _) in &self.ordered_ids { + if let found @ Some(_) = check(artifact, false, &mut min_score) { + return found; + } + } + } + trace!(target: "evm::traces", %min_score, "no exact match found"); // Note: the diff score can be inaccurate for small contracts so we're using a relatively @@ -111,17 +129,16 @@ impl<'a> LocalTraceIdentifier<'a> { impl TraceIdentifier for LocalTraceIdentifier<'_> { fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> where - A: Iterator)>, + A: Iterator, Option<&'a [u8]>)>, { trace!(target: "evm::traces", "identify {:?} addresses", addresses.size_hint().1); addresses - .filter_map(|(address, code)| { + .filter_map(|(address, runtime_code, creation_code)| { let _span = trace_span!(target: "evm::traces", "identify", %address).entered(); trace!(target: "evm::traces", "identifying"); - let (id, abi) = self.identify_code(code?).or_else(|| { - self.deployments.get(address).and_then(|bytes| self.identify_code(bytes)) - })?; + let (id, abi) = self.identify_code(runtime_code?, creation_code?)?; + trace!(target: "evm::traces", id=%id.identifier(), "identified"); Some(AddressIdentity { diff --git a/crates/evm/traces/src/identifier/mod.rs b/crates/evm/traces/src/identifier/mod.rs index a16b108d8..008e5f841 100644 --- a/crates/evm/traces/src/identifier/mod.rs +++ b/crates/evm/traces/src/identifier/mod.rs @@ -35,7 +35,7 @@ pub trait TraceIdentifier { /// Attempts to identify an address in one or more call traces. fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> where - A: Iterator)> + Clone; + A: Iterator, Option<&'a [u8]>)> + Clone; } /// A collection of trace identifiers. @@ -55,7 +55,7 @@ impl Default for TraceIdentifiers<'_> { impl TraceIdentifier for TraceIdentifiers<'_> { fn identify_addresses<'a, A>(&mut self, addresses: A) -> Vec> where - A: Iterator)> + Clone, + A: Iterator, Option<&'a [u8]>)> + Clone, { let mut identities = Vec::new(); if let Some(local) = &mut self.local { diff --git a/crates/evm/traces/src/identifier/signatures.rs b/crates/evm/traces/src/identifier/signatures.rs index 2a2f6a2f2..1e3924aa3 100644 --- a/crates/evm/traces/src/identifier/signatures.rs +++ b/crates/evm/traces/src/identifier/signatures.rs @@ -1,16 +1,12 @@ use alloy_json_abi::{Event, Function}; -use alloy_primitives::hex; +use alloy_primitives::{hex, map::HashSet}; use foundry_common::{ abi::{get_event, get_func}, fs, selectors::{OpenChainClient, SelectorType}, }; use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashSet}, - path::PathBuf, - sync::Arc, -}; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; use tokio::sync::RwLock; pub type SingleSignaturesIdentifier = Arc>; @@ -56,12 +52,12 @@ impl SignaturesIdentifier { } CachedSignatures::default() }; - Self { cached, cached_path: Some(path), unavailable: HashSet::new(), client } + Self { cached, cached_path: Some(path), unavailable: HashSet::default(), client } } else { Self { cached: Default::default(), cached_path: None, - unavailable: HashSet::new(), + unavailable: HashSet::default(), client, } }; @@ -161,6 +157,7 @@ impl Drop for SignaturesIdentifier { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 2557ac73a..dd4a89664 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -8,7 +8,6 @@ #[macro_use] extern crate tracing; -use alloy_primitives::{Address, Bytes}; use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use revm::interpreter::OpCode; use revm_inspectors::tracing::{ @@ -18,10 +17,12 @@ use revm_inspectors::tracing::{ use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, - collections::{BTreeSet, HashMap}, + collections::BTreeSet, ops::{Deref, DerefMut}, }; +use alloy_primitives::map::HashMap; + pub use revm_inspectors::tracing::{ types::{ CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog, @@ -179,7 +180,15 @@ pub async fn decode_trace_arena( /// Render a collection of call traces to a string. pub fn render_trace_arena(arena: &SparsedTraceArena) -> String { - let mut w = TraceWriter::new(Vec::::new()); + render_trace_arena_with_bytecodes(arena, false) +} + +/// Render a collection of call traces to a string optionally including contract creation bytecodes. +pub fn render_trace_arena_with_bytecodes( + arena: &SparsedTraceArena, + with_bytecodes: bool, +) -> String { + let mut w = TraceWriter::new(Vec::::new()).write_bytecodes(with_bytecodes); w.write_arena(&arena.resolve_arena()).expect("Failed to write traces"); String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8") } @@ -222,10 +231,8 @@ impl TraceKind { pub fn load_contracts<'a>( traces: impl IntoIterator, known_contracts: &ContractsByArtifact, - deployments: &HashMap, ) -> ContractsByAddress { let mut local_identifier = LocalTraceIdentifier::new(known_contracts); - local_identifier.deployments.clone_from(deployments); let decoder = CallTraceDecoder::new(); let mut contracts = ContractsByAddress::new(); for trace in traces { diff --git a/crates/fmt/src/buffer.rs b/crates/fmt/src/buffer.rs index b09e9e620..9226d5f6b 100644 --- a/crates/fmt/src/buffer.rs +++ b/crates/fmt/src/buffer.rs @@ -173,7 +173,7 @@ impl FormatBuffer { let mut comment_state = self.state.comment_state(); while let Some(line) = lines.next() { // remove the whitespace that covered by the base indent length (this is normally the - // case with temporary buffers as this will be readded by the underlying IndentWriter + // case with temporary buffers as this will be re-added by the underlying IndentWriter // later on let (new_comment_state, line_start) = line .comment_state_char_indices() diff --git a/crates/fmt/src/comments.rs b/crates/fmt/src/comments.rs index 5b9d7fb27..90054f132 100644 --- a/crates/fmt/src/comments.rs +++ b/crates/fmt/src/comments.rs @@ -409,7 +409,7 @@ impl std::iter::FusedIterator for CommentStateCharIndices<'_> {} /// An Iterator over characters in a string slice which are not a apart of comments pub struct NonCommentChars<'a>(CommentStateCharIndices<'a>); -impl<'a> Iterator for NonCommentChars<'a> { +impl Iterator for NonCommentChars<'_> { type Item = char; #[inline] diff --git a/crates/fmt/src/formatter.rs b/crates/fmt/src/formatter.rs index 2ae083e5d..02d62f705 100644 --- a/crates/fmt/src/formatter.rs +++ b/crates/fmt/src/formatter.rs @@ -1872,7 +1872,7 @@ impl<'a, W: Write> Formatter<'a, W> { } // Traverse the Solidity Parse Tree and write to the code formatter -impl<'a, W: Write> Visitor for Formatter<'a, W> { +impl Visitor for Formatter<'_, W> { type Error = FormatterError; #[instrument(name = "source", skip(self))] @@ -2340,8 +2340,11 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> { "" }; let closing_bracket = format!("{prefix}{}", "}"); - let closing_bracket_loc = args.last().unwrap().loc.end(); - write_chunk!(self, closing_bracket_loc, "{closing_bracket}")?; + if let Some(arg) = args.last() { + write_chunk!(self, arg.loc.end(), "{closing_bracket}")?; + } else { + write_chunk!(self, "{closing_bracket}")?; + } Ok(()) } @@ -3244,7 +3247,7 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> { let is_constructor = self.context.is_constructor_function(); // we can't make any decisions here regarding trailing `()` because we'd need to // find out if the `base` is a solidity modifier or an - // interface/contract therefor we we its raw content. + // interface/contract therefore we we its raw content. // we can however check if the contract `is` the `base`, this however also does // not cover all cases @@ -3843,14 +3846,14 @@ struct Transaction<'f, 'a, W> { comments: Comments, } -impl<'f, 'a, W> std::ops::Deref for Transaction<'f, 'a, W> { +impl<'a, W> std::ops::Deref for Transaction<'_, 'a, W> { type Target = Formatter<'a, W>; fn deref(&self) -> &Self::Target { self.fmt } } -impl<'f, 'a, W> std::ops::DerefMut for Transaction<'f, 'a, W> { +impl std::ops::DerefMut for Transaction<'_, '_, W> { fn deref_mut(&mut self) -> &mut Self::Target { self.fmt } diff --git a/crates/fmt/src/solang_ext/loc.rs b/crates/fmt/src/solang_ext/loc.rs index 2fcbaf995..54bf771c6 100644 --- a/crates/fmt/src/solang_ext/loc.rs +++ b/crates/fmt/src/solang_ext/loc.rs @@ -10,19 +10,19 @@ pub trait CodeLocationExt { fn loc(&self) -> pt::Loc; } -impl<'a, T: ?Sized + CodeLocationExt> CodeLocationExt for &'a T { +impl CodeLocationExt for &T { fn loc(&self) -> pt::Loc { (**self).loc() } } -impl<'a, T: ?Sized + CodeLocationExt> CodeLocationExt for &'a mut T { +impl CodeLocationExt for &mut T { fn loc(&self) -> pt::Loc { (**self).loc() } } -impl<'a, T: ?Sized + ToOwned + CodeLocationExt> CodeLocationExt for Cow<'a, T> { +impl CodeLocationExt for Cow<'_, T> { fn loc(&self) -> pt::Loc { (**self).loc() } diff --git a/crates/fmt/src/string.rs b/crates/fmt/src/string.rs index 1dbc2f2f6..ae570a39b 100644 --- a/crates/fmt/src/string.rs +++ b/crates/fmt/src/string.rs @@ -38,7 +38,7 @@ impl<'a> QuoteStateCharIndices<'a> { } } -impl<'a> Iterator for QuoteStateCharIndices<'a> { +impl Iterator for QuoteStateCharIndices<'_> { type Item = (QuoteState, usize, char); fn next(&mut self) -> Option { let (idx, ch) = self.iter.next()?; @@ -68,14 +68,14 @@ impl<'a> Iterator for QuoteStateCharIndices<'a> { /// An iterator over the indices of quoted string locations pub struct QuotedRanges<'a>(QuoteStateCharIndices<'a>); -impl<'a> QuotedRanges<'a> { +impl QuotedRanges<'_> { pub fn with_state(mut self, state: QuoteState) -> Self { self.0 = self.0.with_state(state); self } } -impl<'a> Iterator for QuotedRanges<'a> { +impl Iterator for QuotedRanges<'_> { type Item = (char, usize, usize); fn next(&mut self) -> Option { let (quote, start) = loop { diff --git a/crates/fmt/src/visit.rs b/crates/fmt/src/visit.rs index b977ba5b3..3fff8893d 100644 --- a/crates/fmt/src/visit.rs +++ b/crates/fmt/src/visit.rs @@ -5,7 +5,7 @@ use crate::solang_ext::pt::*; /// A trait that is invoked while traversing the Solidity Parse Tree. /// Each method of the [Visitor] trait is a hook that can be potentially overridden. /// -/// Currently the main implementor of this trait is the [`Formatter`](crate::Formatter<'_>) struct. +/// Currently the main implementer of this trait is the [`Formatter`](crate::Formatter<'_>) struct. pub trait Visitor { type Error: std::error::Error; diff --git a/crates/fmt/testdata/Repros/fmt.sol b/crates/fmt/testdata/Repros/fmt.sol index a61a626a0..0a480c0b0 100644 --- a/crates/fmt/testdata/Repros/fmt.sol +++ b/crates/fmt/testdata/Repros/fmt.sol @@ -144,3 +144,18 @@ contract IfElseTest { } } } + +contract DbgFmtTest is Test { + function test_argsList() public { + uint256 result1 = internalNoArgs({}); + result2 = add({a: 1, b: 2}); + } + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + function internalNoArgs() internal pure returns (uint256) { + return 0; + } +} diff --git a/crates/fmt/testdata/Repros/original.sol b/crates/fmt/testdata/Repros/original.sol index ef75931e2..6f18784d3 100644 --- a/crates/fmt/testdata/Repros/original.sol +++ b/crates/fmt/testdata/Repros/original.sol @@ -143,3 +143,18 @@ contract IfElseTest { } } } + +contract DbgFmtTest is Test { + function test_argsList() public { + uint256 result1 = internalNoArgs({}); + result2 = add({a: 1, b: 2}); + } + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + function internalNoArgs() internal pure returns (uint256) { + return 0; + } +} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 42ec8cdca..fa6b5ad55 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -25,7 +25,6 @@ vergen = { workspace = true, default-features = false, features = [ ] } [dependencies] -globset = "0.4" # lib foundry-block-explorers = { workspace = true, features = ["foundry-compilers"] } @@ -103,7 +102,6 @@ watchexec-events = "3.0" watchexec-signals = "3.0" clearscreen = "3.0" evm-disassembler.workspace = true -rustc-hash.workspace = true # doc server axum = { workspace = true, features = ["ws"] } @@ -112,7 +110,7 @@ tower-http = { workspace = true, features = ["fs"] } opener = "0.7" # soldeer -soldeer.workspace = true +soldeer-commands.workspace = true quick-junit = "0.5.0" # zk @@ -127,7 +125,7 @@ anvil.workspace = true foundry-test-utils.workspace = true mockall = "0.13" -criterion = "0.5" +globset = "0.4" paste = "1.0" path-slash = "0.2" similar-asserts.workspace = true @@ -152,7 +150,3 @@ asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["dep:tikv-jemallocator"] aws-kms = ["foundry-wallets/aws-kms"] isolate-by-default = ["foundry-config/isolate-by-default"] - -[[bench]] -name = "test" -harness = false diff --git a/crates/forge/README.md b/crates/forge/README.md index d4cfeede2..a40a852db 100644 --- a/crates/forge/README.md +++ b/crates/forge/README.md @@ -206,7 +206,7 @@ contract MyTest { function testBarExpectedRevert() public { vm.expectRevert("My expected revert string"); - // This would fail *if* we didnt expect revert. Since we expect the revert, + // This would fail *if* we didn't expect revert. Since we expect the revert, // it doesn't, unless the revert string is wrong. foo.bar(101); } diff --git a/crates/forge/benches/test.rs b/crates/forge/benches/test.rs deleted file mode 100644 index 593bce3d3..000000000 --- a/crates/forge/benches/test.rs +++ /dev/null @@ -1,29 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use foundry_test_utils::{ - util::{lossy_string, setup_forge_remote}, - TestCommand, TestProject, -}; - -/// Returns a cloned and `forge built` `solmate` project -fn built_solmate() -> (TestProject, TestCommand) { - setup_forge_remote("transmissions11/solmate") -} - -fn forge_test_benchmark(c: &mut Criterion) { - let (prj, _) = built_solmate(); - - let mut group = c.benchmark_group("forge test"); - group.sample_size(10); - group.bench_function("solmate", |b| { - let mut cmd = prj.forge_command(); - cmd.arg("test"); - b.iter(|| { - let output = cmd.execute(); - println!("stdout:\n{}", lossy_string(&output.stdout)); - println!("\nstderr:\n{}", lossy_string(&output.stderr)); - }); - }); -} - -criterion_group!(benches, forge_test_benchmark); -criterion_main!(benches); diff --git a/crates/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index d5d236332..d8739b42d 100644 --- a/crates/forge/bin/cmd/bind.rs +++ b/crates/forge/bin/cmd/bind.rs @@ -1,3 +1,4 @@ +use alloy_primitives::map::HashSet; use clap::{Parser, ValueHint}; use ethers_contract_abigen::{ Abigen, ContractFilter, ExcludeContracts, MultiAbigen, SelectContracts, @@ -244,7 +245,7 @@ impl BindArgs { } fn get_solmacrogen(&self, artifacts: &Path) -> Result { - let mut dup = std::collections::HashSet::::new(); + let mut dup = HashSet::::default(); let instances = self .get_json_files(artifacts)? .filter_map(|(name, path)| { diff --git a/crates/forge/bin/cmd/bind_json.rs b/crates/forge/bin/cmd/bind_json.rs index bd2d0ea30..2736325a7 100644 --- a/crates/forge/bin/cmd/bind_json.rs +++ b/crates/forge/bin/cmd/bind_json.rs @@ -187,7 +187,7 @@ struct StructToWrite { } impl StructToWrite { - /// Returns the name of the imported item. If struct is definied at the file level, returns the + /// Returns the name of the imported item. If struct is defined at the file level, returns the /// struct name, otherwise returns the parent contract name. fn struct_or_contract_name(&self) -> &str { self.contract_name.as_deref().unwrap_or(&self.name) @@ -309,10 +309,7 @@ impl CompiledState { for ((path, id), (def, contract_name)) in structs { // For some structs there's no schema (e.g. if they contain a mapping), so we just skip // those. - let Some(schema) = resolver.resolve_struct_eip712(id, &mut Default::default(), true)? - else { - continue - }; + let Some(schema) = resolver.resolve_struct_eip712(id)? else { continue }; if !include.is_empty() { if !include.iter().any(|matcher| matcher.is_match(path)) { diff --git a/crates/forge/bin/cmd/clone.rs b/crates/forge/bin/cmd/clone.rs index 9fddceb70..b71c5d3c5 100644 --- a/crates/forge/bin/cmd/clone.rs +++ b/crates/forge/bin/cmd/clone.rs @@ -609,6 +609,7 @@ impl EtherscanClient for Client { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; use alloy_primitives::hex; diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 55d47f0ab..17e4c088d 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -1,5 +1,5 @@ use super::{install, test::TestArgs}; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{map::HashMap, Address, Bytes, U256}; use clap::{Parser, ValueEnum, ValueHint}; use eyre::{Context, Result}; use forge::{ @@ -25,10 +25,8 @@ use foundry_compilers::{ use foundry_config::{Config, SolcReq}; use foundry_zksync_compiler::DualCompiledContracts; use rayon::prelude::*; -use rustc_hash::FxHashMap; use semver::Version; use std::{ - collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; @@ -151,7 +149,7 @@ impl CoverageArgs { // Collect source files. let project_paths = &project.paths; - let mut versioned_sources = HashMap::>::new(); + let mut versioned_sources = HashMap::>::default(); for (path, source_file, version) in output.output().sources.sources_with_version() { report.add_source(version.clone(), source_file.id as usize, path.clone()); @@ -192,7 +190,7 @@ impl CoverageArgs { let source_analysis = SourceAnalyzer::new(sources).analyze()?; // Build helper mapping used by `find_anchors` - let mut items_by_source_id = FxHashMap::<_, Vec<_>>::with_capacity_and_hasher( + let mut items_by_source_id = HashMap::<_, Vec<_>>::with_capacity_and_hasher( source_analysis.items.len(), Default::default(), ); @@ -411,7 +409,7 @@ impl BytecodeData { pub fn find_anchors( &self, source_analysis: &SourceAnalysis, - items_by_source_id: &FxHashMap>, + items_by_source_id: &HashMap>, ) -> Vec { find_anchors( &self.bytecode, diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 3cf92ed8d..9f0212ae7 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -261,7 +261,7 @@ impl CreateArgs { } else { // Deploy with signer // Avoid initializing `signer` twice as it will error out with Ledger - // and potentailly other devices that rely on HID too + // and potentially other devices that rely on HID too let zk_signer = self.eth.wallet.signer().await?; let deployer = zk_signer.address(); let provider = ProviderBuilder::<_, _, AnyNetwork>::default().on_provider(provider); @@ -520,8 +520,11 @@ impl CreateArgs { println!("Starting contract verification..."); - let num_of_optimizations = - if self.opts.compiler.optimize { self.opts.compiler.optimizer_runs } else { None }; + let num_of_optimizations = if self.opts.compiler.optimize.unwrap_or_default() { + self.opts.compiler.optimizer_runs + } else { + None + }; let verify = forge_verify::VerifyArgs { address, contract: Some(self.contract), @@ -612,7 +615,7 @@ impl CreateArgs { .await?; deployer.tx.set_gas_limit(if let Some(gas_limit) = self.tx.gas_limit { - gas_limit.to::() + gas_limit.to::() } else { estimated_gas.limit }); @@ -667,8 +670,11 @@ impl CreateArgs { println!("Starting contract verification..."); - let num_of_optimizations = - if self.opts.compiler.optimize { self.opts.compiler.optimizer_runs } else { None }; + let num_of_optimizations = if self.opts.compiler.optimize.unwrap_or_default() { + self.opts.compiler.optimizer_runs + } else { + None + }; let verify = forge_verify::VerifyArgs { address, contract: Some(self.contract), diff --git a/crates/forge/bin/cmd/doc/server.rs b/crates/forge/bin/cmd/doc/server.rs index f5991ba43..72585a2bf 100644 --- a/crates/forge/bin/cmd/doc/server.rs +++ b/crates/forge/bin/cmd/doc/server.rs @@ -90,6 +90,7 @@ impl Server { } } +#[allow(clippy::needless_return)] #[tokio::main] async fn serve(build_dir: PathBuf, address: SocketAddr, file_404: &str) -> io::Result<()> { let file_404 = build_dir.join(file_404); diff --git a/crates/forge/bin/cmd/eip712.rs b/crates/forge/bin/cmd/eip712.rs index 4fa2a165f..e9ef88008 100644 --- a/crates/forge/bin/cmd/eip712.rs +++ b/crates/forge/bin/cmd/eip712.rs @@ -11,7 +11,7 @@ use foundry_compilers::{ }, CompilerSettings, }; -use std::{collections::BTreeMap, path::PathBuf}; +use std::{collections::BTreeMap, fmt::Write, path::PathBuf}; foundry_config::impl_figment_convert!(Eip712Args, opts); @@ -62,9 +62,7 @@ impl Eip712Args { }; for (id, _) in structs_in_target { - if let Some(resolved) = - resolver.resolve_struct_eip712(id, &mut Default::default(), true)? - { + if let Some(resolved) = resolver.resolve_struct_eip712(id)? { println!("{resolved}"); println!(); } @@ -128,14 +126,19 @@ impl Resolver { /// /// Returns `None` if struct contains any fields that are not supported by EIP-712 (e.g. /// mappings or function pointers). - pub fn resolve_struct_eip712( + pub fn resolve_struct_eip712(&self, id: usize) -> Result> { + self.resolve_eip712_inner(id, &mut Default::default(), true, None) + } + + fn resolve_eip712_inner( &self, id: usize, subtypes: &mut BTreeMap, append_subtypes: bool, + rename: Option<&str>, ) -> Result> { let def = &self.structs[&id]; - let mut result = format!("{}(", def.name); + let mut result = format!("{}(", rename.unwrap_or(&def.name)); for (idx, member) in def.members.iter().enumerate() { let Some(ty) = self.resolve_type( @@ -146,9 +149,7 @@ impl Resolver { return Ok(None) }; - result.push_str(&ty); - result.push(' '); - result.push_str(&member.name); + write!(result, "{ty} {name}", name = member.name)?; if idx < def.members.len() - 1 { result.push(','); @@ -161,11 +162,14 @@ impl Resolver { return Ok(Some(result)) } - for subtype_id in subtypes.values().copied().collect::>() { + for (subtype_name, subtype_id) in + subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::>() + { if subtype_id == id { continue } - let Some(encoded_subtype) = self.resolve_struct_eip712(subtype_id, subtypes, false)? + let Some(encoded_subtype) = + self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))? else { return Ok(None) }; @@ -204,6 +208,17 @@ impl Resolver { name.clone() // Otherwise, try assigning a new name. } else { + // iterate over members to check if they are resolvable and to populate subtypes + for member in &def.members { + if self.resolve_type( + member.type_name.as_ref().ok_or_eyre("missing type name")?, + subtypes, + )? + .is_none() + { + return Ok(None) + } + } let mut i = 0; let mut name = def.name.clone(); while subtypes.contains_key(&name) { diff --git a/crates/forge/bin/cmd/geiger/find.rs b/crates/forge/bin/cmd/geiger/find.rs index 6629390ca..e3cd65413 100644 --- a/crates/forge/bin/cmd/geiger/find.rs +++ b/crates/forge/bin/cmd/geiger/find.rs @@ -47,7 +47,7 @@ pub struct SolFileMetricsPrinter<'a, 'b> { pub root: &'b Path, } -impl<'a, 'b> fmt::Display for SolFileMetricsPrinter<'a, 'b> { +impl fmt::Display for SolFileMetricsPrinter<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let SolFileMetricsPrinter { metrics, root } = *self; diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index d3097854c..14d43d6f5 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -52,7 +52,7 @@ impl InspectArgs { // Run Optimized? let optimized = if field == ContractArtifactField::AssemblyOptimized { - true + Some(true) } else { build.compiler.optimize }; @@ -101,6 +101,9 @@ impl InspectArgs { ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => { print_json_str(&artifact.assembly, None)?; } + ContractArtifactField::LegacyAssembly => { + print_json_str(&artifact.legacy_assembly, None)?; + } ContractArtifactField::MethodIdentifiers => { print_json(&artifact.method_identifiers)?; } @@ -210,6 +213,7 @@ pub enum ContractArtifactField { DeployedBytecode, Assembly, AssemblyOptimized, + LegacyAssembly, MethodIdentifiers, GasEstimates, StorageLayout, @@ -292,6 +296,7 @@ impl_value_enum! { DeployedBytecode => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode" | "deployed" | "deployedbytecode", Assembly => "assembly" | "asm", + LegacyAssembly => "legacyAssembly" | "legacyassembly" | "legacy_assembly", AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized" | "assembly_optimized" | "asmopt" | "assembly-optimized" | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized", @@ -324,6 +329,7 @@ impl From for ContractOutputSelection { DeployedBytecodeOutputSelection::All, )), Caf::Assembly | Caf::AssemblyOptimized => Self::Evm(EvmOutputSelection::Assembly), + Caf::LegacyAssembly => Self::Evm(EvmOutputSelection::LegacyAssembly), Caf::MethodIdentifiers => Self::Evm(EvmOutputSelection::MethodIdentifiers), Caf::GasEstimates => Self::Evm(EvmOutputSelection::GasEstimates), Caf::StorageLayout => Self::StorageLayout, @@ -354,6 +360,7 @@ impl PartialEq for ContractArtifactField { (Self::Bytecode, Cos::Evm(Eos::ByteCode(_))) | (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_))) | (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly)) | + (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly)) | (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers)) | (Self::GasEstimates, Cos::Evm(Eos::GasEstimates)) | (Self::StorageLayout, Cos::StorageLayout) | diff --git a/crates/forge/bin/cmd/snapshot.rs b/crates/forge/bin/cmd/snapshot.rs index 8c3d57fc3..0d7c2843a 100644 --- a/crates/forge/bin/cmd/snapshot.rs +++ b/crates/forge/bin/cmd/snapshot.rs @@ -1,5 +1,5 @@ use super::test; -use alloy_primitives::U256; +use alloy_primitives::{map::HashMap, U256}; use clap::{builder::RangedU64ValueParser, Parser, ValueHint}; use eyre::{Context, Result}; use forge::result::{SuiteTestResult, TestKindReport, TestOutcome}; @@ -7,7 +7,6 @@ use foundry_cli::utils::STATIC_FUZZ_SEED; use regex::Regex; use std::{ cmp::Ordering, - collections::HashMap, fs, io::{self, BufRead}, path::{Path, PathBuf}, @@ -24,8 +23,8 @@ pub static RE_BASIC_SNAPSHOT_ENTRY: LazyLock = LazyLock::new(|| { /// CLI arguments for `forge snapshot`. #[derive(Clone, Debug, Parser)] -pub struct SnapshotArgs { - /// Output a diff against a pre-existing snapshot. +pub struct GasSnapshotArgs { + /// Output a diff against a pre-existing gas snapshot. /// /// By default, the comparison is done with .gas-snapshot. #[arg( @@ -36,9 +35,9 @@ pub struct SnapshotArgs { )] diff: Option>, - /// Compare against a pre-existing snapshot, exiting with code 1 if they do not match. + /// Compare against a pre-existing gas snapshot, exiting with code 1 if they do not match. /// - /// Outputs a diff if the snapshots do not match. + /// Outputs a diff if the gas snapshots do not match. /// /// By default, the comparison is done with .gas-snapshot. #[arg( @@ -54,7 +53,7 @@ pub struct SnapshotArgs { #[arg(long, hide(true))] format: Option, - /// Output file for the snapshot. + /// Output file for the gas snapshot. #[arg( long, default_value = ".gas-snapshot", @@ -77,11 +76,11 @@ pub struct SnapshotArgs { /// Additional configs for test results #[command(flatten)] - config: SnapshotConfig, + config: GasSnapshotConfig, } -impl SnapshotArgs { - /// Returns whether `SnapshotArgs` was configured with `--watch` +impl GasSnapshotArgs { + /// Returns whether `GasSnapshotArgs` was configured with `--watch` pub fn is_watch(&self) -> bool { self.test.is_watch() } @@ -102,18 +101,18 @@ impl SnapshotArgs { if let Some(path) = self.diff { let snap = path.as_ref().unwrap_or(&self.snap); - let snaps = read_snapshot(snap)?; + let snaps = read_gas_snapshot(snap)?; diff(tests, snaps)?; } else if let Some(path) = self.check { let snap = path.as_ref().unwrap_or(&self.snap); - let snaps = read_snapshot(snap)?; + let snaps = read_gas_snapshot(snap)?; if check(tests, snaps, self.tolerance) { std::process::exit(0) } else { std::process::exit(1) } } else { - write_to_snapshot_file(&tests, self.snap, self.format)?; + write_to_gas_snapshot_file(&tests, self.snap, self.format)?; } Ok(()) } @@ -138,7 +137,7 @@ impl FromStr for Format { /// Additional filters that can be applied on the test results #[derive(Clone, Debug, Default, Parser)] -struct SnapshotConfig { +struct GasSnapshotConfig { /// Sort results by gas used (ascending). #[arg(long)] asc: bool, @@ -156,7 +155,7 @@ struct SnapshotConfig { max: Option, } -impl SnapshotConfig { +impl GasSnapshotConfig { fn is_in_gas_range(&self, gas_used: u64) -> bool { if let Some(min) = self.min { if gas_used < min { @@ -187,20 +186,20 @@ impl SnapshotConfig { } } -/// A general entry in a snapshot file +/// A general entry in a gas snapshot file /// /// Has the form: /// `(gas:? 40181)` for normal tests /// `(runs: 256, μ: 40181, ~: 40181)` for fuzz tests /// `(runs: 256, calls: 40181, reverts: 40181)` for invariant tests #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SnapshotEntry { +pub struct GasSnapshotEntry { pub contract_name: String, pub signature: String, pub gas_used: TestKindReport, } -impl FromStr for SnapshotEntry { +impl FromStr for GasSnapshotEntry { type Err = String; fn from_str(s: &str) -> Result { @@ -253,8 +252,8 @@ impl FromStr for SnapshotEntry { } } -/// Reads a list of snapshot entries from a snapshot file -fn read_snapshot(path: impl AsRef) -> Result> { +/// Reads a list of gas snapshot entries from a gas snapshot file. +fn read_gas_snapshot(path: impl AsRef) -> Result> { let path = path.as_ref(); let mut entries = Vec::new(); for line in io::BufReader::new( @@ -263,13 +262,14 @@ fn read_snapshot(path: impl AsRef) -> Result> { ) .lines() { - entries.push(SnapshotEntry::from_str(line?.as_str()).map_err(|err| eyre::eyre!("{err}"))?); + entries + .push(GasSnapshotEntry::from_str(line?.as_str()).map_err(|err| eyre::eyre!("{err}"))?); } Ok(entries) } -/// Writes a series of tests to a snapshot file after sorting them -fn write_to_snapshot_file( +/// Writes a series of tests to a gas snapshot file after sorting them. +fn write_to_gas_snapshot_file( tests: &[SuiteTestResult], path: impl AsRef, _format: Option, @@ -288,15 +288,15 @@ fn write_to_snapshot_file( Ok(fs::write(path, content)?) } -/// A Snapshot entry diff +/// A Gas snapshot entry diff. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SnapshotDiff { +pub struct GasSnapshotDiff { pub signature: String, pub source_gas_used: TestKindReport, pub target_gas_used: TestKindReport, } -impl SnapshotDiff { +impl GasSnapshotDiff { /// Returns the gas diff /// /// `> 0` if the source used more gas @@ -311,10 +311,14 @@ impl SnapshotDiff { } } -/// Compares the set of tests with an existing snapshot +/// Compares the set of tests with an existing gas snapshot. /// /// Returns true all tests match -fn check(tests: Vec, snaps: Vec, tolerance: Option) -> bool { +fn check( + tests: Vec, + snaps: Vec, + tolerance: Option, +) -> bool { let snaps = snaps .into_iter() .map(|s| ((s.contract_name, s.signature), s.gas_used)) @@ -347,8 +351,8 @@ fn check(tests: Vec, snaps: Vec, tolerance: Opti !has_diff } -/// Compare the set of tests with an existing snapshot -fn diff(tests: Vec, snaps: Vec) -> Result<()> { +/// Compare the set of tests with an existing gas snapshot. +fn diff(tests: Vec, snaps: Vec) -> Result<()> { let snaps = snaps .into_iter() .map(|s| ((s.contract_name, s.signature), s.gas_used)) @@ -358,7 +362,7 @@ fn diff(tests: Vec, snaps: Vec) -> Result<()> { if let Some(target_gas_used) = snaps.get(&(test.contract_name().to_string(), test.signature.clone())).cloned() { - diffs.push(SnapshotDiff { + diffs.push(GasSnapshotDiff { source_gas_used: test.result.kind.report(), signature: test.signature, target_gas_used, @@ -446,12 +450,12 @@ mod tests { } #[test] - fn can_parse_basic_snapshot_entry() { + fn can_parse_basic_gas_snapshot_entry() { let s = "Test:deposit() (gas: 7222)"; - let entry = SnapshotEntry::from_str(s).unwrap(); + let entry = GasSnapshotEntry::from_str(s).unwrap(); assert_eq!( entry, - SnapshotEntry { + GasSnapshotEntry { contract_name: "Test".to_string(), signature: "deposit()".to_string(), gas_used: TestKindReport::Unit { gas: 7222 } @@ -460,12 +464,12 @@ mod tests { } #[test] - fn can_parse_fuzz_snapshot_entry() { + fn can_parse_fuzz_gas_snapshot_entry() { let s = "Test:deposit() (runs: 256, μ: 100, ~:200)"; - let entry = SnapshotEntry::from_str(s).unwrap(); + let entry = GasSnapshotEntry::from_str(s).unwrap(); assert_eq!( entry, - SnapshotEntry { + GasSnapshotEntry { contract_name: "Test".to_string(), signature: "deposit()".to_string(), gas_used: TestKindReport::Fuzz { runs: 256, median_gas: 200, mean_gas: 100 } @@ -474,12 +478,12 @@ mod tests { } #[test] - fn can_parse_invariant_snapshot_entry() { + fn can_parse_invariant_gas_snapshot_entry() { let s = "Test:deposit() (runs: 256, calls: 100, reverts: 200)"; - let entry = SnapshotEntry::from_str(s).unwrap(); + let entry = GasSnapshotEntry::from_str(s).unwrap(); assert_eq!( entry, - SnapshotEntry { + GasSnapshotEntry { contract_name: "Test".to_string(), signature: "deposit()".to_string(), gas_used: TestKindReport::Invariant { runs: 256, calls: 100, reverts: 200 } @@ -488,12 +492,12 @@ mod tests { } #[test] - fn can_parse_invariant_snapshot_entry2() { + fn can_parse_invariant_gas_snapshot_entry2() { let s = "ERC20Invariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2388)"; - let entry = SnapshotEntry::from_str(s).unwrap(); + let entry = GasSnapshotEntry::from_str(s).unwrap(); assert_eq!( entry, - SnapshotEntry { + GasSnapshotEntry { contract_name: "ERC20Invariants".to_string(), signature: "invariantBalanceSum()".to_string(), gas_used: TestKindReport::Invariant { runs: 256, calls: 3840, reverts: 2388 } diff --git a/crates/forge/bin/cmd/soldeer.rs b/crates/forge/bin/cmd/soldeer.rs index 5482bdc6f..56b8c31f3 100644 --- a/crates/forge/bin/cmd/soldeer.rs +++ b/crates/forge/bin/cmd/soldeer.rs @@ -1,7 +1,7 @@ use clap::Parser; use eyre::Result; -use soldeer::commands::Subcommands; +use soldeer_commands::Command; // CLI arguments for `forge soldeer`. // The following list of commands and their actions: @@ -22,14 +22,14 @@ use soldeer::commands::Subcommands; override_usage = "Native Solidity Package Manager, `run forge soldeer [COMMAND] --help` for more details" )] pub struct SoldeerArgs { - /// Command must be one of the following install/push/login/update/version. + /// Command must be one of the following init/install/login/push/uninstall/update/version. #[command(subcommand)] - command: Subcommands, + command: Command, } impl SoldeerArgs { - pub fn run(self) -> Result<()> { - match soldeer::run(self.command) { + pub async fn run(self) -> Result<()> { + match soldeer_commands::run(self.command).await { Ok(_) => Ok(()), Err(err) => Err(eyre::eyre!("Failed to run soldeer {}", err)), } @@ -38,11 +38,12 @@ impl SoldeerArgs { #[cfg(test)] mod tests { - use super::*; - use soldeer::commands::Version; + use soldeer_commands::{commands::Version, Command}; - #[test] - fn test_soldeer_version() { - assert!(soldeer::run(Subcommands::Version(Version {})).is_ok()); + #[tokio::test] + #[allow(clippy::needless_return)] + async fn test_soldeer_version() { + let command = Command::Version(Version::default()); + assert!(soldeer_commands::run(command).await.is_ok()); } } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 286f30a1c..db66a5fdb 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -5,7 +5,7 @@ use clap::{Parser, ValueHint}; use eyre::{Context, OptionExt, Result}; use forge::{ decode::decode_console_logs, - gas_report::GasReport, + gas_report::{GasReport, GasReportKind}, multi_runner::matches_contract, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ @@ -20,7 +20,7 @@ use foundry_cli::{ opts::CoreBuildArgs, utils::{self, LoadConfig}, }; -use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, fs, shell}; +use foundry_common::{cli_warn, compile::ProjectCompiler, evm::EvmArgs, fs, shell}; use foundry_compilers::{ artifacts::output_selection::OutputSelection, compilers::{multi::MultiCompilerLanguage, CompilerSettings, Language}, @@ -68,44 +68,36 @@ pub struct TestArgs { #[arg(value_hint = ValueHint::FilePath)] pub path: Option, - /// Run a test in the debugger. - /// - /// The argument passed to this flag is the name of the test function you want to run, and it - /// works the same as --match-test. - /// - /// If more than one test matches your specified criteria, you must add additional filters - /// until only one test is found (see --match-contract and --match-path). + /// Run a single test in the debugger. /// /// The matching test will be opened in the debugger regardless of the outcome of the test. /// /// If the matching test is a fuzz test, then it will open the debugger on the first failure - /// case. - /// If the fuzz test does not fail, it will open the debugger on the last fuzz case. - /// - /// For more fine-grained control of which fuzz case is run, see forge run. - #[arg(long, value_name = "TEST_FUNCTION")] - debug: Option, + /// case. If the fuzz test does not fail, it will open the debugger on the last fuzz case. + #[arg(long, value_name = "DEPRECATED_TEST_FUNCTION_REGEX")] + debug: Option>, /// Generate a flamegraph for a single test. Implies `--decode-internal`. - #[arg(long, conflicts_with = "flamechart")] + /// + /// A flame graph is used to visualize which functions or operations within the smart contract + /// are consuming the most gas overall in a sorted manner. + #[arg(long)] flamegraph: bool, /// Generate a flamechart for a single test. Implies `--decode-internal`. + /// + /// A flame chart shows the gas usage over time, illustrating when each function is + /// called (execution order) and how much gas it consumes at each point in the timeline. #[arg(long, conflicts_with = "flamegraph")] flamechart: bool, - /// Whether to identify internal functions in traces. - /// - /// If no argument is passed to this flag, it will trace internal functions scope and decode - /// stack parameters, but parameters stored in memory (such as bytes or arrays) will not be - /// decoded. + /// Identify internal functions in traces. /// - /// To decode memory parameters, you should pass an argument with a test function name, - /// similarly to --debug and --match-test. + /// This will trace internal functions and decode stack parameters. /// - /// If more than one test matches your specified criteria, you must add additional filters - /// until only one test is found (see --match-contract and --match-path). - #[arg(long, value_name = "TEST_FUNCTION")] + /// Parameters stored in memory (such as bytes or arrays) are currently decoded only when a + /// single function is matched, similarly to `--debug`, for performance reasons. + #[arg(long, value_name = "DEPRECATED_TEST_FUNCTION_REGEX")] decode_internal: Option>, /// Print a gas report. @@ -121,7 +113,7 @@ pub struct TestArgs { json: bool, /// Output test results as JUnit XML report. - #[arg(long, conflicts_with = "json", help_heading = "Display options")] + #[arg(long, conflicts_with_all(["json", "gas_report"]), help_heading = "Display options")] junit: bool, /// Stop running tests after the first failure. @@ -196,7 +188,7 @@ impl TestArgs { /// Returns sources which include any tests to be executed. /// If no filters are provided, sources are filtered by existence of test/invariant methods in - /// them, If filters are provided, sources are additionaly filtered by them. + /// them, If filters are provided, sources are additionally filtered by them. pub fn get_sources_to_compile( &self, config: &Config, @@ -338,6 +330,17 @@ impl TestArgs { let toml = config.get_config_path(); let profiles = get_available_profiles(toml)?; + // Remove the snapshots directory if it exists. + // This is to ensure that we don't have any stale snapshots. + // If `FORGE_SNAPSHOT_CHECK` is set, we don't remove the snapshots directory as it is + // required for comparison. + if std::env::var("FORGE_SNAPSHOT_CHECK").is_err() { + let snapshot_dir = project_root.join(&config.snapshots); + if snapshot_dir.exists() { + let _ = fs::remove_dir_all(project_root.join(&config.snapshots)); + } + } + let test_options: TestOptions = TestOptionsBuilder::default() .fuzz(config.fuzz.clone()) .invariant(config.invariant.clone()) @@ -356,19 +359,15 @@ impl TestArgs { let env = evm_opts.evm_env().await?; // Enable internal tracing for more informative flamegraph. - if should_draw { + if should_draw && self.decode_internal.is_none() { self.decode_internal = Some(None); } // Choose the internal function tracing mode, if --decode-internal is provided. - let decode_internal = if let Some(maybe_fn) = self.decode_internal.as_ref() { - if maybe_fn.is_some() { - // If function filter is provided, we enable full tracing. - InternalTraceMode::Full - } else { - // If no function filter is provided, we enable simple tracing. - InternalTraceMode::Simple - } + let decode_internal = if self.decode_internal.is_some() { + // If more than one function matched, we enable simple tracing. + // If only one function matched, we enable full tracing. This is done in `run_tests`. + InternalTraceMode::Simple } else { InternalTraceMode::None }; @@ -395,13 +394,18 @@ impl TestArgs { dual_compiled_contracts.unwrap_or_default(), )?; - let mut maybe_override_mt = |flag, maybe_regex: Option<&Regex>| { - if let Some(regex) = maybe_regex { + let mut maybe_override_mt = |flag, maybe_regex: Option<&Option>| { + if let Some(Some(regex)) = maybe_regex { + cli_warn!( + "specifying argument for --{flag} is deprecated and will be removed in the future, \ + use --match-test instead" + ); + let test_pattern = &mut filter.args_mut().test_pattern; if test_pattern.is_some() { eyre::bail!( "Cannot specify both --{flag} and --match-test. \ - Use --match-contract and --match-path to further limit the search instead." + Use --match-contract and --match-path to further limit the search instead." ); } *test_pattern = Some(regex.clone()); @@ -409,12 +413,8 @@ impl TestArgs { Ok(()) }; - maybe_override_mt("debug", self.debug.as_ref())?; - maybe_override_mt( - "decode-internal", - self.decode_internal.as_ref().and_then(|v| v.as_ref()), - )?; + maybe_override_mt("decode-internal", self.decode_internal.as_ref())?; let libraries = runner.libraries.clone(); let mut outcome = self.run_tests(runner, config, verbosity, &filter, &output).await?; @@ -423,18 +423,10 @@ impl TestArgs { let (suite_name, test_name, mut test_result) = outcome.remove_first().ok_or_eyre("no tests were executed")?; - let arena = test_result + let (_, arena) = test_result .traces .iter_mut() - .find_map( - |(kind, arena)| { - if *kind == TraceKind::Execution { - Some(arena) - } else { - None - } - }, - ) + .find(|(kind, _)| *kind == TraceKind::Execution) .unwrap(); // Decode traces. @@ -447,6 +439,7 @@ impl TestArgs { let test_name = test_name.trim_end_matches("()"); let file_name = format!("cache/{label}_{contract}_{test_name}.svg"); let file = std::fs::File::create(&file_name).wrap_err("failed to create file")?; + let file = std::io::BufWriter::new(file); let mut options = inferno::flamegraph::Options::default(); options.title = format!("{label} {contract}::{test_name}"); @@ -457,13 +450,13 @@ impl TestArgs { } // Generate SVG. - inferno::flamegraph::from_lines(&mut options, fst.iter().map(|s| s.as_str()), file) + inferno::flamegraph::from_lines(&mut options, fst.iter().map(String::as_str), file) .wrap_err("failed to write svg")?; println!("\nSaved to {file_name}"); // Open SVG in default program. - if opener::open(&file_name).is_err() { - println!("\nFailed to open {file_name}. Please open it manually."); + if let Err(e) = opener::open(&file_name) { + eprintln!("\nFailed to open {file_name}; please open it manually: {e}"); } } @@ -508,23 +501,48 @@ impl TestArgs { trace!(target: "forge::test", "running all tests"); + // If we need to render to a serialized format, we should not print anything else to stdout. + let silent = self.gas_report && self.json; + let num_filtered = runner.matching_test_functions(filter).count(); - if (self.debug.is_some() || - self.decode_internal.as_ref().map_or(false, |v| v.is_some()) || - self.flamegraph || - self.flamechart) && - num_filtered != 1 - { + if num_filtered != 1 && (self.debug.is_some() || self.flamegraph || self.flamechart) { + let action = if self.flamegraph { + "generate a flamegraph" + } else if self.flamechart { + "generate a flamechart" + } else { + "run the debugger" + }; + let filter = if filter.is_empty() { + String::new() + } else { + format!("\n\nFilter used:\n{filter}") + }; eyre::bail!( "{num_filtered} tests matched your criteria, but exactly 1 test must match in order to {action}.\n\n\ - Use --match-contract and --match-path to further limit the search.\n\ - Filter used:\n{filter}", - action = if self.flamegraph {"generate a flamegraph"} else if self.flamechart {"generate a flamechart"} else {"run the debugger"}, + Use --match-contract and --match-path to further limit the search.{filter}", ); } - if self.json { - let results = runner.test_collect(filter); + // If exactly one test matched, we enable full tracing. + if num_filtered == 1 && self.decode_internal.is_some() { + runner.decode_internal = InternalTraceMode::Full; + } + + // Run tests in a non-streaming fashion and collect results for serialization. + if !self.gas_report && self.json { + let mut results = runner.test_collect(filter); + results.values_mut().for_each(|suite_result| { + for test_result in suite_result.test_results.values_mut() { + if verbosity >= 2 { + // Decode logs at level 2 and above. + test_result.decoded_logs = decode_console_logs(&test_result.logs); + } else { + // Empty logs for non verbose runs. + test_result.logs = vec![]; + } + } + }); println!("{}", serde_json::to_string(&results)?); return Ok(TestOutcome::new(results, self.allow_failure)); } @@ -540,7 +558,7 @@ impl TestArgs { let libraries = runner.libraries.clone(); - // Run tests. + // Run tests in a streaming fashion. let (tx, rx) = channel::<(String, SuiteResult)>(); let timer = Instant::now(); let show_progress = config.show_progress; @@ -577,9 +595,15 @@ impl TestArgs { } let mut decoder = builder.build(); - let mut gas_report = self - .gas_report - .then(|| GasReport::new(config.gas_reports.clone(), config.gas_reports_ignore.clone())); + let mut gas_report = self.gas_report.then(|| { + GasReport::new( + config.gas_reports.clone(), + config.gas_reports_ignore.clone(), + if self.json { GasReportKind::JSON } else { GasReportKind::Markdown }, + ) + }); + + let mut gas_snapshots = BTreeMap::>::new(); let mut outcome = TestOutcome::empty(self.allow_failure); @@ -598,30 +622,34 @@ impl TestArgs { self.flamechart; // Print suite header. - println!(); - for warning in suite_result.warnings.iter() { - eprintln!("{} {warning}", "Warning:".yellow().bold()); - } - if !tests.is_empty() { - let len = tests.len(); - let tests = if len > 1 { "tests" } else { "test" }; - println!("Ran {len} {tests} for {contract_name}"); + if !silent { + println!(); + for warning in suite_result.warnings.iter() { + eprintln!("{} {warning}", "Warning:".yellow().bold()); + } + if !tests.is_empty() { + let len = tests.len(); + let tests = if len > 1 { "tests" } else { "test" }; + println!("Ran {len} {tests} for {contract_name}"); + } } // Process individual test results, printing logs and traces when necessary. for (name, result) in tests { - shell::println(result.short_result(name))?; - - // We only display logs at level 2 and above - if verbosity >= 2 { - // We only decode logs from Hardhat and DS-style console events - let console_logs = decode_console_logs(&result.logs); - if !console_logs.is_empty() { - println!("Logs:"); - for log in console_logs { - println!(" {log}"); + if !silent { + shell::println(result.short_result(name))?; + + // We only display logs at level 2 and above + if verbosity >= 2 { + // We only decode logs from Hardhat and DS-style console events + let console_logs = decode_console_logs(&result.logs); + if !console_logs.is_empty() { + println!("Logs:"); + for log in console_logs { + println!(" {log}"); + } + println!(); } - println!(); } } @@ -663,7 +691,7 @@ impl TestArgs { } } - if !decoded_traces.is_empty() { + if !silent && !decoded_traces.is_empty() { shell::println("Traces:")?; for trace in &decoded_traces { shell::println(trace)?; @@ -690,10 +718,89 @@ impl TestArgs { } } } + + // Collect and merge gas snapshots. + for (group, new_snapshots) in result.gas_snapshots.iter() { + gas_snapshots.entry(group.clone()).or_default().extend(new_snapshots.clone()); + } + } + + // Write gas snapshots to disk if any were collected. + if !gas_snapshots.is_empty() { + // Check for differences in gas snapshots if `FORGE_SNAPSHOT_CHECK` is set. + // Exiting early with code 1 if differences are found. + if std::env::var("FORGE_SNAPSHOT_CHECK").is_ok() { + let differences_found = gas_snapshots.clone().into_iter().fold( + false, + |mut found, (group, snapshots)| { + // If the snapshot file doesn't exist, we can't compare so we skip. + if !&config.snapshots.join(format!("{group}.json")).exists() { + return false; + } + + let previous_snapshots: BTreeMap = + fs::read_json_file(&config.snapshots.join(format!("{group}.json"))) + .expect("Failed to read snapshots from disk"); + + let diff: BTreeMap<_, _> = snapshots + .iter() + .filter_map(|(k, v)| { + previous_snapshots.get(k).and_then(|previous_snapshot| { + if previous_snapshot != v { + Some(( + k.clone(), + (previous_snapshot.clone(), v.clone()), + )) + } else { + None + } + }) + }) + .collect(); + + if !diff.is_empty() { + println!( + "{}", + format!("\n[{group}] Failed to match snapshots:").red().bold() + ); + + for (key, (previous_snapshot, snapshot)) in &diff { + println!( + "{}", + format!("- [{key}] {previous_snapshot} → {snapshot}").red() + ); + } + + found = true; + } + + found + }, + ); + + if differences_found { + println!(); + eyre::bail!("Snapshots differ from previous run"); + } + } + + // Create `snapshots` directory if it doesn't exist. + fs::create_dir_all(&config.snapshots)?; + + // Write gas snapshots to disk per group. + gas_snapshots.clone().into_iter().for_each(|(group, snapshots)| { + fs::write_pretty_json_file( + &config.snapshots.join(format!("{group}.json")), + &snapshots, + ) + .expect("Failed to write gas snapshots to disk"); + }); } // Print suite summary. - shell::println(suite_result.summary())?; + if !silent { + shell::println(suite_result.summary())?; + } // Add the suite result to the outcome. outcome.results.insert(contract_name, suite_result); @@ -714,7 +821,7 @@ impl TestArgs { outcome.gas_report = Some(finalized); } - if !outcome.results.is_empty() { + if !silent && !outcome.results.is_empty() { shell::println(outcome.summary(duration))?; if self.summary { @@ -996,7 +1103,7 @@ contract FooBarTest is DSTest { let call_cnts = gas_report .contracts .values() - .flat_map(|c| c.functions.values().flat_map(|f| f.values().map(|v| v.calls.len()))) + .flat_map(|c| c.functions.values().flat_map(|f| f.values().map(|v| v.frames.len()))) .collect::>(); // assert that all functions were called at least 100 times assert!(call_cnts.iter().all(|c| *c > 100)); diff --git a/crates/forge/bin/cmd/watch.rs b/crates/forge/bin/cmd/watch.rs index ff1c9c6c1..861002fd3 100644 --- a/crates/forge/bin/cmd/watch.rs +++ b/crates/forge/bin/cmd/watch.rs @@ -1,11 +1,11 @@ -use super::{build::BuildArgs, doc::DocArgs, snapshot::SnapshotArgs, test::TestArgs}; +use super::{build::BuildArgs, doc::DocArgs, snapshot::GasSnapshotArgs, test::TestArgs}; +use alloy_primitives::map::HashSet; use clap::Parser; use eyre::Result; use foundry_cli::utils::{self, FoundryPathExt}; use foundry_config::Config; use parking_lot::Mutex; use std::{ - collections::HashSet, path::PathBuf, sync::{ atomic::{AtomicU8, Ordering}, @@ -249,7 +249,7 @@ pub async fn watch_build(args: BuildArgs) -> Result<()> { /// Executes a [`Watchexec`] that listens for changes in the project's src dir and reruns `forge /// snapshot` -pub async fn watch_snapshot(args: SnapshotArgs) -> Result<()> { +pub async fn watch_gas_snapshot(args: GasSnapshotArgs) -> Result<()> { let config = args.watchexec_config()?; run(config).await } @@ -265,7 +265,7 @@ pub async fn watch_test(args: TestArgs) -> Result<()> { filter.args().contract_pattern.is_some() || args.watch.run_all; - let last_test_files = Mutex::new(HashSet::::new()); + let last_test_files = Mutex::new(HashSet::::default()); let project_root = config.root.0.to_string_lossy().into_owned(); let config = args.watch.watchexec_config_with_override( || [&config.test, &config.src], diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 206eb730a..1253d2682 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -91,7 +91,7 @@ fn main() -> Result<()> { } ForgeSubcommand::Snapshot(cmd) => { if cmd.is_watch() { - utils::block_on(watch::watch_snapshot(cmd)) + utils::block_on(watch::watch_gas_snapshot(cmd)) } else { utils::block_on(cmd.run()) } @@ -121,7 +121,7 @@ fn main() -> Result<()> { ForgeSubcommand::Generate(cmd) => match cmd.sub { GenerateSubcommands::Test(cmd) => cmd.run(), }, - ForgeSubcommand::Soldeer(cmd) => cmd.run(), + ForgeSubcommand::Soldeer(cmd) => utils::block_on(cmd.run()), ForgeSubcommand::Eip712(cmd) => cmd.run(), ForgeSubcommand::BindJson(cmd) => cmd.run(), } diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index fbe1f67df..c929d0185 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -123,9 +123,9 @@ pub enum ForgeSubcommand { /// Manage the Foundry cache. Cache(CacheArgs), - /// Create a snapshot of each test's gas usage. + /// Create a gas snapshot of each test's gas usage. #[command(visible_alias = "s")] - Snapshot(snapshot::SnapshotArgs), + Snapshot(snapshot::GasSnapshotArgs), /// Display the current config. #[command(visible_alias = "co")] diff --git a/crates/forge/src/coverage.rs b/crates/forge/src/coverage.rs index 135636d3c..4a00675dd 100644 --- a/crates/forge/src/coverage.rs +++ b/crates/forge/src/coverage.rs @@ -1,15 +1,17 @@ //! Coverage reports. +use alloy_primitives::map::HashMap; use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, Color, Row, Table}; use evm_disassembler::disassemble_bytes; use foundry_common::fs; -pub use foundry_evm::coverage::*; use std::{ - collections::{hash_map, HashMap}, + collections::hash_map, io::Write, path::{Path, PathBuf}, }; +pub use foundry_evm::coverage::*; + /// A coverage reporter. pub trait CoverageReporter { fn report(self, report: &CoverageReport) -> eyre::Result<()>; @@ -86,7 +88,7 @@ impl<'a> LcovReporter<'a> { } } -impl<'a> CoverageReporter for LcovReporter<'a> { +impl CoverageReporter for LcovReporter<'_> { fn report(self, report: &CoverageReport) -> eyre::Result<()> { for (file, items) in report.items_by_source() { let summary = items.iter().fold(CoverageSummary::default(), |mut summary, item| { @@ -268,7 +270,7 @@ struct LineNumberCache { impl LineNumberCache { pub fn new(root: PathBuf) -> Self { - Self { root, line_offsets: HashMap::new() } + Self { root, line_offsets: HashMap::default() } } pub fn get_position(&mut self, path: &Path, offset: usize) -> eyre::Result<(usize, usize)> { diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index 0cfe4b8bf..6088f5678 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -4,22 +4,34 @@ use crate::{ constants::CHEATCODE_ADDRESS, traces::{CallTraceArena, CallTraceDecoder, CallTraceNode, DecodedCallData}, }; +use alloy_primitives::map::HashSet; use comfy_table::{presets::ASCII_MARKDOWN, *}; use foundry_common::{calc, TestFunctionExt}; use foundry_evm::traces::CallKind; use foundry_evm_abi::HARDHAT_CONSOLE_ADDRESS; use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashSet}, - fmt::Display, -}; +use std::{collections::BTreeMap, fmt::Display}; use yansi::Paint; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum GasReportKind { + Markdown, + JSON, +} + +impl Default for GasReportKind { + fn default() -> Self { + Self::Markdown + } +} + /// Represents the gas report for a set of contracts. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct GasReport { /// Whether to report any contracts. report_any: bool, + /// What kind of report to generate. + report_type: GasReportKind, /// Contracts to generate the report for. report_for: HashSet, /// Contracts to ignore when generating the report. @@ -33,11 +45,13 @@ impl GasReport { pub fn new( report_for: impl IntoIterator, ignore: impl IntoIterator, + report_kind: GasReportKind, ) -> Self { let report_for = report_for.into_iter().collect::>(); let ignore = ignore.into_iter().collect::>(); let report_any = report_for.is_empty() || report_for.contains("*"); - Self { report_any, report_for, ignore, ..Default::default() } + let report_type = report_kind; + Self { report_any, report_type, report_for, ignore, ..Default::default() } } /// Whether the given contract should be reported. @@ -80,7 +94,7 @@ impl GasReport { return; } - // Only include top-level calls which accout for calldata and base (21.000) cost. + // Only include top-level calls which account for calldata and base (21.000) cost. // Only include Calls and Creates as only these calls are isolated in inspector. if trace.depth > 1 && (trace.kind == CallKind::Call || @@ -122,7 +136,7 @@ impl GasReport { .or_default() .entry(signature.clone()) .or_default(); - gas_info.calls.push(trace.gas_used); + gas_info.frames.push(trace.gas_used); } } } @@ -134,11 +148,12 @@ impl GasReport { for contract in self.contracts.values_mut() { for sigs in contract.functions.values_mut() { for func in sigs.values_mut() { - func.calls.sort_unstable(); - func.min = func.calls.first().copied().unwrap_or_default(); - func.max = func.calls.last().copied().unwrap_or_default(); - func.mean = calc::mean(&func.calls); - func.median = calc::median_sorted(&func.calls); + func.frames.sort_unstable(); + func.min = func.frames.first().copied().unwrap_or_default(); + func.max = func.frames.last().copied().unwrap_or_default(); + func.mean = calc::mean(&func.frames); + func.median = calc::median_sorted(&func.frames); + func.calls = func.frames.len() as u64; } } } @@ -154,6 +169,11 @@ impl Display for GasReport { continue; } + if self.report_type == GasReportKind::JSON { + writeln!(f, "{}", serde_json::to_string(&contract).unwrap())?; + continue; + } + let mut table = Table::new(); table.load_preset(ASCII_MARKDOWN); table.set_header([Cell::new(format!("{name} contract")) @@ -185,7 +205,7 @@ impl Display for GasReport { Cell::new(gas_info.mean.to_string()).fg(Color::Yellow), Cell::new(gas_info.median.to_string()).fg(Color::Yellow), Cell::new(gas_info.max.to_string()).fg(Color::Red), - Cell::new(gas_info.calls.len().to_string()), + Cell::new(gas_info.calls.to_string()), ]); }) }); @@ -206,9 +226,12 @@ pub struct ContractInfo { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct GasInfo { - pub calls: Vec, + pub calls: u64, pub min: u64, pub mean: u64, pub median: u64, pub max: u64, + + #[serde(skip)] + pub frames: Vec, } diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 0761b9054..66fb2e3e1 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -252,7 +252,7 @@ impl MultiContractRunner { &self.config, self.evm_opts.clone(), Some(self.known_contracts.clone()), - None, + Some(artifact_id.name.clone()), Some(artifact_id.version.clone()), self.dual_compiled_contracts.clone(), self.use_zk, diff --git a/crates/forge/src/progress.rs b/crates/forge/src/progress.rs index 8d047c413..2b45d5513 100644 --- a/crates/forge/src/progress.rs +++ b/crates/forge/src/progress.rs @@ -1,7 +1,7 @@ +use alloy_primitives::map::HashMap; use indicatif::{MultiProgress, ProgressBar}; use parking_lot::Mutex; -use std::{collections::HashMap, sync::Arc, time::Duration}; - +use std::{sync::Arc, time::Duration}; /// State of [ProgressBar]s displayed for the given test run. /// Shows progress of all test suites matching filter. /// For each test within the test suite an individual progress bar is displayed. diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index e611529be..4fb88dfd0 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -4,7 +4,10 @@ use crate::{ fuzz::{BaseCounterExample, FuzzedCases}, gas_report::GasReport, }; -use alloy_primitives::{Address, Bytes, Log}; +use alloy_primitives::{ + map::{AddressHashMap, HashMap}, + Address, Log, +}; use eyre::Report; use foundry_common::{evm::Breakpoints, get_contract_name, get_file_name, shell}; use foundry_evm::{ @@ -16,7 +19,7 @@ use foundry_evm::{ }; use serde::{Deserialize, Serialize}; use std::{ - collections::{BTreeMap, HashMap}, + collections::BTreeMap, fmt::{self, Write}, time::Duration, }; @@ -215,8 +218,26 @@ impl SuiteResult { pub fn new( duration: Duration, test_results: BTreeMap, - warnings: Vec, + mut warnings: Vec, ) -> Self { + // Add deprecated cheatcodes warning, if any of them used in current test suite. + let mut deprecated_cheatcodes = HashMap::new(); + for test_result in test_results.values() { + deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone()); + } + if !deprecated_cheatcodes.is_empty() { + let mut warning = + "the following cheatcode(s) are deprecated and will be removed in future versions:" + .to_string(); + for (cheatcode, reason) in deprecated_cheatcodes { + write!(warning, "\n {cheatcode}").unwrap(); + if let Some(reason) = reason { + write!(warning, ": {reason}").unwrap(); + } + } + warnings.push(warning); + } + Self { duration, test_results, warnings } } @@ -368,11 +389,14 @@ pub struct TestResult { /// be printed to the user. pub logs: Vec, + /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs). + /// Used for json output. + pub decoded_logs: Vec, + /// What kind of test this was pub kind: TestKind, /// Traces - #[serde(skip)] pub traces: Traces, /// Additional traces to use for gas report. @@ -384,12 +408,19 @@ pub struct TestResult { pub coverage: Option, /// Labeled addresses - pub labeled_addresses: HashMap, + pub labeled_addresses: AddressHashMap, pub duration: Duration, /// pc breakpoint char map pub breakpoints: Breakpoints, + + /// Any captured gas snapshots along the test's execution which should be accumulated. + pub gas_snapshots: BTreeMap>, + + /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test. + #[serde(skip)] + pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>, } impl fmt::Display for TestResult { @@ -501,9 +532,15 @@ impl TestResult { false => TestStatus::Failure, }; self.reason = reason; - self.breakpoints = raw_call_result.cheatcodes.map(|c| c.breakpoints).unwrap_or_default(); self.duration = Duration::default(); self.gas_report_traces = Vec::new(); + + if let Some(cheatcodes) = raw_call_result.cheatcodes { + self.breakpoints = cheatcodes.breakpoints; + self.gas_snapshots = cheatcodes.gas_snapshots; + self.deprecated_cheatcodes = cheatcodes.deprecated; + } + self } @@ -535,6 +572,7 @@ impl TestResult { self.duration = Duration::default(); self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect(); self.breakpoints = result.breakpoints.unwrap_or_default(); + self.deprecated_cheatcodes = result.deprecated_cheatcodes; self } @@ -703,8 +741,6 @@ impl TestKind { #[derive(Clone, Debug, Default)] pub struct TestSetup { - /// Deployments generated during the setup - pub deployments: HashMap, /// The address at which the test contract was deployed pub address: Address, /// The logs emitted during setup @@ -712,7 +748,7 @@ pub struct TestSetup { /// Call traces of the setup pub traces: Traces, /// Addresses labeled during setup - pub labeled_addresses: HashMap, + pub labeled_addresses: AddressHashMap, /// The reason the setup failed, if it did pub reason: Option, /// Coverage info during setup @@ -726,7 +762,7 @@ impl TestSetup { error: EvmError, mut logs: Vec, mut traces: Traces, - mut labeled_addresses: HashMap, + mut labeled_addresses: AddressHashMap, ) -> Self { match error { EvmError::Execution(err) => { @@ -746,34 +782,23 @@ impl TestSetup { } pub fn success( - deployments: HashMap, address: Address, logs: Vec, traces: Traces, - labeled_addresses: HashMap, + labeled_addresses: AddressHashMap, coverage: Option, fuzz_fixtures: FuzzFixtures, ) -> Self { - Self { - deployments, - address, - logs, - traces, - labeled_addresses, - reason: None, - coverage, - fuzz_fixtures, - } + Self { address, logs, traces, labeled_addresses, reason: None, coverage, fuzz_fixtures } } pub fn failed_with( logs: Vec, traces: Traces, - labeled_addresses: HashMap, + labeled_addresses: AddressHashMap, reason: String, ) -> Self { Self { - deployments: HashMap::new(), address: Address::ZERO, logs, traces, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index ec6c64300..74e98d197 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy_dyn_abi::DynSolValue; use alloy_json_abi::Function; -use alloy_primitives::{address, Address, Bytes, U256}; +use alloy_primitives::{address, map::HashMap, Address, Bytes, U256}; use eyre::Result; use foundry_common::{ contracts::{ContractsByAddress, ContractsByArtifact}, @@ -35,12 +35,7 @@ use foundry_evm::{ }; use proptest::test_runner::TestRunner; use rayon::prelude::*; -use std::{ - borrow::Cow, - cmp::min, - collections::{BTreeMap, HashMap}, - time::Instant, -}; +use std::{borrow::Cow, cmp::min, collections::BTreeMap, time::Instant}; /// When running tests, we deploy all external libraries present in the project. To avoid additional /// libraries affecting nonces of senders used in tests, we are using separate address to @@ -76,7 +71,7 @@ pub struct ContractRunner<'a> { pub span: tracing::Span, } -impl<'a> ContractRunner<'a> { +impl ContractRunner<'_> { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. pub fn setup(&mut self, call_setup: bool) -> TestSetup { @@ -125,8 +120,6 @@ impl<'a> ContractRunner<'a> { // construction self.executor.set_balance(address, self.initial_balance)?; - let mut zk_setup_deployments = HashMap::new(); - // Deploy the test contract match self.executor.deploy( self.sender, @@ -156,8 +149,7 @@ impl<'a> ContractRunner<'a> { trace!("calling setUp"); let res = self.executor.setup(None, address, Some(self.revert_decoder)); let (setup_logs, setup_traces, labeled_addresses, reason, coverage) = match res { - Ok(RawCallResult { traces, labels, logs, coverage, deployments, .. }) => { - zk_setup_deployments.extend(deployments); + Ok(RawCallResult { traces, labels, logs, coverage, .. }) => { trace!(%address, "successfully called setUp"); (logs, traces, labels, None, coverage) } @@ -168,15 +160,18 @@ impl<'a> ContractRunner<'a> { } = *err; (logs, traces, labels, Some(format!("setup failed: {reason}")), coverage) } - Err(err) => { - (Vec::new(), None, HashMap::new(), Some(format!("setup failed: {err}")), None) - } + Err(err) => ( + Vec::new(), + None, + HashMap::default(), + Some(format!("setup failed: {err}")), + None, + ), }; traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); logs.extend(setup_logs); TestSetup { - deployments: zk_setup_deployments, address, logs, traces, @@ -187,7 +182,6 @@ impl<'a> ContractRunner<'a> { } } else { TestSetup::success( - zk_setup_deployments, address, logs, traces, @@ -216,7 +210,7 @@ impl<'a> ContractRunner<'a> { /// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the /// current test. fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures { - let mut fixtures = HashMap::new(); + let mut fixtures = HashMap::default(); let fixture_functions = self.contract.abi.functions().filter(|func| func.is_fixture()); for func in fixture_functions { if func.inputs.is_empty() { @@ -355,13 +349,8 @@ impl<'a> ContractRunner<'a> { find_time, ); - let identified_contracts = has_invariants.then(|| { - load_contracts( - setup.traces.iter().map(|(_, t)| &t.arena), - &known_contracts, - &setup.deployments, - ) - }); + let identified_contracts = has_invariants + .then(|| load_contracts(setup.traces.iter().map(|(_, t)| &t.arena), &known_contracts)); let test_results = functions .par_iter() @@ -542,6 +531,7 @@ impl<'a> ContractRunner<'a> { &mut test_result.logs, &mut test_result.traces, &mut test_result.coverage, + &mut test_result.deprecated_cheatcodes, &txes, ); return test_result.invariant_replay_fail( @@ -584,6 +574,7 @@ impl<'a> ContractRunner<'a> { &mut test_result.logs, &mut test_result.traces, &mut test_result.coverage, + &mut test_result.deprecated_cheatcodes, progress.as_ref(), ) { Ok(call_sequence) => { @@ -619,6 +610,7 @@ impl<'a> ContractRunner<'a> { &mut test_result.logs, &mut test_result.traces, &mut test_result.coverage, + &mut test_result.deprecated_cheatcodes, &invariant_result.last_run_inputs, ) { error!(%err, "Failed to replay last invariant run"); diff --git a/crates/forge/tests/cli/alphanet.rs b/crates/forge/tests/cli/alphanet.rs new file mode 100644 index 000000000..6e41551ac --- /dev/null +++ b/crates/forge/tests/cli/alphanet.rs @@ -0,0 +1,19 @@ +// Ensure we can run basic counter tests with EOF support. +#[cfg(target_os = "linux")] +forgetest_init!(test_eof_flag, |prj, cmd| { + cmd.forge_fuse().args(["test", "--eof"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (3805): This is a pre-release compiler version, please do not use it in production. + + +Ran 2 tests for test/Counter.t.sol:CounterTest +[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS]) +[PASS] test_Increment() ([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/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 92b362d1a..fcead9cb0 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -8,6 +8,7 @@ use foundry_config::{ use foundry_test_utils::{ foundry_compilers::PathStyle, rpc::next_mainnet_etherscan_api_key, + snapbox::IntoData, util::{pretty_err, read_string, OutputExt, TestCommand}, }; use semver::Version; @@ -229,7 +230,7 @@ forgetest!(can_init_repo_with_config, |prj, cmd| { Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -261,9 +262,6 @@ Check the current git repository's status with `git status`. Then, you can track files with `git add ...` and then commit them with `git commit`, ignore them in the `.gitignore` file, or run this command again with the `--no-commit` flag. -If none of the previous steps worked, please open an issue at: -https://github.com/foundry-rs/foundry/issues/new/choose - "#]]); // ensure nothing was emitted, dir is empty @@ -277,7 +275,7 @@ forgetest!(can_init_no_git, |prj, cmd| { cmd.arg("init").arg(prj.root()).arg("--no-git").assert_success().stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std + Installed forge-std[..] Initialized forge project "#]]); @@ -362,7 +360,7 @@ Run with the `--force` flag to initialize regardless. Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -397,7 +395,7 @@ Run with the `--force` flag to initialize regardless. Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -433,7 +431,7 @@ Run with the `--force` flag to initialize regardless. Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -452,7 +450,7 @@ forgetest!(can_init_vscode, |prj, cmd| { cmd.arg("init").arg(prj.root()).arg("--vscode").assert_success().stdout_eq(str![[r#" Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -548,7 +546,7 @@ forgetest!(can_clone, |prj, cmd| { Downloading the source code of 0x044b75f554b886A065b9567891e45c79542d7357 from Etherscan... Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project Collecting the creation information of 0x044b75f554b886A065b9567891e45c79542d7357 from Etherscan... [COMPILING_FILES] with [SOLC_VERSION] @@ -596,7 +594,7 @@ forgetest!(can_clone_no_remappings_txt, |prj, cmd| { Downloading the source code of 0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf from Etherscan... Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project Collecting the creation information of 0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf from Etherscan... [COMPILING_FILES] with [SOLC_VERSION] @@ -741,9 +739,17 @@ Compiler run successful! // checks that extra output works forgetest_init!(can_emit_multiple_extra_output, |prj, cmd| { - cmd.args(["build", "--extra-output", "metadata", "ir-optimized", "--extra-output", "ir"]) - .assert_success() - .stdout_eq(str![[r#" + cmd.args([ + "build", + "--extra-output", + "metadata", + "legacyAssembly", + "ir-optimized", + "--extra-output", + "ir", + ]) + .assert_success() + .stdout_eq(str![[r#" [COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -754,6 +760,7 @@ Compiler run successful! let artifact: ConfigurableContractArtifact = foundry_compilers::utils::read_json_file(&artifact_path).unwrap(); assert!(artifact.metadata.is_some()); + assert!(artifact.legacy_assembly.is_some()); assert!(artifact.ir.is_some()); assert!(artifact.ir_optimized.is_some()); @@ -764,6 +771,7 @@ Compiler run successful! "metadata", "ir-optimized", "evm.bytecode.sourceMap", + "evm.legacyAssembly", "--force", ]) .root_arg() @@ -785,6 +793,12 @@ Compiler run successful! let sourcemap = prj.paths().artifacts.join(format!("{TEMPLATE_CONTRACT_ARTIFACT_BASE}.sourcemap")); std::fs::read_to_string(sourcemap).unwrap(); + + let legacy_assembly = prj + .paths() + .artifacts + .join(format!("{TEMPLATE_CONTRACT_ARTIFACT_BASE}.legacyAssembly.json")); + std::fs::read_to_string(legacy_assembly).unwrap(); }); forgetest!(can_print_warnings, |prj, cmd| { @@ -985,7 +999,7 @@ Warning: SPDX license identifier not provided in source file. Before publishing, "#]]); }); -// test that `forge build` does not print `(with warnings)` if there arent any +// test that `forge build` does not print `(with warnings)` if there aren't any forgetest!(can_compile_without_warnings, |prj, cmd| { let config = Config { ignored_error_codes: vec![SolidityErrorCode::SpdxLicenseNotProvided], @@ -1229,7 +1243,7 @@ forgetest!(can_install_and_remove, |prj, cmd| { .assert_success() .stdout_eq(str![[r#" Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] "#]]); @@ -1297,7 +1311,7 @@ forgetest!(can_reinstall_after_manual_remove, |prj, cmd| { .assert_success() .stdout_eq(str![[r#" Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] "#]]); @@ -1419,11 +1433,7 @@ Compiler run successful! } ); -forgetest!(gas_report_all_contracts, |prj, cmd| { - prj.insert_ds_test(); - prj.add_source( - "Contracts.sol", - r#" +const GAS_REPORT_CONTRACTS: &str = r#" //SPDX-license-identifier: MIT import "./test.sol"; @@ -1506,9 +1516,11 @@ contract ContractThreeTest is DSTest { c3.baz(); } } - "#, - ) - .unwrap(); +"#; + +forgetest!(gas_report_all_contracts, |prj, cmd| { + prj.insert_ds_test(); + prj.add_source("Contracts.sol", GAS_REPORT_CONTRACTS).unwrap(); // report for all prj.write_config(Config { @@ -1517,34 +1529,121 @@ contract ContractThreeTest is DSTest { ..Default::default() }); - let first_out = cmd - .forge_fuse() + cmd.forge_fuse().arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() .arg("test") .arg("--gas-report") + .arg("--json") .assert_success() - .get_output() - .stdout_lossy(); - assert!(first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); prj.write_config(Config { gas_reports: (vec![]), ..Default::default() }); - let second_out = cmd - .forge_fuse() + cmd.forge_fuse().arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() .arg("test") .arg("--gas-report") + .arg("--json") .assert_success() - .get_output() - .stdout_lossy(); - assert!(second_out.contains("foo") && second_out.contains("bar") && second_out.contains("baz")); + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); prj.write_config(Config { gas_reports: (vec!["*".to_string()]), ..Default::default() }); - let third_out = cmd - .forge_fuse() + cmd.forge_fuse().arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() .arg("test") .arg("--gas-report") + .arg("--json") .assert_success() - .get_output() - .stdout_lossy(); - assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); prj.write_config(Config { gas_reports: (vec![ @@ -1554,125 +1653,90 @@ contract ContractThreeTest is DSTest { ]), ..Default::default() }); - let fourth_out = cmd - .forge_fuse() + cmd.forge_fuse().arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() .arg("test") .arg("--gas-report") + .arg("--json") .assert_success() - .get_output() - .stdout_lossy(); - assert!(fourth_out.contains("foo") && fourth_out.contains("bar") && fourth_out.contains("baz")); + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); }); forgetest!(gas_report_some_contracts, |prj, cmd| { prj.insert_ds_test(); - prj.add_source( - "Contracts.sol", - r#" -//SPDX-license-identifier: MIT - -import "./test.sol"; - -contract ContractOne { - int public i; - - constructor() { - i = 0; - } - - function foo() public{ - while(i<5){ - i++; - } - } -} - -contract ContractOneTest is DSTest { - ContractOne c1; - - function setUp() public { - c1 = new ContractOne(); - } - - function testFoo() public { - c1.foo(); - } -} - - -contract ContractTwo { - int public i; - - constructor() { - i = 0; - } - - function bar() public{ - while(i<50){ - i++; - } - } -} - -contract ContractTwoTest is DSTest { - ContractTwo c2; - - function setUp() public { - c2 = new ContractTwo(); - } - - function testBar() public { - c2.bar(); - } -} - -contract ContractThree { - int public i; - - constructor() { - i = 0; - } - - function baz() public{ - while(i<500){ - i++; - } - } -} - -contract ContractThreeTest is DSTest { - ContractThree c3; - - function setUp() public { - c3 = new ContractThree(); - } - - function testBaz() public { - c3.baz(); - } -} - "#, - ) - .unwrap(); + prj.add_source("Contracts.sol", GAS_REPORT_CONTRACTS).unwrap(); // report for One prj.write_config(Config { gas_reports: vec!["ContractOne".to_string()], ..Default::default() }); cmd.forge_fuse(); - let first_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!( - first_out.contains("foo") && !first_out.contains("bar") && !first_out.contains("baz"), - "foo:\n{first_out}" - ); + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | +... + +"#]]); + cmd.forge_fuse() + .arg("test") + .arg("--gas-report") + .arg("--json") + .assert_success() + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +"#]].is_jsonlines()); // report for Two prj.write_config(Config { gas_reports: vec!["ContractTwo".to_string()], ..Default::default() }); cmd.forge_fuse(); - let second_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!( - !second_out.contains("foo") && second_out.contains("bar") && !second_out.contains("baz"), - "bar:\n{second_out}" + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse().arg("test").arg("--gas-report").arg("--json").assert_success().stdout_eq( + str![[r#" +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines(), ); // report for Three @@ -1681,104 +1745,30 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let third_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!( - !third_out.contains("foo") && !third_out.contains("bar") && third_out.contains("baz"), - "baz:\n{third_out}" - ); + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | +... + +"#]]); + cmd.forge_fuse() + .arg("test") + .arg("--gas-report") + .arg("--json") + .assert_success() + .stdout_eq(str![[r#" +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +"#]].is_jsonlines()); }); forgetest!(gas_ignore_some_contracts, |prj, cmd| { prj.insert_ds_test(); - prj.add_source( - "Contracts.sol", - r#" -//SPDX-license-identifier: MIT - -import "./test.sol"; - -contract ContractOne { - int public i; - - constructor() { - i = 0; - } - - function foo() public{ - while(i<5){ - i++; - } - } -} - -contract ContractOneTest is DSTest { - ContractOne c1; - - function setUp() public { - c1 = new ContractOne(); - } - - function testFoo() public { - c1.foo(); - } -} - - -contract ContractTwo { - int public i; - - constructor() { - i = 0; - } - - function bar() public{ - while(i<50){ - i++; - } - } -} - -contract ContractTwoTest is DSTest { - ContractTwo c2; - - function setUp() public { - c2 = new ContractTwo(); - } - - function testBar() public { - c2.bar(); - } -} - -contract ContractThree { - int public i; - - constructor() { - i = 0; - } - - function baz() public{ - while(i<500){ - i++; - } - } -} - -contract ContractThreeTest is DSTest { - ContractThree c3; - - function setUp() public { - c3 = new ContractThree(); - } - - function testBaz() public { - c3.baz(); - } -} - "#, - ) - .unwrap(); + prj.add_source("Contracts.sol", GAS_REPORT_CONTRACTS).unwrap(); // ignore ContractOne prj.write_config(Config { @@ -1787,9 +1777,34 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let first_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!(!first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() + .arg("test") + .arg("--gas-report") + .arg("--json") + .assert_success() + .stdout_eq(str![[r#" +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); // ignore ContractTwo cmd.forge_fuse(); @@ -1799,11 +1814,34 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let second_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!( - second_out.contains("foo") && !second_out.contains("bar") && second_out.contains("baz") - ); + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | +... + +"#]]); + cmd.forge_fuse() + .arg("test") + .arg("--gas-report") + .arg("--json") + .assert_success() + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +"#]].is_jsonlines()); // ignore ContractThree cmd.forge_fuse(); @@ -1817,9 +1855,43 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let third_out = - cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); - assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); + cmd.arg("test").arg("--gas-report").assert_success().stdout_eq(str![[r#" +... +| src/Contracts.sol:ContractOne contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| foo | 45387 | 45387 | 45387 | 45387 | 1 | + + +| src/Contracts.sol:ContractThree contract | | | | | | +|------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103591 | 256 | | | | | +| Function Name | min | avg | median | max | # calls | +| baz | 260712 | 260712 | 260712 | 260712 | 1 | + + +| src/Contracts.sol:ContractTwo contract | | | | | | +|----------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 103375 | 255 | | | | | +| Function Name | min | avg | median | max | # calls | +| bar | 64984 | 64984 | 64984 | 64984 | 1 | +... + +"#]]); + cmd.forge_fuse() + .arg("test") + .arg("--gas-report") + .arg("--json") + .assert_success() + .stdout_eq(str![[r#" +{"gas":103375,"size":255,"functions":{"foo":{"foo()":{"calls":1,"min":45387,"mean":45387,"median":45387,"max":45387}}}} +{"gas":103591,"size":256,"functions":{"baz":{"baz()":{"calls":1,"min":260712,"mean":260712,"median":260712,"max":260712}}}} +{"gas":103375,"size":255,"functions":{"bar":{"bar()":{"calls":1,"min":64984,"mean":64984,"median":64984,"max":64984}}}} +"#]].is_jsonlines()); }); forgetest!(zk_gas_report, |prj, cmd| { diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 56938d18b..e84137dbc 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -37,6 +37,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { libs: vec!["lib-test".into()], cache: true, cache_path: "test-cache".into(), + snapshots: "snapshots".into(), broadcast: "broadcast".into(), force: true, evm_version: EvmVersion::Byzantium, @@ -151,6 +152,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { eof_version: None, alphanet: false, transaction_timeout: 120, + eof: false, _non_exhaustive: (), zksync: Default::default(), }; @@ -205,17 +207,6 @@ forgetest_init!(can_override_config, |prj, cmd| { Remapping::from(config.remappings[0].clone()).to_string() ); - // env vars work - std::env::set_var("DAPP_REMAPPINGS", "ds-test/=lib/forge-std/lib/ds-test/from-env/"); - let config = forge_utils::load_config_with_root(Some(prj.root())); - assert_eq!( - format!( - "ds-test/={}/", - prj.root().join("lib/forge-std/lib/ds-test/from-env").to_slash_lossy() - ), - Remapping::from(config.remappings[0].clone()).to_string() - ); - let config = prj.config_from_output(["--remappings", "ds-test/=lib/forge-std/lib/ds-test/from-cli"]); assert_eq!( @@ -234,7 +225,6 @@ forgetest_init!(can_override_config, |prj, cmd| { Remapping::from(config.remappings[0].clone()).to_string() ); - std::env::remove_var("DAPP_REMAPPINGS"); pretty_err(&remappings_txt, fs::remove_file(&remappings_txt)); let expected = profile.into_basic().to_string_pretty().unwrap().trim().to_string(); @@ -506,7 +496,6 @@ forgetest!(can_set_gas_price, |prj, cmd| { // test that we can detect remappings from foundry.toml forgetest_init!(can_detect_lib_foundry_toml, |prj, cmd| { - std::env::remove_var("DAPP_REMAPPINGS"); let config = cmd.config(); let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::>(); similar_asserts::assert_eq!( @@ -547,7 +536,6 @@ forgetest_init!(can_detect_lib_foundry_toml, |prj, cmd| { let toml_file = nested.join("foundry.toml"); pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap())); - std::env::remove_var("DAPP_REMAPPINGS"); let another_config = cmd.config(); let remappings = another_config.remappings.iter().cloned().map(Remapping::from).collect::>(); @@ -567,7 +555,6 @@ forgetest_init!(can_detect_lib_foundry_toml, |prj, cmd| { config.src = "custom-source-dir".into(); pretty_err(&toml_file, fs::write(&toml_file, config.to_string_pretty().unwrap())); - std::env::remove_var("DAPP_REMAPPINGS"); let config = cmd.config(); let remappings = config.remappings.iter().cloned().map(Remapping::from).collect::>(); similar_asserts::assert_eq!( @@ -620,7 +607,7 @@ forgetest!(can_update_libs_section, |prj, cmd| { cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]).assert_success().stdout_eq(str![ [r#" Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] "#] ]); @@ -652,7 +639,7 @@ forgetest!(config_emit_warnings, |prj, cmd| { cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]).assert_success().stdout_eq(str![ [r#" Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] "#] ]); diff --git a/crates/forge/tests/cli/debug.rs b/crates/forge/tests/cli/debug.rs index 8ee1eb3f2..bbed7dc72 100644 --- a/crates/forge/tests/cli/debug.rs +++ b/crates/forge/tests/cli/debug.rs @@ -11,7 +11,7 @@ forgetest_async!( Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); diff --git a/crates/forge/tests/cli/eip712.rs b/crates/forge/tests/cli/eip712.rs new file mode 100644 index 000000000..2f832ae31 --- /dev/null +++ b/crates/forge/tests/cli/eip712.rs @@ -0,0 +1,52 @@ +forgetest!(test_eip712, |prj, cmd| { + let path = prj + .add_source( + "Structs", + r#" +library Structs { + struct Foo { + Bar bar; + } + + struct Bar { + Art art; + } + + struct Art { + uint256 id; + } + + struct Complex { + Structs2.Foo foo2; + Foo[] foos; + } +} + +library Structs2 { + struct Foo { + uint256 id; + } +} +"#, + ) + .unwrap(); + + cmd.forge_fuse().args(["eip712", path.to_string_lossy().as_ref()]).assert_success().stdout_eq( + str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +No files changed, compilation skipped +Foo(Bar bar)Art(uint256 id)Bar(Art art) + +Bar(Art art)Art(uint256 id) + +Art(uint256 id) + +Complex(Foo foo2,Foo_1[] foos)Art(uint256 id)Bar(Art art)Foo(uint256 id)Foo_1(Bar bar) + +Foo(uint256 id) + + +"#]], + ); +}); diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index e8c6f1cc0..6ad29ca48 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -4,6 +4,7 @@ extern crate foundry_test_utils; pub mod constants; pub mod utils; +mod alphanet; mod bind_json; mod build; mod cache; @@ -14,6 +15,7 @@ mod coverage; mod create; mod debug; mod doc; +mod eip712; mod multi_script; mod script; mod soldeer; diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index baea401e0..38f8f9479 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3,7 +3,11 @@ use crate::constants::TEMPLATE_CONTRACT; use alloy_primitives::{hex, Address, Bytes}; use anvil::{spawn, NodeConfig}; -use foundry_test_utils::{rpc, util::OutputExt, ScriptOutcome, ScriptTester}; +use foundry_test_utils::{ + rpc, + util::{OutputExt, OTHER_SOLC_VERSION, SOLC_VERSION}, + ScriptOutcome, ScriptTester, +}; use regex::Regex; use serde_json::Value; use std::{env, path::PathBuf, str::FromStr}; @@ -930,7 +934,7 @@ forgetest_async!(check_broadcast_log, |prj, cmd| { // Check broadcast logs // Ignore timestamp, blockHash, blockNumber, cumulativeGasUsed, effectiveGasPrice, - // transactionIndex and logIndex values since they can change inbetween runs + // transactionIndex and logIndex values since they can change in between runs let re = Regex::new(r#"((timestamp":).[0-9]*)|((blockHash":).*)|((blockNumber":).*)|((cumulativeGasUsed":).*)|((effectiveGasPrice":).*)|((transactionIndex":).*)|((logIndex":).*)"#).unwrap(); let fixtures_log = std::fs::read_to_string( @@ -954,7 +958,7 @@ forgetest_async!(check_broadcast_log, |prj, cmd| { // ); // Check sensitive logs - // Ignore port number since it can change inbetween runs + // Ignore port number since it can change in between runs let re = Regex::new(r":[0-9]+").unwrap(); let fixtures_log = std::fs::read_to_string( @@ -1012,7 +1016,7 @@ forgetest_async!(can_execute_script_with_arguments, |prj, cmd| { Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -1134,7 +1138,7 @@ forgetest_async!(can_execute_script_with_arguments_nested_deploy, |prj, cmd| { Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -1301,7 +1305,7 @@ forgetest_async!(assert_tx_origin_is_not_overritten, |prj, cmd| { Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -1382,7 +1386,7 @@ forgetest_async!(assert_can_create_multiple_contracts_with_correct_nonce, |prj, Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -1520,36 +1524,45 @@ forgetest_async!(can_detect_contract_when_multiple_versions, |prj, cmd| { prj.add_script( "A.sol", - r#"pragma solidity 0.8.20; + &format!( + r#" +pragma solidity {SOLC_VERSION}; import "./B.sol"; -contract ScriptA {} -"#, +contract ScriptA {{}} +"# + ), ) .unwrap(); prj.add_script( "B.sol", - r#"pragma solidity >=0.8.5 <=0.8.20; + &format!( + r#" +pragma solidity >={OTHER_SOLC_VERSION} <={SOLC_VERSION}; import 'forge-std/Script.sol'; -contract ScriptB is Script { - function run() external { +contract ScriptB is Script {{ + function run() external {{ vm.broadcast(); address(0).call(""); - } -} -"#, + }} +}} +"# + ), ) .unwrap(); prj.add_script( "C.sol", - r#"pragma solidity 0.8.5; + &format!( + r#" +pragma solidity {OTHER_SOLC_VERSION}; import "./B.sol"; -contract ScriptC {} -"#, +contract ScriptC {{}} +"# + ), ) .unwrap(); @@ -1607,7 +1620,7 @@ forgetest_async!(can_decode_custom_errors, |prj, cmd| { Target directory is not empty, but `--force` was specified Initializing [..]... Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) - Installed forge-std [..] + Installed forge-std[..] Initialized forge project "#]]); @@ -2094,3 +2107,150 @@ contract DeployScript is Script { let transactions = transactions.transactions; assert_eq!(transactions.len(), 3); }); + +// +forgetest_async!(test_broadcast_raw_create2_deployer, |prj, cmd| { + let (_api, handle) = + spawn(NodeConfig::test().with_disable_default_create2_deployer(true)).await; + + foundry_test_utils::util::initialize(prj.root()); + prj.add_script( + "Foo", + r#" +import "forge-std/Script.sol"; + +contract SimpleScript is Script { + function run() external { + // send funds to create2 factory deployer + vm.broadcast(); + payable(0x3fAB184622Dc19b6109349B94811493BF2a45362).transfer(10000000 gwei); + // deploy create2 factory + vm.broadcastRawTransaction( + hex"f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222" + ); + } +} + "#, + ) + .unwrap(); + + cmd.args([ + "script", + "--private-key", + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "--rpc-url", + &handle.http_endpoint(), + "--broadcast", + "SimpleScript", + ]); + + cmd.assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. + +## Setting up 1 EVM. + +========================== + +Chain 31337 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); +}); + +forgetest_init!(can_get_script_wallets, |prj, cmd| { + let script = prj + .add_source( + "Foo", + r#" +import "forge-std/Script.sol"; + +interface Vm { + function getWallets() external returns (address[] memory wallets); +} + +contract WalletScript is Script { + function run() public { + address[] memory wallets = Vm(address(vm)).getWallets(); + console.log(wallets[0]); + } +}"#, + ) + .unwrap(); + cmd.arg("script") + .arg(script) + .args([ + "--private-key", + "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6", + "-v", + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +== Logs == + 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 + +"#]]); +}); + +forgetest_init!(can_remeber_keys, |prj, cmd| { + let script = prj + .add_source( + "Foo", + r#" +import "forge-std/Script.sol"; + +interface Vm { + function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs); +} + +contract WalletScript is Script { + function run() public { + string memory mnemonic = "test test test test test test test test test test test junk"; + string memory derivationPath = "m/44'/60'/0'/0/"; + address[] memory wallets = Vm(address(vm)).rememberKeys(mnemonic, derivationPath, 3); + for (uint256 i = 0; i < wallets.length; i++) { + console.log(wallets[i]); + } + } +}"#, + ) + .unwrap(); + cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +== Logs == + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC + +"#]]); +}); diff --git a/crates/forge/tests/cli/soldeer.rs b/crates/forge/tests/cli/soldeer.rs index 6c57f69f3..30b0c4957 100644 --- a/crates/forge/tests/cli/soldeer.rs +++ b/crates/forge/tests/cli/soldeer.rs @@ -1,32 +1,25 @@ //! Contains various tests related to `forge soldeer`. -use std::{ - fs::{self, OpenOptions}, - path::Path, -}; +use std::{fs, path::Path}; use foundry_test_utils::forgesoldeer; -use std::io::Write; forgesoldeer!(install_dependency, |prj, cmd| { let command = "install"; let dependency = "forge-std~1.8.1"; + let mut foundry_contents = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[dependencies] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +"#; let foundry_file = prj.root().join("foundry.toml"); + fs::write(&foundry_file, foundry_contents).unwrap(); - cmd.arg("soldeer").args([command, dependency]).assert_success().stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started HTTP download of forge-std~1.8.1 -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -The dependency forge-std~1.8.1 was unzipped! -Writing forge-std~1.8.1 to the lock file. -Added forge-std~1.8.1 to remappings - -"#]]); + cmd.arg("soldeer").args([command, dependency]).assert_success(); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -36,20 +29,12 @@ Added forge-std~1.8.1 to remappings // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_8_1_23-03-2024_00:05:44_forge-std-v1.8.1.zip" - // checksum = "0f7cd44f5670c31a9646d4031e70c66321cd3ed6ebac3c7278e4e57e4e5c5bd0" - // integrity = "6a52f0c34d935e508af46a6d12a3a741798252f20a66f6bbee86c23dd6ef7c8d" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones - let foundry_contents = r#"[profile.default] + foundry_contents = r#"[profile.default] src = "src" out = "out" libs = ["lib"] @@ -68,39 +53,19 @@ forgesoldeer!(install_dependency_git, |prj, cmd| { let dependency = "forge-std~1.8.1"; let git = "https://gitlab.com/mario4582928/Mario.git"; + let mut foundry_contents = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[dependencies] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +"#; let foundry_file = prj.root().join("foundry.toml"); + fs::write(&foundry_file, foundry_contents).unwrap(); - cmd.arg("soldeer") - .args([command, dependency, git]) - .assert_success() - .stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started GIT download of forge-std~1.8.1 -Successfully downloaded forge-std~1.8.1 the dependency via git -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -Writing forge-std~1.8.1 to the lock file. -Added forge-std~1.8.1 to remappings - -"#]]) - .stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started GIT download of forge-std~1.8.1 -Successfully downloaded forge-std~1.8.1 the dependency via git -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -Writing forge-std~1.8.1 to the lock file. -Added forge-std~1.8.1 to remappings - -"#]]); + cmd.arg("soldeer").args([command, dependency, git]).assert_success(); // Making sure the path was created to the dependency and that README.md exists // meaning that the dependencies were installed correctly @@ -109,19 +74,12 @@ Added forge-std~1.8.1 to remappings // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://gitlab.com/mario4582928/Mario.git" - // checksum = "22868f426bd4dd0e682b5ec5f9bd55507664240c" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones - let foundry_contents = r#"[profile.default] + foundry_contents = r#"[profile.default] src = "src" out = "out" libs = ["lib"] @@ -142,25 +100,19 @@ forgesoldeer!(install_dependency_git_commit, |prj, cmd| { let rev_flag = "--rev"; let commit = "7a0663eaf7488732f39550be655bad6694974cb3"; + let mut foundry_contents = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[dependencies] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +"#; let foundry_file = prj.root().join("foundry.toml"); + fs::write(&foundry_file, foundry_contents).unwrap(); - cmd.arg("soldeer") - .args([command, dependency, git, rev_flag, commit]) - .assert_success() - .stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started GIT download of forge-std~1.8.1 -Successfully downloaded forge-std~1.8.1 the dependency via git -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -Writing forge-std~1.8.1 to the lock file. -Added forge-std~1.8.1 to remappings - -"#]]); + cmd.arg("soldeer").args([command, dependency, git, rev_flag, commit]).assert_success(); // Making sure the path was created to the dependency and that README.md exists // meaning that the dependencies were installed correctly @@ -170,19 +122,12 @@ Added forge-std~1.8.1 to remappings // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://gitlab.com/mario4582928/Mario.git" - // checksum = "7a0663eaf7488732f39550be655bad6694974cb3" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones - let foundry_contents = r#"[profile.default] + foundry_contents = r#"[profile.default] src = "src" out = "out" libs = ["lib"] @@ -200,7 +145,13 @@ forgesoldeer!(update_dependencies, |prj, cmd| { let command = "update"; // We need to write this into the foundry.toml to make the update install the dependency - let foundry_updates = r#" + let foundry_updates = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + [dependencies] "@tt" = {version = "1.6.1", url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/3_3_0-rc_2_22-01-2024_13:12:57_contracts.zip"} forge-std = { version = "1.8.1" } @@ -210,12 +161,7 @@ mario-custom-tag = { version = "1.0", git = "https://gitlab.com/mario4582928/Mar mario-custom-branch = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", tag = "custom-branch" } "#; let foundry_file = prj.root().join("foundry.toml"); - - let mut file = OpenOptions::new().append(true).open(&foundry_file).unwrap(); - - if let Err(e) = write!(file, "{foundry_updates}") { - eprintln!("Couldn't write to file: {e}"); - } + fs::write(&foundry_file, foundry_updates).unwrap(); cmd.arg("soldeer").arg(command).assert_success(); @@ -237,45 +183,6 @@ mario-custom-branch = { version = "1.0", git = "https://gitlab.com/mario4582928/ // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "@tt" - // version = "1.6.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/3_3_0-rc_2_22-01-2024_13:12:57_contracts.zip" - // checksum = "3aa5b07e796ce2ae54bbab3a5280912444ae75807136a513fa19ff3a314c323f" - // integrity = "24e7847580674bd0a4abf222b82fac637055141704c75a3d679f637acdcfe817" - - // [[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_8_1_23-03-2024_00:05:44_forge-std-v1.8.1.zip" - // checksum = "0f7cd44f5670c31a9646d4031e70c66321cd3ed6ebac3c7278e4e57e4e5c5bd0" - // integrity = "6a52f0c34d935e508af46a6d12a3a741798252f20a66f6bbee86c23dd6ef7c8d" - - // [[dependencies]] - // name = "mario" - // version = "1.0" - // source = "https://gitlab.com/mario4582928/Mario.git" - // checksum = "22868f426bd4dd0e682b5ec5f9bd55507664240c" - - // [[dependencies]] - // name = "mario-custom-branch" - // version = "1.0" - // source = "https://gitlab.com/mario4582928/Mario.git" - // checksum = "84c3b38dba44a4c29ec44f45a31e1e59d36aa77b" - - // [[dependencies]] - // name = "mario-custom-tag" - // version = "1.0" - // source = "https://gitlab.com/mario4582928/Mario.git" - // checksum = "a366c4b560022d12e668d6c1756c6382e2352d0f" - - // [[dependencies]] - // name = "solmate" - // version = "6.7.0" - // source = "https://soldeer-revisions.s3.amazonaws.com/solmate/6_7_0_22-01-2024_13:21:00_solmate.zip" - // checksum = "dd0f08cdaaaad1de0ac45993d4959351ba89c2d9325a0b5df5570357064f2c33" - // integrity = "ec330877af853f9d34b2b1bf692fb33c9f56450625f5c4abdcf0d3405839730e" - // "#; // assert_data_eq!(lock_contents, read_file_to_string(&path_lock_file)); let actual_lock_contents = read_file_to_string(&path_lock_file); @@ -297,7 +204,6 @@ mario = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", re mario-custom-tag = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", tag = "custom-tag" } mario-custom-branch = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", tag = "custom-branch" } "#; - assert_data_eq!(read_file_to_string(&foundry_file), foundry_contents); }); @@ -306,27 +212,21 @@ forgesoldeer!(update_dependencies_simple_version, |prj, cmd| { // We need to write this into the foundry.toml to make the update install the dependency, this // is he simplified version of version specification - let foundry_updates = r#" + let foundry_updates = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + [dependencies] forge-std = "1.8.1" "#; let foundry_file = prj.root().join("foundry.toml"); - let mut file = OpenOptions::new().append(true).open(&foundry_file).unwrap(); - - if let Err(e) = write!(file, "{foundry_updates}") { - eprintln!("Couldn't write to file: {e}"); - } - - cmd.arg("soldeer").arg(command).assert_success().stdout_eq(str![[r#" -🦌 Running Soldeer update 🦌 -Started HTTP download of forge-std~1.8.1 -Dependency forge-std~1.8.1 downloaded! -The dependency forge-std~1.8.1 was unzipped! -Writing forge-std~1.8.1 to the lock file. - -"#]]); + fs::write(&foundry_file, foundry_updates).unwrap(); + cmd.arg("soldeer").arg(command).assert_success(); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly let path_dep_forge = @@ -335,16 +235,8 @@ Writing forge-std~1.8.1 to the lock file. // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_8_1_23-03-2024_00:05:44_forge-std-v1.8.1.zip" - // checksum = "0f7cd44f5670c31a9646d4031e70c66321cd3ed6ebac3c7278e4e57e4e5c5bd0" - // integrity = "6a52f0c34d935e508af46a6d12a3a741798252f20a66f6bbee86c23dd6ef7c8d" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones @@ -362,47 +254,28 @@ forge-std = "1.8.1" assert_data_eq!(read_file_to_string(&foundry_file), foundry_contents); }); -forgesoldeer!(login, |prj, cmd| { - let command = "login"; - - let output = cmd.arg("soldeer").arg(command).execute(); - - // On login, we can only check if the prompt is displayed in the stdout - let stdout = String::from_utf8(output.stdout).expect("Could not parse the output"); - assert!(stdout.contains("Please enter your email")); -}); - forgesoldeer!(install_dependency_with_remappings_config, |prj, cmd| { let command = "install"; let dependency = "forge-std~1.8.1"; - let foundry_updates = r#" + let foundry_updates = r#"[profile.default] +src = "src" +out = "out" +libs = ["lib", "dependencies"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + [soldeer] remappings_generate = true remappings_prefix = "@custom-f@" remappings_location = "config" remappings_regenerate = true + +[dependencies] "#; let foundry_file = prj.root().join("foundry.toml"); - let mut file = OpenOptions::new().append(true).open(&foundry_file).unwrap(); - - if let Err(e) = write!(file, "{foundry_updates}") { - eprintln!("Couldn't write to file: {e}"); - } - - cmd.arg("soldeer").args([command, dependency]).assert_success().stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started HTTP download of forge-std~1.8.1 -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -The dependency forge-std~1.8.1 was unzipped! -Writing forge-std~1.8.1 to the lock file. -Added all dependencies to remapppings - -"#]]); + fs::write(&foundry_file, foundry_updates).unwrap(); + + cmd.arg("soldeer").args([command, dependency]).assert_success(); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -412,23 +285,15 @@ Added all dependencies to remapppings // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_8_1_23-03-2024_00:05:44_forge-std-v1.8.1.zip" - // checksum = "0f7cd44f5670c31a9646d4031e70c66321cd3ed6ebac3c7278e4e57e4e5c5bd0" - // integrity = "6a52f0c34d935e508af46a6d12a3a741798252f20a66f6bbee86c23dd6ef7c8d" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones let foundry_contents = r#"[profile.default] src = "src" out = "out" -libs = ["lib"] +libs = ["lib", "dependencies"] remappings = ["@custom-f@forge-std-1.8.1/=dependencies/forge-std-1.8.1/"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options @@ -450,6 +315,8 @@ forgesoldeer!(install_dependency_with_remappings_txt, |prj, cmd| { let command = "install"; let dependency = "forge-std~1.8.1"; let foundry_updates = r#" +[dependencies] + [soldeer] remappings_generate = true remappings_prefix = "@custom-f@" @@ -457,26 +324,9 @@ remappings_location = "txt" remappings_regenerate = true "#; let foundry_file = prj.root().join("foundry.toml"); - let mut file = OpenOptions::new().append(true).open(&foundry_file).unwrap(); - - if let Err(e) = write!(file, "{foundry_updates}") { - eprintln!("Couldn't write to file: {e}"); - } - - cmd.arg("soldeer").args([command, dependency]).assert_success().stdout_eq(str![[r#" -🦌 Running Soldeer install 🦌 -No config file found. If you wish to proceed, please select how you want Soldeer to be configured: -1. Using foundry.toml -2. Using soldeer.toml -(Press 1 or 2), default is foundry.toml -Started HTTP download of forge-std~1.8.1 -Dependency forge-std~1.8.1 downloaded! -Adding dependency forge-std-1.8.1 to the config file -The dependency forge-std~1.8.1 was unzipped! -Writing forge-std~1.8.1 to the lock file. -Added all dependencies to remapppings - -"#]]); + fs::write(&foundry_file, foundry_updates).unwrap(); + + cmd.arg("soldeer").args([command, dependency]).assert_success(); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -486,16 +336,8 @@ Added all dependencies to remapppings // Making sure the lock contents are the right ones let path_lock_file = prj.root().join("soldeer.lock"); - // let lock_contents = r#"[[dependencies]] - // name = "forge-std" - // version = "1.8.1" - // source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_8_1_23-03-2024_00:05:44_forge-std-v1.8.1.zip" - // checksum = "0f7cd44f5670c31a9646d4031e70c66321cd3ed6ebac3c7278e4e57e4e5c5bd0" - // integrity = "6a52f0c34d935e508af46a6d12a3a741798252f20a66f6bbee86c23dd6ef7c8d" - // "#; let actual_lock_contents = read_file_to_string(&path_lock_file); - // assert_data_eq!(lock_contents, actual_lock_contents); assert!(actual_lock_contents.contains("forge-std")); // Making sure the foundry contents are the right ones @@ -505,6 +347,12 @@ Added all dependencies to remapppings assert_data_eq!(read_file_to_string(&remappings_file), remappings_content); }); +forgesoldeer!(login, |prj, cmd| { + let command = "login"; + + let _ = cmd.arg("soldeer").arg(command).assert_failure(); +}); + fn read_file_to_string(path: &Path) -> String { let contents: String = fs::read_to_string(path).unwrap_or_default(); contents diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index 67a3727ba..d9eee082b 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -274,6 +274,51 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); +const SIMPLE_CONTRACT: &str = r#" +import "./test.sol"; +import "./console.sol"; + +contract SimpleContract { + uint256 public num; + + function setValues(uint256 _num) public { + num = _num; + } +} + +contract SimpleContractTest is DSTest { + function test() public { + SimpleContract c = new SimpleContract(); + c.setValues(100); + console.log("Value set: ", 100); + } +} + "#; + +forgetest!(can_run_test_with_json_output_verbose, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_console(); + + prj.add_source("Simple.t.sol", SIMPLE_CONTRACT).unwrap(); + + // Assert that with verbose output the json output includes the traces + cmd.args(["test", "-vvv", "--json"]) + .assert_success() + .stdout_eq(file!["../fixtures/SimpleContractTestVerbose.json": Json]); +}); + +forgetest!(can_run_test_with_json_output_non_verbose, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_console(); + + prj.add_source("Simple.t.sol", SIMPLE_CONTRACT).unwrap(); + + // Assert that without verbose output the json output does not include the traces + cmd.args(["test", "--json"]) + .assert_success() + .stdout_eq(file!["../fixtures/SimpleContractTestNonVerbose.json": Json]); +}); + // tests that `forge test` will pick up tests that are stored in the `test = ` config value forgetest!(can_run_test_in_custom_test_folder, |prj, cmd| { prj.insert_ds_test(); @@ -597,43 +642,12 @@ Encountered a total of 1 failing tests, 1 tests succeeded "#]]); }); -forgetest_init!(can_test_selfdestruct_with_isolation, |prj, cmd| { - prj.wipe_contracts(); - - prj.add_test( - "Contract.t.sol", - r#" -import {Test} from "forge-std/Test.sol"; - -contract Destructing { - function destruct() public { - selfdestruct(payable(address(0))); - } -} - -contract SelfDestructTest is Test { - function test() public { - Destructing d = new Destructing(); - vm.store(address(d), bytes32(0), bytes32(uint256(1))); - d.destruct(); - assertEq(address(d).code.length, 0); - assertEq(vm.load(address(d), bytes32(0)), bytes32(0)); - } -} - "#, - ) - .unwrap(); - - cmd.args(["test", "-vvvv", "--isolate"]).assert_success(); -}); - forgetest_init!(can_test_transient_storage_with_isolation, |prj, cmd| { prj.wipe_contracts(); prj.add_test( "Contract.t.sol", r#" -pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; contract TransientTester { @@ -2131,5 +2145,265 @@ contract JunitReportTest is Test { +"#]]); +}); + +forgetest_init!( + // Enable this if no cheatcodes are deprecated. + // #[ignore = "no cheatcodes are deprecated"] + test_deprecated_cheatcode_warning, + |prj, cmd| { + prj.add_test( + "DeprecatedCheatcodeTest.t.sol", + r#" + import "forge-std/Test.sol"; + contract DeprecatedCheatcodeTest is Test { + function test_deprecated_cheatcode() public view { + vm.keyExists('{"a": 123}', ".a"); + vm.keyExists('{"a": 123}', ".a"); + } + } + + contract DeprecatedCheatcodeFuzzTest is Test { + function test_deprecated_cheatcode(uint256 a) public view { + vm.keyExists('{"a": 123}', ".a"); + } + } + + contract Counter { + uint256 a; + + function count() public { + a++; + } + } + + contract DeprecatedCheatcodeInvariantTest is Test { + function setUp() public { + Counter counter = new Counter(); + } + + /// forge-config: default.invariant.runs = 1 + function invariant_deprecated_cheatcode() public { + vm.keyExists('{"a": 123}', ".a"); + } + } + "#, + ) + .unwrap(); + + // Tests deprecated cheatcode warning for unit tests. + cmd.args(["test", "--mc", "DeprecatedCheatcodeTest"]).assert_success().stderr_eq(str![[ + r#" +Warning: the following cheatcode(s) are deprecated and will be removed in future versions: + keyExists(string,string): replaced by `keyExistsJson` + +"# + ]]); + + // Tests deprecated cheatcode warning for fuzz tests. + cmd.forge_fuse() + .args(["test", "--mc", "DeprecatedCheatcodeFuzzTest"]) + .assert_success() + .stderr_eq(str![[r#" +Warning: the following cheatcode(s) are deprecated and will be removed in future versions: + keyExists(string,string): replaced by `keyExistsJson` + +"#]]); + + // Tests deprecated cheatcode warning for invariant tests. + cmd.forge_fuse() + .args(["test", "--mc", "DeprecatedCheatcodeInvariantTest"]) + .assert_success() + .stderr_eq(str![[r#" +Warning: the following cheatcode(s) are deprecated and will be removed in future versions: + keyExists(string,string): replaced by `keyExistsJson` + +"#]]); + } +); + +forgetest_init!(requires_single_test, |prj, cmd| { + cmd.args(["test", "--debug"]).assert_failure().stderr_eq(str![[r#" +Error: +2 tests matched your criteria, but exactly 1 test must match in order to run the debugger. + +Use --match-contract and --match-path to further limit the search. + +"#]]); + cmd.forge_fuse().args(["test", "--flamegraph"]).assert_failure().stderr_eq(str![[r#" +Error: +2 tests matched your criteria, but exactly 1 test must match in order to generate a flamegraph. + +Use --match-contract and --match-path to further limit the search. + +"#]]); + cmd.forge_fuse().args(["test", "--flamechart"]).assert_failure().stderr_eq(str![[r#" +Error: +2 tests matched your criteria, but exactly 1 test must match in order to generate a flamechart. + +Use --match-contract and --match-path to further limit the search. + +"#]]); +}); + +forgetest_init!(deprecated_regex_arg, |prj, cmd| { + cmd.args(["test", "--decode-internal", "test_Increment"]).assert_success().stderr_eq(str![[r#" +warning: specifying argument for --decode-internal is deprecated and will be removed in the future, use --match-test instead + +"#]]); +}); + +// Test a script that calls vm.rememberKeys +forgetest_init!(script_testing, |prj, cmd| { + prj + .add_source( + "Foo", + r#" +import "forge-std/Script.sol"; + +interface Vm { +function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs); +} + +contract WalletScript is Script { +function run() public { + string memory mnemonic = "test test test test test test test test test test test junk"; + string memory derivationPath = "m/44'/60'/0'/0/"; + address[] memory wallets = Vm(address(vm)).rememberKeys(mnemonic, derivationPath, 3); + for (uint256 i = 0; i < wallets.length; i++) { + console.log(wallets[i]); + } +} +} + +contract FooTest { + WalletScript public script; + + + function setUp() public { + script = new WalletScript(); + } + + function testWalletScript() public { + script.run(); + } +} + +"#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "testWalletScript", "-vvv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for src/Foo.sol:FooTest +[PASS] testWalletScript() ([GAS]) +Logs: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +... +"#]]); +}); + +// +forgetest_init!(metadata_bytecode_traces, |prj, cmd| { + prj.add_source( + "ParentProxy.sol", + r#" +import {Counter} from "./Counter.sol"; + +abstract contract ParentProxy { + Counter impl; + bytes data; + + constructor(Counter _implementation, bytes memory _data) { + impl = _implementation; + data = _data; + } +} + "#, + ) + .unwrap(); + prj.add_source( + "Proxy.sol", + r#" +import {ParentProxy} from "./ParentProxy.sol"; +import {Counter} from "./Counter.sol"; + +contract Proxy is ParentProxy { + constructor(Counter _implementation, bytes memory _data) + ParentProxy(_implementation, _data) + {} +} + "#, + ) + .unwrap(); + + prj.add_test( + "MetadataTraceTest.t.sol", + r#" +import {Counter} from "src/Counter.sol"; +import {Proxy} from "src/Proxy.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract MetadataTraceTest is Test { + function test_proxy_trace() public { + Counter counter = new Counter(); + new Proxy(counter, ""); + } +} + "#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "test_proxy_trace", "-vvvv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/MetadataTraceTest.t.sol:MetadataTraceTest +[PASS] test_proxy_trace() ([GAS]) +Traces: + [152142] MetadataTraceTest::test_proxy_trace() + ├─ [49499] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 247 bytes of code + ├─ [37978] → new Proxy@0x2e234DAe75C793f67A35089C9d99245E1C58470b + │ └─ ← [Return] 63 bytes of code + └─ ← [Stop] + +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) + +"#]]); + + // Check consistent traces for running with no metadata. + cmd.forge_fuse() + .args(["test", "--mt", "test_proxy_trace", "-vvvv", "--no-metadata"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/MetadataTraceTest.t.sol:MetadataTraceTest +[PASS] test_proxy_trace() ([GAS]) +Traces: + [130521] MetadataTraceTest::test_proxy_trace() + ├─ [38693] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 193 bytes of code + ├─ [27175] → new Proxy@0x2e234DAe75C793f67A35089C9d99245E1C58470b + │ └─ ← [Return] 9 bytes of code + └─ ← [Stop] + +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) + "#]]); }); diff --git a/crates/forge/tests/cli/verify_bytecode.rs b/crates/forge/tests/cli/verify_bytecode.rs index 7e615b565..398ecb52d 100644 --- a/crates/forge/tests/cli/verify_bytecode.rs +++ b/crates/forge/tests/cli/verify_bytecode.rs @@ -33,6 +33,7 @@ fn test_verify_bytecode( prj.add_source(contract_name, &source_code).unwrap(); prj.write_config(config); + let etherscan_key = next_mainnet_etherscan_api_key(); let mut args = vec![ "verify-bytecode", addr, diff --git a/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json b/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json new file mode 100644 index 000000000..b4e396863 --- /dev/null +++ b/crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json @@ -0,0 +1,28 @@ +{ + "src/Simple.t.sol:SimpleContractTest": { + "duration": "{...}", + "test_results": { + "test()": { + "status": "Success", + "reason": null, + "counterexample": null, + "logs": [], + "decoded_logs": [], + "kind": { + "Unit": { + "gas": "{...}" + } + }, + "traces": [], + "labeled_addresses": {}, + "duration": { + "secs": "{...}", + "nanos": "{...}" + }, + "breakpoints": {}, + "gas_snapshots": {} + } + }, + "warnings": [] + } +} \ No newline at end of file diff --git a/crates/forge/tests/fixtures/SimpleContractTestVerbose.json b/crates/forge/tests/fixtures/SimpleContractTestVerbose.json new file mode 100644 index 000000000..81803d949 --- /dev/null +++ b/crates/forge/tests/fixtures/SimpleContractTestVerbose.json @@ -0,0 +1,217 @@ +{ + "src/Simple.t.sol:SimpleContractTest": { + "duration": "{...}", + "test_results": { + "test()": { + "status": "Success", + "reason": null, + "counterexample": null, + "logs": [ + { + "address": "0x000000000000000000636f6e736f6c652e6c6f67", + "topics": [ + "0x41304facd9323d75b11bcdd609cb38effffdb05710f7caf0e9b16c6d9d709f50" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f56616c7565207365743a20203130300000000000000000000000000000000000" + } + ], + "decoded_logs": [ + "Value set: 100" + ], + "kind": { + "Unit": { + "gas": "{...}" + } + }, + "traces": [ + [ + "Deployment", + { + "arena": [ + { + "parent": null, + "children": [], + "idx": 0, + "trace": { + "depth": 0, + "success": true, + "caller": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38", + "address": "0x7fa9385be102ac3eac297483dd6233d62b3e1496", + "maybe_precompile": false, + "selfdestruct_address": null, + "selfdestruct_refund_target": null, + "selfdestruct_transferred_value": null, + "kind": "CREATE", + "value": "0x0", + "data": "{...}", + "output": "{...}", + "gas_used": "{...}", + "gas_limit": "{...}", + "status": "Return", + "steps": [], + "decoded": { + "label": null, + "return_data": null, + "call_data": null + } + }, + "logs": [], + "ordering": [] + } + ] + } + ], + [ + "Execution", + { + "arena": [ + { + "parent": null, + "children": [ + 1, + 2, + 3 + ], + "idx": 0, + "trace": { + "depth": 0, + "success": true, + "caller": "0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38", + "address": "0x7fa9385be102ac3eac297483dd6233d62b3e1496", + "maybe_precompile": null, + "selfdestruct_address": null, + "selfdestruct_refund_target": null, + "selfdestruct_transferred_value": null, + "kind": "CALL", + "value": "0x0", + "data": "0xf8a8fd6d", + "output": "0x", + "gas_used": "{...}", + "gas_limit": "{...}", + "status": "Stop", + "steps": [], + "decoded": { + "label": null, + "return_data": null, + "call_data": null + } + }, + "logs": [], + "ordering": [ + { + "Call": 0 + }, + { + "Call": 1 + }, + { + "Call": 2 + } + ] + }, + { + "parent": 0, + "children": [], + "idx": 1, + "trace": { + "depth": 1, + "success": true, + "caller": "0x7fa9385be102ac3eac297483dd6233d62b3e1496", + "address": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", + "maybe_precompile": false, + "selfdestruct_address": null, + "selfdestruct_refund_target": null, + "selfdestruct_transferred_value": null, + "kind": "CREATE", + "value": "0x0", + "data": "{...}", + "output": "{...}", + "gas_used": "{...}", + "gas_limit": "{...}", + "status": "Return", + "steps": [], + "decoded": { + "label": null, + "return_data": null, + "call_data": null + } + }, + "logs": [], + "ordering": [] + }, + { + "parent": 0, + "children": [], + "idx": 2, + "trace": { + "depth": 1, + "success": true, + "caller": "0x7fa9385be102ac3eac297483dd6233d62b3e1496", + "address": "0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", + "maybe_precompile": null, + "selfdestruct_address": null, + "selfdestruct_refund_target": null, + "selfdestruct_transferred_value": null, + "kind": "CALL", + "value": "0x0", + "data": "0xe26d14740000000000000000000000000000000000000000000000000000000000000064", + "output": "0x", + "gas_used": "{...}", + "gas_limit": "{...}", + "status": "Stop", + "steps": [], + "decoded": { + "label": null, + "return_data": null, + "call_data": null + } + }, + "logs": [], + "ordering": [] + }, + { + "parent": 0, + "children": [], + "idx": 3, + "trace": { + "depth": 1, + "success": true, + "caller": "0x7fa9385be102ac3eac297483dd6233d62b3e1496", + "address": "0x000000000000000000636f6e736f6c652e6c6f67", + "maybe_precompile": null, + "selfdestruct_address": null, + "selfdestruct_refund_target": null, + "selfdestruct_transferred_value": null, + "kind": "STATICCALL", + "value": "0x0", + "data": "{...}", + "output": "0x", + "gas_used": "{...}", + "gas_limit": "{...}", + "status": "Stop", + "steps": [], + "decoded": { + "label": null, + "return_data": null, + "call_data": null + } + }, + "logs": [], + "ordering": [] + } + ] + } + ] + ], + "labeled_addresses": {}, + "duration": { + "secs": "{...}", + "nanos": "{...}" + }, + "breakpoints": {}, + "gas_snapshots": {} + } + }, + "warnings": [] + } +} \ No newline at end of file diff --git a/crates/forge/tests/it/fuzz.rs b/crates/forge/tests/it/fuzz.rs index 1d810c9c5..d6b047a17 100644 --- a/crates/forge/tests/it/fuzz.rs +++ b/crates/forge/tests/it/fuzz.rs @@ -155,27 +155,60 @@ async fn test_persist_fuzz_failure() { assert_ne!(initial_calldata, new_calldata); } -#[tokio::test(flavor = "multi_thread")] -async fn test_scrape_bytecode() { - let filter = Filter::new(".*", ".*", ".*fuzz/FuzzScrapeBytecode.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); - runner.test_options.fuzz.runs = 2000; - runner.test_options.fuzz.seed = Some(U256::from(100u32)); - let suite_result = runner.test_collect(&filter); +forgetest_init!(test_can_scrape_bytecode, |prj, cmd| { + prj.add_source( + "FuzzerDict.sol", + r#" +// https://github.com/foundry-rs/foundry/issues/1168 +contract FuzzerDict { + // Immutables should get added to the dictionary. + address public immutable immutableOwner; + // Regular storage variables should also get added to the dictionary. + address public storageOwner; + + constructor(address _immutableOwner, address _storageOwner) { + immutableOwner = _immutableOwner; + storageOwner = _storageOwner; + } +} + "#, + ) + .unwrap(); - assert!(!suite_result.is_empty()); + prj.add_test( + "FuzzerDictTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; +import "src/FuzzerDict.sol"; - for (_, SuiteResult { test_results, .. }) in suite_result { - for (test_name, result) in test_results { - match test_name.as_str() { - "testImmutableOwner(address)" | "testStorageOwner(address)" => { - assert_eq!(result.status, TestStatus::Failure) - } - _ => {} - } - } +contract FuzzerDictTest is Test { + FuzzerDict fuzzerDict; + + function setUp() public { + fuzzerDict = new FuzzerDict(address(100), address(200)); + } + + /// forge-config: default.fuzz.runs = 2000 + function testImmutableOwner(address who) public { + assertTrue(who != fuzzerDict.immutableOwner()); + } + + /// forge-config: default.fuzz.runs = 2000 + function testStorageOwner(address who) public { + assertTrue(who != fuzzerDict.storageOwner()); } } + "#, + ) + .unwrap(); + + // Test that immutable address is used as fuzzed input, causing test to fail. + cmd.args(["test", "--fuzz-seed", "100", "--mt", "testImmutableOwner"]).assert_failure(); + // Test that storage address is used as fuzzed input, causing test to fail. + cmd.forge_fuse() + .args(["test", "--fuzz-seed", "100", "--mt", "testStorageOwner"]) + .assert_failure(); +}); // tests that inline max-test-rejects config is properly applied forgetest_init!(test_inline_max_test_rejects, |prj, cmd| { diff --git a/crates/forge/tests/it/inline.rs b/crates/forge/tests/it/inline.rs index ed7729f7f..4448f982d 100644 --- a/crates/forge/tests/it/inline.rs +++ b/crates/forge/tests/it/inline.rs @@ -1,6 +1,6 @@ //! Inline configuration tests. -use crate::test_helpers::TEST_DATA_DEFAULT; +use crate::test_helpers::{ForgeTestData, ForgeTestProfile, TEST_DATA_DEFAULT}; use forge::{result::TestKind, TestOptionsBuilder}; use foundry_config::{FuzzConfig, InvariantConfig}; use foundry_test_utils::Filter; @@ -8,7 +8,8 @@ use foundry_test_utils::Filter; #[tokio::test(flavor = "multi_thread")] async fn inline_config_run_fuzz() { let filter = Filter::new(".*", ".*", ".*inline/FuzzInlineConf.t.sol"); - let mut runner = TEST_DATA_DEFAULT.runner(); + // Fresh runner to make sure there's no persisted failure from previous tests. + let mut runner = ForgeTestData::new(ForgeTestProfile::Default).runner(); let result = runner.test_collect(&filter); let results = result .into_iter() diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index 5a571f772..37b3c9b23 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -3,7 +3,8 @@ use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; use alloy_primitives::U256; use forge::fuzz::CounterExample; -use foundry_test_utils::Filter; +use foundry_config::{Config, InvariantConfig}; +use foundry_test_utils::{forgetest_init, str, Filter}; use std::collections::BTreeMap; macro_rules! get_counterexample { @@ -700,3 +701,157 @@ async fn test_no_reverts_in_counterexample() { } }; } + +// Tests that a persisted failure doesn't fail due to assume revert if test driver is changed. +forgetest_init!(should_not_fail_replay_assume, |prj, cmd| { + let config = Config { + invariant: { + InvariantConfig { fail_on_revert: true, max_assume_rejects: 10, ..Default::default() } + }, + ..Default::default() + }; + prj.write_config(config); + + // Add initial test that breaks invariant. + prj.add_test( + "AssumeTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract AssumeHandler is Test { + function fuzzMe(uint256 a) public { + require(false, "Invariant failure"); + } +} + +contract AssumeTest is Test { + function setUp() public { + AssumeHandler handler = new AssumeHandler(); + } + function invariant_assume() public {} +} + "#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "invariant_assume"]).assert_failure().stdout_eq(str![[r#" +... +[FAIL: revert: Invariant failure] +... +"#]]); + + // Change test to use assume instead require. Same test should fail with too many inputs + // rejected message instead persisted failure revert. + prj.add_test( + "AssumeTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract AssumeHandler is Test { + function fuzzMe(uint256 a) public { + vm.assume(false); + } +} + +contract AssumeTest is Test { + function setUp() public { + AssumeHandler handler = new AssumeHandler(); + } + function invariant_assume() public {} +} + "#, + ) + .unwrap(); + + cmd.assert_failure().stdout_eq(str![[r#" +... +[FAIL: `vm.assume` rejected too many inputs (10 allowed)] invariant_assume() (runs: 0, calls: 0, reverts: 0) +... +"#]]); +}); + +// Test too many inputs rejected for `assumePrecompile`/`assumeForgeAddress`. +// +forgetest_init!(should_revert_with_assume_code, |prj, cmd| { + let config = Config { + invariant: { + InvariantConfig { fail_on_revert: true, max_assume_rejects: 10, ..Default::default() } + }, + ..Default::default() + }; + prj.write_config(config); + + // Add initial test that breaks invariant. + prj.add_test( + "AssumeTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract BalanceTestHandler is Test { + address public ref = address(1412323); + address alice; + + constructor(address _alice) { + alice = _alice; + } + + function increment(uint256 amount_, address addr) public { + assumeNotPrecompile(addr); + assumeNotForgeAddress(addr); + assertEq(alice.balance, 100_000 ether); + } +} + +contract BalanceAssumeTest is Test { + function setUp() public { + address alice = makeAddr("alice"); + vm.deal(alice, 100_000 ether); + targetSender(alice); + BalanceTestHandler handler = new BalanceTestHandler(alice); + targetContract(address(handler)); + } + + function invariant_balance() public {} +} + "#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "invariant_balance"]).assert_failure().stdout_eq(str![[r#" +... +[FAIL: `vm.assume` rejected too many inputs (10 allowed)] invariant_balance() (runs: 0, calls: 0, reverts: 0) +... +"#]]); +}); + +// Test proper message displayed if `targetSelector`/`excludeSelector` called with empty selectors. +// +forgetest_init!(should_not_panic_if_no_selectors, |prj, cmd| { + prj.add_test( + "NoSelectorTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract TestHandler is Test {} + +contract NoSelectorTest is Test { + bytes4[] selectors; + + function setUp() public { + TestHandler handler = new TestHandler(); + targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); + excludeSelector(FuzzSelector({addr: address(handler), selectors: selectors})); + } + + function invariant_panic() public {} +} + "#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "invariant_panic"]).assert_failure().stdout_eq(str![[r#" +... +[FAIL: failed to set up invariant testing environment: No contracts to fuzz.] invariant_panic() (runs: 0, calls: 0, reverts: 0) +... +"#]]); +}); diff --git a/crates/forge/tests/it/main.rs b/crates/forge/tests/it/main.rs index 4d0120bd2..728e593b6 100644 --- a/crates/forge/tests/it/main.rs +++ b/crates/forge/tests/it/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::needless_return)] + pub mod config; pub mod test_helpers; diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index b26b36592..c2154b6aa 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -250,7 +250,7 @@ test_repro!(6355, false, None, |res| { let test = res.test_results.remove("test_shouldFail()").unwrap(); assert_eq!(test.status, TestStatus::Failure); - let test = res.test_results.remove("test_shouldFailWithRevertTo()").unwrap(); + let test = res.test_results.remove("test_shouldFailWithRevertToState()").unwrap(); assert_eq!(test.status, TestStatus::Failure); }); @@ -386,3 +386,10 @@ test_repro!(1543); // https://github.com/foundry-rs/foundry/issues/6643 test_repro!(6643); + +// https://github.com/foundry-rs/foundry/issues/8971 +test_repro!(8971; |config| { + let mut prj_config = Config::clone(&config.runner.config); + prj_config.isolate = true; + config.runner.config = Arc::new(prj_config); +}); diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index 1ef9c9add..e44ee7748 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -283,9 +283,12 @@ impl<'a> Linker<'a> { #[cfg(test)] mod tests { use super::*; - use alloy_primitives::fixed_bytes; - use foundry_compilers::{Project, ProjectCompileOutput, ProjectPathsConfig}; - use std::collections::HashMap; + use alloy_primitives::{fixed_bytes, map::HashMap}; + use foundry_compilers::{ + multi::MultiCompiler, + solc::{Solc, SolcCompiler}, + Project, ProjectCompileOutput, ProjectPathsConfig, + }; struct LinkerTest { project: Project, @@ -304,11 +307,12 @@ mod tests { .build() .unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 18)).unwrap(); let project = Project::builder() .paths(paths) .ephemeral() .no_artifacts() - .build(Default::default()) + .build(MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None }) .unwrap(); let mut output = project.compile().unwrap(); @@ -317,7 +321,7 @@ mod tests { output = output.with_stripped_file_prefixes(project.root()); } - Self { project, output, dependency_assertions: HashMap::new() } + Self { project, output, dependency_assertions: HashMap::default() } } fn assert_dependencies( @@ -394,7 +398,7 @@ mod tests { for (dep_identifier, address) in assertions { let (file, name) = dep_identifier.split_once(':').unwrap(); if let Some(lib_address) = - libraries.libs.get(&PathBuf::from(file)).and_then(|libs| libs.get(name)) + libraries.libs.get(Path::new(file)).and_then(|libs| libs.get(name)) { assert_eq!( *lib_address, @@ -638,7 +642,7 @@ mod tests { "default/linking/nested/Nested.t.sol:NestedLib".to_string(), vec![( "default/linking/nested/Nested.t.sol:Lib".to_string(), - Address::from_str("0xCD3864eB2D88521a5477691EE589D9994b796834").unwrap(), + Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74").unwrap(), )], ) .assert_dependencies( @@ -648,12 +652,12 @@ mod tests { // have the same address and nonce. ( "default/linking/nested/Nested.t.sol:Lib".to_string(), - Address::from_str("0xCD3864eB2D88521a5477691EE589D9994b796834") + Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74") .unwrap(), ), ( "default/linking/nested/Nested.t.sol:NestedLib".to_string(), - Address::from_str("0x023d9a6bfA39c45997572dC4F87b3E2713b6EBa4") + Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369") .unwrap(), ), ], @@ -663,12 +667,12 @@ mod tests { vec![ ( "default/linking/nested/Nested.t.sol:Lib".to_string(), - Address::from_str("0xCD3864eB2D88521a5477691EE589D9994b796834") + Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74") .unwrap(), ), ( "default/linking/nested/Nested.t.sol:NestedLib".to_string(), - Address::from_str("0x023d9a6bfA39c45997572dC4F87b3E2713b6EBa4") + Address::from_str("0xfebE2F30641170642f317Ff6F644Cee60E7Ac369") .unwrap(), ), ], diff --git a/crates/macros/src/cheatcodes.rs b/crates/macros/src/cheatcodes.rs index b38186e57..d9c2d2c91 100644 --- a/crates/macros/src/cheatcodes.rs +++ b/crates/macros/src/cheatcodes.rs @@ -1,5 +1,5 @@ use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::quote; use syn::{Attribute, Data, DataStruct, DeriveInput, Error, Result}; pub fn derive_cheatcode(input: &DeriveInput) -> Result { @@ -20,7 +20,7 @@ pub fn derive_cheatcode(input: &DeriveInput) -> Result { /// Implements `CheatcodeDef` for a function call struct. fn derive_call(name: &Ident, data: &DataStruct, attrs: &[Attribute]) -> Result { let mut group = None::; - let mut status = None::; + let mut status = None::; let mut safety = None::; for attr in attrs.iter().filter(|a| a.path().is_ident("cheatcode")) { attr.meta.require_list()?.parse_nested_meta(|meta| { @@ -38,17 +38,14 @@ fn derive_call(name: &Ident, data: &DataStruct, attrs: &[Attribute]) -> Result - panic!("cannot determine safety from the group, add a `#[cheatcode(safety = ...)]` attribute") - }; quote! { match Group::#group.safety() { Some(s) => s, - None => #panic, + None => panic_unknown_safety(), } } }; diff --git a/crates/script-sequence/Cargo.toml b/crates/script-sequence/Cargo.toml new file mode 100644 index 000000000..4b63dc1fe --- /dev/null +++ b/crates/script-sequence/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "forge-script-sequence" +description = "Script sequence types" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +foundry-config.workspace = true +foundry-common.workspace = true +foundry-compilers = { workspace = true, features = ["full"] } +foundry-zksync-core.workspace = true + +serde.workspace = true +eyre.workspace = true +serde_json.workspace = true +tracing.workspace = true + +revm-inspectors.workspace = true + +alloy-rpc-types.workspace = true +alloy-primitives.workspace = true diff --git a/crates/script-sequence/src/lib.rs b/crates/script-sequence/src/lib.rs new file mode 100644 index 000000000..3aa5fc65a --- /dev/null +++ b/crates/script-sequence/src/lib.rs @@ -0,0 +1,7 @@ +//! Script Sequence and related types. + +pub mod sequence; +pub mod transaction; + +pub use sequence::*; +pub use transaction::*; diff --git a/crates/script-sequence/src/sequence.rs b/crates/script-sequence/src/sequence.rs new file mode 100644 index 000000000..080c725be --- /dev/null +++ b/crates/script-sequence/src/sequence.rs @@ -0,0 +1,248 @@ +use crate::transaction::TransactionWithMetadata; +use alloy_primitives::{hex, map::HashMap, TxHash}; +use alloy_rpc_types::AnyTransactionReceipt; +use eyre::{ContextCompat, Result, WrapErr}; +use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN}; +use foundry_compilers::ArtifactId; +use foundry_config::Config; +use serde::{Deserialize, Serialize}; +use std::{ + collections::VecDeque, + io::{BufWriter, Write}, + path::PathBuf, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +pub const DRY_RUN_DIR: &str = "dry-run"; + +#[derive(Clone, Serialize, Deserialize)] +pub struct NestedValue { + pub internal_type: String, + pub value: String, +} + +/// Helper that saves the transactions sequence and its state on which transactions have been +/// broadcasted +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct ScriptSequence { + pub transactions: VecDeque, + pub receipts: Vec, + pub libraries: Vec, + pub pending: Vec, + #[serde(skip)] + /// Contains paths to the sequence files + /// None if sequence should not be saved to disk (e.g. part of a multi-chain sequence) + pub paths: Option<(PathBuf, PathBuf)>, + pub returns: HashMap, + pub timestamp: u64, + pub chain: u64, + pub commit: Option, +} + +/// Sensitive values from the transactions in a script sequence +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct SensitiveTransactionMetadata { + pub rpc: String, +} + +/// Sensitive info from the script sequence which is saved into the cache folder +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct SensitiveScriptSequence { + pub transactions: VecDeque, +} + +impl From for SensitiveScriptSequence { + fn from(sequence: ScriptSequence) -> Self { + Self { + transactions: sequence + .transactions + .iter() + .map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() }) + .collect(), + } + } +} + +impl ScriptSequence { + /// Loads The sequence for the corresponding json file + pub fn load( + config: &Config, + sig: &str, + target: &ArtifactId, + chain_id: u64, + dry_run: bool, + ) -> Result { + let (path, sensitive_path) = Self::get_paths(config, sig, target, chain_id, dry_run)?; + + let mut script_sequence: Self = fs::read_json_file(&path) + .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?; + + let sensitive_script_sequence: SensitiveScriptSequence = fs::read_json_file( + &sensitive_path, + ) + .wrap_err(format!("Deployment's sensitive details not found for chain `{chain_id}`."))?; + + script_sequence.fill_sensitive(&sensitive_script_sequence); + + script_sequence.paths = Some((path, sensitive_path)); + + Ok(script_sequence) + } + + /// Saves the transactions as file if it's a standalone deployment. + /// `save_ts` should be set to true for checkpoint updates, which might happen many times and + /// could result in us saving many identical files. + pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> { + self.sort_receipts(); + + if self.transactions.is_empty() { + return Ok(()) + } + + let Some((path, sensitive_path)) = self.paths.clone() else { return Ok(()) }; + + self.timestamp = now().as_secs(); + let ts_name = format!("run-{}.json", self.timestamp); + + let sensitive_script_sequence: SensitiveScriptSequence = self.clone().into(); + + // broadcast folder writes + //../run-latest.json + let mut writer = BufWriter::new(fs::create_file(&path)?); + serde_json::to_writer_pretty(&mut writer, &self)?; + writer.flush()?; + if save_ts { + //../run-[timestamp].json + fs::copy(&path, path.with_file_name(&ts_name))?; + } + + // cache folder writes + //../run-latest.json + let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?); + serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?; + writer.flush()?; + if save_ts { + //../run-[timestamp].json + fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?; + } + + if !silent { + shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; + shell::println(format!("Sensitive values saved to: {}\n", sensitive_path.display()))?; + } + + Ok(()) + } + + pub fn add_receipt(&mut self, receipt: AnyTransactionReceipt) { + self.receipts.push(receipt); + } + + /// Sorts all receipts with ascending transaction index + pub fn sort_receipts(&mut self) { + self.receipts.sort_by_key(|r| (r.block_number, r.transaction_index)); + } + + pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) { + if !self.pending.contains(&tx_hash) { + self.transactions[index].hash = Some(tx_hash); + self.pending.push(tx_hash); + } + } + + pub fn remove_pending(&mut self, tx_hash: TxHash) { + self.pending.retain(|element| element != &tx_hash); + } + + /// Gets paths in the formats + /// `./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json` and + /// `./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json`. + pub fn get_paths( + config: &Config, + sig: &str, + target: &ArtifactId, + chain_id: u64, + dry_run: bool, + ) -> Result<(PathBuf, PathBuf)> { + let mut broadcast = config.broadcast.to_path_buf(); + let mut cache = config.cache_path.to_path_buf(); + let mut common = PathBuf::new(); + + let target_fname = target.source.file_name().wrap_err("No filename.")?; + common.push(target_fname); + common.push(chain_id.to_string()); + if dry_run { + common.push(DRY_RUN_DIR); + } + + broadcast.push(common.clone()); + cache.push(common); + + fs::create_dir_all(&broadcast)?; + fs::create_dir_all(&cache)?; + + // TODO: ideally we want the name of the function here if sig is calldata + let filename = sig_to_file_name(sig); + + broadcast.push(format!("{filename}-latest.json")); + cache.push(format!("{filename}-latest.json")); + + Ok((broadcast, cache)) + } + + /// Returns the first RPC URL of this sequence. + pub fn rpc_url(&self) -> &str { + self.transactions.front().expect("empty sequence").rpc.as_str() + } + + /// Returns the list of the transactions without the metadata. + pub fn transactions(&self) -> impl Iterator { + self.transactions.iter().map(|tx| tx.tx()) + } + + pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) { + self.transactions + .iter_mut() + .enumerate() + .for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc)); + } +} + +/// Converts the `sig` argument into the corresponding file path. +/// +/// This accepts either the signature of the function or the raw calldata. +pub fn sig_to_file_name(sig: &str) -> String { + if let Some((name, _)) = sig.split_once('(') { + // strip until call argument parenthesis + return name.to_string() + } + // assume calldata if `sig` is hex + if let Ok(calldata) = hex::decode(sig) { + // in which case we return the function signature + return hex::encode(&calldata[..SELECTOR_LEN]) + } + + // return sig as is + sig.to_string() +} + +pub fn now() -> Duration { + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_convert_sig() { + assert_eq!(sig_to_file_name("run()").as_str(), "run"); + assert_eq!( + sig_to_file_name( + "522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266" + ) + .as_str(), + "522bb704" + ); + } +} diff --git a/crates/script-sequence/src/transaction.rs b/crates/script-sequence/src/transaction.rs new file mode 100644 index 000000000..04c33199e --- /dev/null +++ b/crates/script-sequence/src/transaction.rs @@ -0,0 +1,79 @@ +use alloy_primitives::{Address, Bytes, B256}; +use foundry_common::TransactionMaybeSigned; +use foundry_zksync_core::ZkTransactionMetadata; +use revm_inspectors::tracing::types::CallKind; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalContract { + #[serde(rename = "transactionType")] + pub opcode: CallKind, + pub address: Address, + pub init_code: Bytes, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionWithMetadata { + pub hash: Option, + #[serde(rename = "transactionType")] + pub opcode: CallKind, + #[serde(default = "default_string")] + pub contract_name: Option, + #[serde(default = "default_address")] + pub contract_address: Option
, + #[serde(default = "default_string")] + pub function: Option, + #[serde(default = "default_vec_of_strings")] + pub arguments: Option>, + #[serde(skip)] + pub rpc: String, + pub transaction: TransactionMaybeSigned, + pub additional_contracts: Vec, + pub is_fixed_gas_limit: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub zk: Option, +} + +fn default_string() -> Option { + Some(String::new()) +} + +fn default_address() -> Option
{ + Some(Address::ZERO) +} + +fn default_vec_of_strings() -> Option> { + Some(vec![]) +} + +impl TransactionWithMetadata { + pub fn from_tx_request(transaction: TransactionMaybeSigned) -> Self { + Self { + transaction, + hash: Default::default(), + opcode: Default::default(), + contract_name: Default::default(), + contract_address: Default::default(), + function: Default::default(), + arguments: Default::default(), + is_fixed_gas_limit: Default::default(), + additional_contracts: Default::default(), + rpc: Default::default(), + zk: Default::default(), + } + } + + pub fn tx(&self) -> &TransactionMaybeSigned { + &self.transaction + } + + pub fn tx_mut(&mut self) -> &mut TransactionMaybeSigned { + &mut self.transaction + } + + pub fn is_create2(&self) -> bool { + self.opcode == CallKind::Create2 + } +} diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 0e7a01e70..2ca07bd68 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -23,6 +23,7 @@ foundry-debugger.workspace = true foundry-cheatcodes.workspace = true foundry-wallets.workspace = true foundry-linking.workspace = true +forge-script-sequence.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compiler.workspace = true @@ -35,6 +36,7 @@ tracing.workspace = true clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } semver.workspace = true futures.workspace = true +tokio.workspace = true async-recursion = "1.0.5" itertools.workspace = true diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index ec56acf5c..51e912da2 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -6,14 +6,18 @@ use alloy_chains::Chain; use alloy_consensus::{Transaction, TxEnvelope}; use alloy_eips::eip2718::Encodable2718; use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; -use alloy_primitives::{utils::format_units, Address, TxHash}; +use alloy_primitives::{ + map::{AddressHashMap, AddressHashSet}, + utils::format_units, + Address, TxHash, +}; use alloy_provider::{utils::Eip1559Estimation, Provider}; use alloy_rpc_types::TransactionRequest; use alloy_serde::WithOtherFields; use alloy_transport::Transport; use eyre::{bail, Context, Result}; use forge_verify::provider::VerificationProviderType; -use foundry_cheatcodes::ScriptWallets; +use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; use foundry_common::{ provider::{get_http_provider, try_get_http_provider, RetryProvider}, @@ -26,10 +30,7 @@ use foundry_zksync_core::{ }; use futures::{future::join_all, StreamExt}; use itertools::Itertools; -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; +use std::{cmp::Ordering, sync::Arc}; use zksync_web3_rs::eip712::{Eip712Meta, Eip712Transaction, Eip712TransactionRequest}; pub async fn estimate_gas( @@ -47,7 +48,7 @@ where tx.set_gas_limit( provider.estimate_gas(tx).await.wrap_err("Failed to estimate gas for tx")? * - estimate_multiplier as u128 / + estimate_multiplier / 100, ); Ok(()) @@ -116,11 +117,26 @@ pub async fn send_transaction( if let SendTransactionKind::Raw(tx, _) | SendTransactionKind::Unlocked(tx) = &mut kind { if sequential_broadcast { let from = tx.from.expect("no sender"); - let nonce = provider.get_transaction_count(from).await?; let tx_nonce = tx.nonce.expect("no nonce"); - if nonce != tx_nonce { - bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") + for attempt in 0..5 { + let nonce = provider.get_transaction_count(from).await?; + match nonce.cmp(&tx_nonce) { + Ordering::Greater => { + bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") + } + Ordering::Less => { + if attempt == 4 { + bail!("After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce}).") + } + warn!("Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."); + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + } + Ordering::Equal => { + // Nonces are equal, we can proceed + break; + } + } } } @@ -197,9 +213,9 @@ pub enum SendTransactionKind<'a> { /// Represents how to send _all_ transactions pub enum SendTransactionsKind { /// Send via `eth_sendTransaction` and rely on the `from` address being unlocked. - Unlocked(HashSet
), + Unlocked(AddressHashSet), /// Send a signed transaction via `eth_sendRawTransaction` - Raw(HashMap), + Raw(AddressHashMap), } impl SendTransactionsKind { @@ -230,12 +246,12 @@ impl SendTransactionsKind { } /// State after we have bundled all -/// [`TransactionWithMetadata`](crate::transaction::TransactionWithMetadata) objects into a single -/// [`ScriptSequenceKind`] object containing one or more script sequences. +/// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a +/// single [`ScriptSequenceKind`] object containing one or more script sequences. pub struct BundledState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub sequence: ScriptSequenceKind, } @@ -281,7 +297,7 @@ impl BundledState { .sequences() .iter() .flat_map(|sequence| sequence.transactions().map(|tx| tx.from().expect("missing from"))) - .collect::>(); + .collect::(); if required_addresses.contains(&Config::DEFAULT_SENDER) { eyre::bail!( @@ -378,7 +394,7 @@ impl BundledState { tx.set_chain_id(sequence.chain); - // Set TxKind::Create explicitly to satify `check_reqd_fields` in + // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in // alloy if tx.to.is_none() { tx.set_create(); diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 2fd5a83e8..67d95cfab 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -1,14 +1,12 @@ use crate::{ - broadcast::BundledState, - execute::LinkedState, - multi_sequence::MultiChainSequence, - sequence::{ScriptSequence, ScriptSequenceKind}, - ScriptArgs, ScriptConfig, + broadcast::BundledState, execute::LinkedState, multi_sequence::MultiChainSequence, + sequence::ScriptSequenceKind, ScriptArgs, ScriptConfig, }; use alloy_primitives::{Bytes, B256}; use alloy_provider::Provider; use eyre::{OptionExt, Result}; -use foundry_cheatcodes::ScriptWallets; +use forge_script_sequence::ScriptSequence; +use foundry_cheatcodes::Wallets; use foundry_common::{ compile::ProjectCompiler, provider::try_get_http_provider, ContractData, ContractsByArtifact, }; @@ -195,7 +193,7 @@ impl LinkedBuildData { pub struct PreprocessedState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, } impl PreprocessedState { @@ -316,7 +314,7 @@ impl PreprocessedState { pub struct CompiledState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: BuildData, } diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index a58955acd..194af7066 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -6,12 +6,15 @@ use crate::{ }; use alloy_dyn_abi::FunctionExt; use alloy_json_abi::{Function, InternalType, JsonAbi}; -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::{ + map::{HashMap, HashSet}, + Address, Bytes, +}; use alloy_provider::Provider; use alloy_rpc_types::TransactionInput; use async_recursion::async_recursion; use eyre::{OptionExt, Result}; -use foundry_cheatcodes::ScriptWallets; +use foundry_cheatcodes::Wallets; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, @@ -31,7 +34,6 @@ use foundry_evm::{ }; use futures::future::join_all; use itertools::Itertools; -use std::collections::{HashMap, HashSet}; use yansi::Paint; /// State after linking, contains the linked build data along with library addresses and optional @@ -39,7 +41,7 @@ use yansi::Paint; pub struct LinkedState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, } @@ -91,7 +93,7 @@ impl LinkedState { pub struct PreExecutionState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, } @@ -274,7 +276,7 @@ pub struct ExecutionArtifacts { pub struct ExecutedState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_result: ScriptResult, @@ -352,7 +354,7 @@ impl ExecutedState { /// Collects the return values from the execution result. fn get_returns(&self) -> Result> { - let mut returns = HashMap::new(); + let mut returns = HashMap::default(); let returned = &self.execution_result.returned; let func = &self.execution_data.func; diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index e7a11a75a..08b217c95 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -8,16 +8,20 @@ #[macro_use] extern crate tracing; -use self::transaction::AdditionalContract; use crate::runner::ScriptRunner; use alloy_json_abi::{Function, JsonAbi}; -use alloy_primitives::{hex, Address, Bytes, Log, TxKind, U256}; +use alloy_primitives::{ + hex, + map::{AddressHashMap, HashMap}, + Address, Bytes, Log, TxKind, U256, +}; use alloy_signer::Signer; use broadcast::next_nonce; use build::PreprocessedState; use clap::{Parser, ValueHint}; use dialoguer::Confirm; use eyre::{ContextCompat, Result}; +use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::RetryArgs; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{ @@ -39,7 +43,7 @@ use foundry_evm::{ constants::DEFAULT_CREATE2_DEPLOYER, executors::ExecutorBuilder, inspectors::{ - cheatcodes::{BroadcastableTransactions, ScriptWallets}, + cheatcodes::{BroadcastableTransactions, Wallets}, CheatsConfig, }, opts::EvmOpts, @@ -47,8 +51,7 @@ use foundry_evm::{ }; use foundry_wallets::MultiWalletOpts; use foundry_zksync_compiler::DualCompiledContracts; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use serde::Serialize; use yansi::Paint; mod broadcast; @@ -205,7 +208,7 @@ pub struct ScriptArgs { impl ScriptArgs { pub async fn preprocess(self) -> Result { let script_wallets = - ScriptWallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); + Wallets::new(self.wallets.get_multi_wallet().await?, self.evm_opts.sender); let (config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; @@ -479,7 +482,7 @@ pub struct ScriptResult { pub logs: Vec, pub traces: Traces, pub gas_used: u64, - pub labeled_addresses: HashMap, + pub labeled_addresses: AddressHashMap, #[serde(skip)] pub transactions: Option, pub returned: Bytes, @@ -516,12 +519,6 @@ struct JsonResult<'a> { result: &'a ScriptResult, } -#[derive(Clone, Serialize, Deserialize)] -pub struct NestedValue { - pub internal_type: String, - pub value: String, -} - #[derive(Clone, Debug)] pub struct ScriptConfig { pub config: Config, @@ -539,7 +536,7 @@ impl ScriptConfig { // dapptools compatibility 1 }; - Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::new() }) + Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default() }) } pub async fn update_sender(&mut self, sender: Address) -> Result<()> { @@ -560,7 +557,7 @@ impl ScriptConfig { async fn get_runner_with_cheatcodes( &mut self, known_contracts: ContractsByArtifact, - script_wallets: ScriptWallets, + script_wallets: Wallets, debug: bool, target: ArtifactId, dual_compiled_contracts: DualCompiledContracts, @@ -574,12 +571,7 @@ impl ScriptConfig { async fn _get_runner( &mut self, - cheats_data: Option<( - ContractsByArtifact, - ScriptWallets, - ArtifactId, - DualCompiledContracts, - )>, + cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId, DualCompiledContracts)>, debug: bool, ) -> Result { trace!("preparing script runner"); @@ -625,13 +617,14 @@ impl ScriptConfig { &self.config, self.evm_opts.clone(), Some(known_contracts), - Some(script_wallets), + Some(target.name), Some(target.version), dual_compiled_contracts, use_zk, ) .into(), ) + .wallets(script_wallets) .enable_isolation(self.evm_opts.isolate) }) .use_zk_vm(use_zk); diff --git a/crates/script/src/multi_sequence.rs b/crates/script/src/multi_sequence.rs index 24490bf01..0aabcf79a 100644 --- a/crates/script/src/multi_sequence.rs +++ b/crates/script/src/multi_sequence.rs @@ -1,6 +1,7 @@ -use super::sequence::{sig_to_file_name, ScriptSequence, SensitiveScriptSequence, DRY_RUN_DIR}; use eyre::{ContextCompat, Result, WrapErr}; -use foundry_cli::utils::now; +use forge_script_sequence::{ + now, sig_to_file_name, ScriptSequence, SensitiveScriptSequence, DRY_RUN_DIR, +}; use foundry_common::fs; use foundry_compilers::ArtifactId; use foundry_config::Config; diff --git a/crates/script/src/progress.rs b/crates/script/src/progress.rs index 9fb4fbb2f..050ce4002 100644 --- a/crates/script/src/progress.rs +++ b/crates/script/src/progress.rs @@ -1,29 +1,30 @@ -use crate::{ - receipts::{check_tx_status, format_receipt, TxStatus}, - sequence::ScriptSequence, -}; +use crate::receipts::{check_tx_status, format_receipt, TxStatus}; use alloy_chains::Chain; -use alloy_primitives::B256; +use alloy_primitives::{ + map::{B256HashMap, HashMap}, + B256, +}; use eyre::Result; +use forge_script_sequence::ScriptSequence; use foundry_cli::utils::init_progress; use foundry_common::provider::RetryProvider; use futures::StreamExt; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use parking_lot::RwLock; -use std::{collections::HashMap, fmt::Write, sync::Arc, time::Duration}; +use std::{fmt::Write, sync::Arc, time::Duration}; use yansi::Paint; /// State of [ProgressBar]s displayed for the given [ScriptSequence]. #[derive(Debug)] pub struct SequenceProgressState { - /// The top spinner with containt of the format "Sequence #{id} on {network} | {status}"" + /// The top spinner with content of the format "Sequence #{id} on {network} | {status}"" top_spinner: ProgressBar, /// Progress bar with the count of transactions. txs: ProgressBar, /// Progress var with the count of confirmed transactions. receipts: ProgressBar, /// Standalone spinners for pending transactions. - tx_spinners: HashMap, + tx_spinners: B256HashMap, /// Copy of the main [MultiProgress] instance. multi: MultiProgress, } diff --git a/crates/script/src/providers.rs b/crates/script/src/providers.rs index 7f1aa0eb3..eb6ea9319 100644 --- a/crates/script/src/providers.rs +++ b/crates/script/src/providers.rs @@ -1,12 +1,9 @@ +use alloy_primitives::map::{hash_map::Entry, HashMap}; use alloy_provider::{utils::Eip1559Estimation, Provider}; use eyre::{Result, WrapErr}; use foundry_common::provider::{get_http_provider, RetryProvider}; use foundry_config::Chain; -use std::{ - collections::{hash_map::Entry, HashMap}, - ops::Deref, - sync::Arc, -}; +use std::{ops::Deref, sync::Arc}; /// Contains a map of RPC urls to single instances of [`ProviderInfo`]. #[derive(Default)] diff --git a/crates/script/src/sequence.rs b/crates/script/src/sequence.rs index 578391697..8bdb35bb3 100644 --- a/crates/script/src/sequence.rs +++ b/crates/script/src/sequence.rs @@ -1,23 +1,10 @@ -use super::{multi_sequence::MultiChainSequence, NestedValue}; -use crate::{ - transaction::{AdditionalContract, TransactionWithMetadata}, - verify::VerifyBundle, -}; -use alloy_primitives::{hex, Address, TxHash}; -use alloy_rpc_types::AnyTransactionReceipt; -use eyre::{eyre, ContextCompat, Result, WrapErr}; -use forge_verify::provider::VerificationProviderType; -use foundry_cli::utils::{now, Git}; -use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN}; +use crate::multi_sequence::MultiChainSequence; +use eyre::Result; +use forge_script_sequence::ScriptSequence; +use foundry_cli::utils::Git; use foundry_compilers::ArtifactId; use foundry_config::Config; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, VecDeque}, - io::{BufWriter, Write}, - path::{Path, PathBuf}, -}; -use yansi::Paint; +use std::path::Path; /// Returns the commit hash of the project if it exists pub fn get_commit_hash(root: &Path) -> Option { @@ -79,339 +66,3 @@ impl Drop for ScriptSequenceKind { } } } - -pub const DRY_RUN_DIR: &str = "dry-run"; - -/// Helper that saves the transactions sequence and its state on which transactions have been -/// broadcasted -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct ScriptSequence { - pub transactions: VecDeque, - pub receipts: Vec, - pub libraries: Vec, - pub pending: Vec, - #[serde(skip)] - /// Contains paths to the sequence files - /// None if sequence should not be saved to disk (e.g. part of a multi-chain sequence) - pub paths: Option<(PathBuf, PathBuf)>, - pub returns: HashMap, - pub timestamp: u64, - pub chain: u64, - pub commit: Option, -} - -/// Sensitive values from the transactions in a script sequence -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct SensitiveTransactionMetadata { - pub rpc: String, -} - -/// Sensitive info from the script sequence which is saved into the cache folder -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct SensitiveScriptSequence { - pub transactions: VecDeque, -} - -impl From for SensitiveScriptSequence { - fn from(sequence: ScriptSequence) -> Self { - Self { - transactions: sequence - .transactions - .iter() - .map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() }) - .collect(), - } - } -} - -impl ScriptSequence { - /// Loads The sequence for the corresponding json file - pub fn load( - config: &Config, - sig: &str, - target: &ArtifactId, - chain_id: u64, - dry_run: bool, - ) -> Result { - let (path, sensitive_path) = Self::get_paths(config, sig, target, chain_id, dry_run)?; - - let mut script_sequence: Self = foundry_compilers::utils::read_json_file(&path) - .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?; - - let sensitive_script_sequence: SensitiveScriptSequence = - foundry_compilers::utils::read_json_file(&sensitive_path).wrap_err(format!( - "Deployment's sensitive details not found for chain `{chain_id}`." - ))?; - - script_sequence.fill_sensitive(&sensitive_script_sequence); - - script_sequence.paths = Some((path, sensitive_path)); - - Ok(script_sequence) - } - - /// Saves the transactions as file if it's a standalone deployment. - /// `save_ts` should be set to true for checkpoint updates, which might happen many times and - /// could result in us saving many identical files. - pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> { - self.sort_receipts(); - - if self.transactions.is_empty() { - return Ok(()) - } - - let Some((path, sensitive_path)) = self.paths.clone() else { return Ok(()) }; - - self.timestamp = now().as_secs(); - let ts_name = format!("run-{}.json", self.timestamp); - - let sensitive_script_sequence: SensitiveScriptSequence = self.clone().into(); - - // broadcast folder writes - //../run-latest.json - let mut writer = BufWriter::new(fs::create_file(&path)?); - serde_json::to_writer_pretty(&mut writer, &self)?; - writer.flush()?; - if save_ts { - //../run-[timestamp].json - fs::copy(&path, path.with_file_name(&ts_name))?; - } - - // cache folder writes - //../run-latest.json - let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?); - serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?; - writer.flush()?; - if save_ts { - //../run-[timestamp].json - fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?; - } - - if !silent { - shell::println(format!("\nTransactions saved to: {}\n", path.display()))?; - shell::println(format!("Sensitive values saved to: {}\n", sensitive_path.display()))?; - } - - Ok(()) - } - - pub fn add_receipt(&mut self, receipt: AnyTransactionReceipt) { - self.receipts.push(receipt); - } - - /// Sorts all receipts with ascending transaction index - pub fn sort_receipts(&mut self) { - self.receipts.sort_by_key(|r| (r.block_number, r.transaction_index)); - } - - pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) { - if !self.pending.contains(&tx_hash) { - self.transactions[index].hash = Some(tx_hash); - self.pending.push(tx_hash); - } - } - - pub fn remove_pending(&mut self, tx_hash: TxHash) { - self.pending.retain(|element| element != &tx_hash); - } - - /// Gets paths in the formats - /// `./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json` and - /// `./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json`. - pub fn get_paths( - config: &Config, - sig: &str, - target: &ArtifactId, - chain_id: u64, - dry_run: bool, - ) -> Result<(PathBuf, PathBuf)> { - let mut broadcast = config.broadcast.to_path_buf(); - let mut cache = config.cache_path.to_path_buf(); - let mut common = PathBuf::new(); - - let target_fname = target.source.file_name().wrap_err("No filename.")?; - common.push(target_fname); - common.push(chain_id.to_string()); - if dry_run { - common.push(DRY_RUN_DIR); - } - - broadcast.push(common.clone()); - cache.push(common); - - fs::create_dir_all(&broadcast)?; - fs::create_dir_all(&cache)?; - - // TODO: ideally we want the name of the function here if sig is calldata - let filename = sig_to_file_name(sig); - - broadcast.push(format!("{filename}-latest.json")); - cache.push(format!("{filename}-latest.json")); - - Ok((broadcast, cache)) - } - - /// Given the broadcast log, it matches transactions with receipts, and tries to verify any - /// created contract on etherscan. - pub async fn verify_contracts( - &mut self, - config: &Config, - mut verify: VerifyBundle, - ) -> Result<()> { - trace!(target: "script", "verifying {} contracts [{}]", verify.known_contracts.len(), self.chain); - - verify.set_chain(config, self.chain.into()); - - if verify.etherscan.has_key() || - verify.verifier.verifier != VerificationProviderType::Etherscan - { - trace!(target: "script", "prepare future verifications"); - - let mut future_verifications = Vec::with_capacity(self.receipts.len()); - let mut unverifiable_contracts = vec![]; - - // Make sure the receipts have the right order first. - self.sort_receipts(); - - for (receipt, tx) in self.receipts.iter_mut().zip(self.transactions.iter()) { - // create2 hash offset - let mut offset = 0; - - if tx.is_create2() { - receipt.contract_address = tx.contract_address; - offset = 32; - } - - // Verify contract created directly from the transaction - if let (Some(address), Some(data)) = (receipt.contract_address, tx.tx().input()) { - if config.zksync.run_in_zk_mode() { - match verify.get_verify_args_zk(address, data, &self.libraries) { - Some(verify) => future_verifications.push(verify.run()), - None => unverifiable_contracts.push(address), - }; - } else { - match verify.get_verify_args(address, offset, data, &self.libraries) { - Some(verify) => future_verifications.push(verify.run()), - None => unverifiable_contracts.push(address), - }; - } - } - - // Verify potential contracts created during the transaction execution - // This will fail in ZKsync context due to `init_code` usage. - for AdditionalContract { address, init_code, .. } in &tx.additional_contracts { - match verify.get_verify_args(*address, 0, init_code.as_ref(), &self.libraries) { - Some(verify) => future_verifications.push(verify.run()), - None => unverifiable_contracts.push(*address), - }; - } - } - - trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", future_verifications.len(), unverifiable_contracts.len()); - - self.check_unverified(unverifiable_contracts, verify); - - let num_verifications = future_verifications.len(); - let mut num_of_successful_verifications = 0; - println!("##\nStart verification for ({num_verifications}) contracts"); - for verification in future_verifications { - match verification.await { - Ok(_) => { - num_of_successful_verifications += 1; - } - Err(err) => eprintln!("Error during verification: {err:#}"), - } - } - - if num_of_successful_verifications < num_verifications { - return Err(eyre!("Not all ({num_of_successful_verifications} / {num_verifications}) contracts were verified!")) - } - - println!("All ({num_verifications}) contracts were verified!"); - } - - Ok(()) - } - - /// Let the user know if there are any contracts which can not be verified. Also, present some - /// hints on potential causes. - fn check_unverified(&self, unverifiable_contracts: Vec
, verify: VerifyBundle) { - if !unverifiable_contracts.is_empty() { - println!( - "\n{}", - format!( - "We haven't found any matching bytecode for the following contracts: {:?}.\n\n{}", - unverifiable_contracts, - "This may occur when resuming a verification, but the underlying source code or compiler version has changed." - ) - .yellow() - .bold(), - ); - - if let Some(commit) = &self.commit { - let current_commit = verify - .project_paths - .root - .map(|root| get_commit_hash(&root).unwrap_or_default()) - .unwrap_or_default(); - - if ¤t_commit != commit { - println!("\tScript was broadcasted on commit `{commit}`, but we are at `{current_commit}`."); - } - } - } - } - - /// Returns the first RPC URL of this sequence. - pub fn rpc_url(&self) -> &str { - self.transactions.front().expect("empty sequence").rpc.as_str() - } - - /// Returns the list of the transactions without the metadata. - pub fn transactions(&self) -> impl Iterator { - self.transactions.iter().map(|tx| tx.tx()) - } - - pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) { - self.transactions - .iter_mut() - .enumerate() - .for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc)); - } -} - -/// Converts the `sig` argument into the corresponding file path. -/// -/// This accepts either the signature of the function or the raw calldata - -pub fn sig_to_file_name(sig: &str) -> String { - if let Some((name, _)) = sig.split_once('(') { - // strip until call argument parenthesis - return name.to_string() - } - // assume calldata if `sig` is hex - if let Ok(calldata) = hex::decode(sig) { - // in which case we return the function signature - return hex::encode(&calldata[..SELECTOR_LEN]) - } - - // return sig as is - sig.to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_convert_sig() { - assert_eq!(sig_to_file_name("run()").as_str(), "run"); - assert_eq!( - sig_to_file_name( - "522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266" - ) - .as_str(), - "522bb704" - ); - } -} diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 9e48e819c..cdeac8572 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -1,9 +1,6 @@ use super::{ - multi_sequence::MultiChainSequence, - providers::ProvidersManager, - runner::ScriptRunner, - sequence::{ScriptSequence, ScriptSequenceKind}, - transaction::TransactionWithMetadata, + multi_sequence::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner, + sequence::ScriptSequenceKind, transaction::ScriptTransactionBuilder, }; use crate::{ broadcast::{estimate_gas, BundledState}, @@ -13,17 +10,18 @@ use crate::{ ScriptArgs, ScriptConfig, ScriptResult, }; use alloy_network::TransactionBuilder; -use alloy_primitives::{utils::format_units, Address, Bytes, TxKind, U256}; +use alloy_primitives::{map::HashMap, utils::format_units, Address, Bytes, TxKind, U256}; use dialoguer::Confirm; use eyre::{Context, Result}; -use foundry_cheatcodes::ScriptWallets; +use forge_script_sequence::{ScriptSequence, TransactionWithMetadata}; +use foundry_cheatcodes::Wallets; use foundry_cli::utils::{has_different_gas_calc, now}; use foundry_common::{get_contract_name, shell, ContractData}; use foundry_evm::traces::{decode_trace_arena, render_trace_arena}; use futures::future::{join_all, try_join_all}; use parking_lot::RwLock; use std::{ - collections::{BTreeMap, HashMap, VecDeque}, + collections::{BTreeMap, VecDeque}, sync::Arc, }; use yansi::Paint; @@ -36,7 +34,7 @@ use yansi::Paint; pub struct PreSimulationState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_data: ExecutionData, pub execution_result: ScriptResult, @@ -60,13 +58,19 @@ impl PreSimulationState { .into_iter() .map(|tx| { let rpc = tx.rpc.expect("missing broadcastable tx rpc url"); - TransactionWithMetadata::new( - tx.transaction, - rpc, - &address_to_abi, - &self.execution_artifacts.decoder, - tx.zk_tx, - ) + 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 to = tx.transaction.to(); + + let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc, tx.zk_tx); + + if let Some(TxKind::Call(_)) = to { + builder.set_call(&address_to_abi, &self.execution_artifacts.decoder)?; + } else { + builder.set_create(false, sender.create(nonce), &address_to_abi)?; + } + + Ok(builder.build()) }) .collect::>>()?; @@ -109,10 +113,11 @@ impl PreSimulationState { // Executes all transactions from the different forks concurrently. let futs = transactions .into_iter() - .map(|transaction| async { + .map(|mut transaction| async { let mut runner = runners.get(&transaction.rpc).expect("invalid rpc url").write(); - let tx = &transaction.transaction; + let zk_metadata = transaction.zk.clone(); + let tx = transaction.tx_mut(); let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner .simulate( @@ -121,7 +126,7 @@ impl PreSimulationState { to, tx.input().map(Bytes::copy_from_slice), tx.value(), - (self.script_config.config.zksync.run_in_zk_mode(), transaction.zk.clone()), + (self.script_config.config.zksync.run_in_zk_mode(), zk_metadata), ) .wrap_err("Internal EVM error during simulation")?; @@ -140,8 +145,9 @@ impl PreSimulationState { false }; - let transaction = - transaction.with_execution_result(&result, self.args.gas_estimate_multiplier); + let transaction = ScriptTransactionBuilder::from(transaction) + .with_execution_result(&result, self.args.gas_estimate_multiplier) + .build(); eyre::Ok((Some(transaction), is_noop_tx, result.traces)) }) @@ -236,7 +242,7 @@ impl PreSimulationState { pub struct FilledTransactionsState { pub args: ScriptArgs, pub script_config: ScriptConfig, - pub script_wallets: ScriptWallets, + pub script_wallets: Wallets, pub build_data: LinkedBuildData, pub execution_artifacts: ExecutionArtifacts, pub transactions: VecDeque, @@ -255,7 +261,7 @@ impl FilledTransactionsState { eyre::bail!("Multi-chain deployment is not supported with libraries."); } - let mut total_gas_per_rpc: HashMap = HashMap::new(); + let mut total_gas_per_rpc: HashMap = HashMap::default(); // Batches sequence of transactions from different rpcs. let mut new_sequence = VecDeque::new(); @@ -267,10 +273,10 @@ impl FilledTransactionsState { let mut txes_iter = self.transactions.clone().into_iter().peekable(); while let Some(mut tx) = txes_iter.next() { - let tx_rpc = tx.rpc.clone(); + let tx_rpc = tx.rpc.to_owned(); let provider_info = manager.get_or_init_provider(&tx.rpc, self.args.legacy).await?; - if let Some(tx) = tx.transaction.as_unsigned_mut() { + if let Some(tx) = tx.tx_mut().as_unsigned_mut() { // Handles chain specific requirements for unsigned transactions. tx.set_chain_id(provider_info.chain); } @@ -420,7 +426,7 @@ impl FilledTransactionsState { }) .collect(); - Ok(ScriptSequence { + let sequence = ScriptSequence { transactions, returns: self.execution_artifacts.returns.clone(), receipts: vec![], @@ -430,6 +436,7 @@ impl FilledTransactionsState { libraries, chain, commit, - }) + }; + Ok(sequence) } } diff --git a/crates/script/src/transaction.rs b/crates/script/src/transaction.rs index f87adcd61..3e14e24ef 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -1,95 +1,47 @@ use super::ScriptResult; use alloy_dyn_abi::JsonAbiExt; -use alloy_primitives::{hex, Address, Bytes, TxKind, B256}; -use eyre::{Result, WrapErr}; +use alloy_primitives::{hex, Address, TxKind, B256}; +use eyre::Result; +use forge_script_sequence::TransactionWithMetadata; use foundry_common::{fmt::format_token_raw, ContractData, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, traces::CallTraceDecoder}; use foundry_zksync_core::ZkTransactionMetadata; use itertools::Itertools; use revm_inspectors::tracing::types::CallKind; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AdditionalContract { - #[serde(rename = "transactionType")] - pub opcode: CallKind, - pub address: Address, - pub init_code: Bytes, +#[derive(Debug)] +pub struct ScriptTransactionBuilder { + transaction: TransactionWithMetadata, } -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionWithMetadata { - pub hash: Option, - #[serde(rename = "transactionType")] - pub opcode: CallKind, - #[serde(default = "default_string")] - pub contract_name: Option, - #[serde(default = "default_address")] - pub contract_address: Option
, - #[serde(default = "default_string")] - pub function: Option, - #[serde(default = "default_vec_of_strings")] - pub arguments: Option>, - #[serde(skip)] - pub rpc: String, - pub transaction: TransactionMaybeSigned, - pub additional_contracts: Vec, - pub is_fixed_gas_limit: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub zk: Option, -} - -fn default_string() -> Option { - Some(String::new()) -} - -fn default_address() -> Option
{ - Some(Address::ZERO) -} - -fn default_vec_of_strings() -> Option> { - Some(vec![]) -} - -impl TransactionWithMetadata { - pub fn from_tx_request(transaction: TransactionMaybeSigned) -> Self { - Self { - transaction, - hash: Default::default(), - opcode: Default::default(), - contract_name: Default::default(), - contract_address: Default::default(), - function: Default::default(), - arguments: Default::default(), - is_fixed_gas_limit: Default::default(), - additional_contracts: Default::default(), - rpc: Default::default(), - zk: Default::default(), - } - } - - #[allow(clippy::too_many_arguments)] +impl ScriptTransactionBuilder { pub fn new( transaction: TransactionMaybeSigned, rpc: String, - local_contracts: &BTreeMap, - decoder: &CallTraceDecoder, zk: Option, - ) -> Result { - let mut metadata = Self::from_tx_request(transaction); - metadata.rpc = rpc; + ) -> Self { + let mut transaction = TransactionWithMetadata::from_tx_request(transaction); + transaction.rpc = rpc; // If tx.gas is already set that means it was specified in script - metadata.is_fixed_gas_limit = metadata.tx().gas().is_some(); - metadata.zk = zk; + transaction.is_fixed_gas_limit = transaction.tx().gas().is_some(); + transaction.zk = zk; + + Self { transaction } + } - if let Some(TxKind::Call(to)) = metadata.transaction.to() { + /// Populate the transaction as CALL tx + pub fn set_call( + &mut self, + local_contracts: &BTreeMap, + decoder: &CallTraceDecoder, + ) -> Result<()> { + if let Some(TxKind::Call(to)) = self.transaction.transaction.to() { if to == DEFAULT_CREATE2_DEPLOYER { - if let Some(input) = metadata.transaction.input() { + if let Some(input) = self.transaction.transaction.input() { let (salt, init_code) = input.split_at(32); - metadata.set_create( + + self.set_create( true, DEFAULT_CREATE2_DEPLOYER .create2_from_code(B256::from_slice(salt), init_code), @@ -97,65 +49,68 @@ impl TransactionWithMetadata { )?; } } else { - metadata - .set_call(to, local_contracts, decoder) - .wrap_err("Could not decode transaction type.")?; - } - } else { - let sender = - metadata.transaction.from().expect("all transactions should have a sender"); - let nonce = metadata.transaction.nonce().expect("all transactions should have a nonce"); - metadata.set_create(false, sender.create(nonce), local_contracts)?; - } + self.transaction.opcode = CallKind::Call; + self.transaction.contract_address = Some(to); - Ok(metadata) - } + let Some(data) = self.transaction.transaction.input() else { return Ok(()) }; - /// Populates additional data from the transaction execution result. - pub fn with_execution_result( - mut self, - result: &ScriptResult, - gas_estimate_multiplier: u64, - ) -> Self { - let mut created_contracts = result.get_created_contracts(); - - // Add the additional contracts created in this transaction, so we can verify them later. - created_contracts.retain(|contract| { - // Filter out the contract that was created by the transaction itself. - self.contract_address.map_or(true, |addr| addr != contract.address) - }); + if data.len() < SELECTOR_LEN { + return Ok(()); + } - if !self.is_fixed_gas_limit { - if let Some(unsigned) = self.transaction.as_unsigned_mut() { - // We inflate the gas used by the user specified percentage - unsigned.gas = Some((result.gas_used * gas_estimate_multiplier / 100) as u128); + let (selector, data) = data.split_at(SELECTOR_LEN); + + let function = if let Some(info) = local_contracts.get(&to) { + // This CALL is made to a local contract. + self.transaction.contract_name = Some(info.name.clone()); + info.abi.functions().find(|function| function.selector() == selector) + } else { + // This CALL is made to an external contract; try to decode it from the given + // decoder. + decoder.functions.get(selector).and_then(|v| v.first()) + }; + + if let Some(function) = function { + self.transaction.function = Some(function.signature()); + + let values = function.abi_decode_input(data, false).inspect_err(|_| { + error!( + contract=?self.transaction.contract_name, + signature=?function, + data=hex::encode(data), + "Failed to decode function arguments", + ); + })?; + self.transaction.arguments = + Some(values.iter().map(format_token_raw).collect()); + } } } - self + Ok(()) } /// Populate the transaction as CREATE tx /// /// If this is a CREATE2 transaction this attempt to decode the arguments from the CREATE2 /// deployer's function - fn set_create( + pub fn set_create( &mut self, is_create2: bool, address: Address, contracts: &BTreeMap, ) -> Result<()> { if is_create2 { - self.opcode = CallKind::Create2; + self.transaction.opcode = CallKind::Create2; } else { - self.opcode = CallKind::Create; + self.transaction.opcode = CallKind::Create; } let info = contracts.get(&address); - self.contract_name = info.map(|info| info.name.clone()); - self.contract_address = Some(address); + self.transaction.contract_name = info.map(|info| info.name.clone()); + self.transaction.contract_address = Some(address); - let Some(data) = self.transaction.input() else { return Ok(()) }; + let Some(data) = self.transaction.transaction.input() else { return Ok(()) }; let Some(info) = info else { return Ok(()) }; let Some(bytecode) = info.bytecode() else { return Ok(()) }; @@ -178,70 +133,51 @@ impl TransactionWithMetadata { let Some(constructor) = info.abi.constructor() else { return Ok(()) }; let values = constructor.abi_decode_input(constructor_args, false).inspect_err(|_| { - error!( - contract=?self.contract_name, - signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")), - is_create2, - constructor_args=%hex::encode(constructor_args), - "Failed to decode constructor arguments", - ); - debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code)); - })?; - self.arguments = Some(values.iter().map(format_token_raw).collect()); + error!( + contract=?self.transaction.contract_name, + signature=%format!("constructor({})", constructor.inputs.iter().map(|p| &p.ty).format(",")), + is_create2, + constructor_args=%hex::encode(constructor_args), + "Failed to decode constructor arguments", + ); + debug!(full_data=%hex::encode(data), bytecode=%hex::encode(creation_code)); + })?; + self.transaction.arguments = Some(values.iter().map(format_token_raw).collect()); Ok(()) } - /// Populate the transaction as CALL tx - fn set_call( - &mut self, - target: Address, - local_contracts: &BTreeMap, - decoder: &CallTraceDecoder, - ) -> Result<()> { - self.opcode = CallKind::Call; - self.contract_address = Some(target); - - let Some(data) = self.transaction.input() else { return Ok(()) }; - if data.len() < SELECTOR_LEN { - return Ok(()); - } - let (selector, data) = data.split_at(SELECTOR_LEN); + /// Populates additional data from the transaction execution result. + pub fn with_execution_result( + mut self, + result: &ScriptResult, + gas_estimate_multiplier: u64, + ) -> Self { + let mut created_contracts = result.get_created_contracts(); - let function = if let Some(info) = local_contracts.get(&target) { - // This CALL is made to a local contract. - self.contract_name = Some(info.name.clone()); - info.abi.functions().find(|function| function.selector() == selector) - } else { - // This CALL is made to an external contract; try to decode it from the given decoder. - decoder.functions.get(selector).and_then(|v| v.first()) - }; - if let Some(function) = function { - self.function = Some(function.signature()); + // Add the additional contracts created in this transaction, so we can verify them later. + created_contracts.retain(|contract| { + // Filter out the contract that was created by the transaction itself. + self.transaction.contract_address.map_or(true, |addr| addr != contract.address) + }); - let values = function.abi_decode_input(data, false).inspect_err(|_| { - error!( - contract=?self.contract_name, - signature=?function, - data=hex::encode(data), - "Failed to decode function arguments", - ); - })?; - self.arguments = Some(values.iter().map(format_token_raw).collect()); + if !self.transaction.is_fixed_gas_limit { + if let Some(unsigned) = self.transaction.transaction.as_unsigned_mut() { + // We inflate the gas used by the user specified percentage + unsigned.gas = Some(result.gas_used * gas_estimate_multiplier / 100); + } } - Ok(()) - } - - pub fn tx(&self) -> &TransactionMaybeSigned { - &self.transaction + self } - pub fn tx_mut(&mut self) -> &mut TransactionMaybeSigned { - &mut self.transaction + pub fn build(self) -> TransactionWithMetadata { + self.transaction } +} - pub fn is_create2(&self) -> bool { - self.opcode == CallKind::Create2 +impl From for ScriptTransactionBuilder { + fn from(transaction: TransactionWithMetadata) -> Self { + Self { transaction } } } diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index 1e339868b..7d1f74bc3 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -1,13 +1,19 @@ -use crate::{build::LinkedBuildData, sequence::ScriptSequenceKind, ScriptArgs, ScriptConfig}; +use crate::{ + build::LinkedBuildData, + sequence::{get_commit_hash, ScriptSequenceKind}, + ScriptArgs, ScriptConfig, +}; use alloy_primitives::{hex, Address}; -use eyre::Result; -use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; +use eyre::{eyre, Result}; +use forge_script_sequence::{AdditionalContract, ScriptSequence}; +use forge_verify::{provider::VerificationProviderType, RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::opts::{EtherscanOpts, ProjectPathsArgs}; use foundry_common::ContractsByArtifact; use foundry_compilers::{info::ContractInfo, Project}; use foundry_config::{Chain, Config}; use foundry_zksync_compiler::ZKSYNC_ARTIFACTS_DIR; use semver::Version; +use yansi::Paint; /// State after we have broadcasted the script. /// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all @@ -32,7 +38,7 @@ impl BroadcastedState { ); for sequence in sequence.sequences_mut() { - sequence.verify_contracts(&script_config.config, verify.clone()).await?; + verify_contracts(sequence, &script_config.config, verify.clone()).await?; } Ok(()) @@ -93,7 +99,7 @@ impl VerifyBundle { /// Configures the chain and sets the etherscan key, if available pub fn set_chain(&mut self, config: &Config, chain: Chain) { - // If dealing with multiple chains, we need to be able to change inbetween the config + // If dealing with multiple chains, we need to be able to change in between the config // chain_id. self.etherscan.key = config.get_etherscan_api_key(Some(chain)); self.etherscan.chain = Some(chain); @@ -253,3 +259,107 @@ impl VerifyBundle { None } } + +/// Given the broadcast log, it matches transactions with receipts, and tries to verify any +/// created contract on etherscan. +async fn verify_contracts( + sequence: &mut ScriptSequence, + config: &Config, + mut verify: VerifyBundle, +) -> Result<()> { + trace!(target: "script", "verifying {} contracts [{}]", verify.known_contracts.len(), sequence.chain); + + verify.set_chain(config, sequence.chain.into()); + + if verify.etherscan.has_key() || verify.verifier.verifier != VerificationProviderType::Etherscan + { + trace!(target: "script", "prepare future verifications"); + + let mut future_verifications = Vec::with_capacity(sequence.receipts.len()); + let mut unverifiable_contracts = vec![]; + + // Make sure the receipts have the right order first. + sequence.sort_receipts(); + + for (receipt, tx) in sequence.receipts.iter_mut().zip(sequence.transactions.iter()) { + // create2 hash offset + let mut offset = 0; + + if tx.is_create2() { + receipt.contract_address = tx.contract_address; + offset = 32; + } + + // Verify contract created directly from the transaction + if let (Some(address), Some(data)) = (receipt.contract_address, tx.tx().input()) { + match verify.get_verify_args(address, offset, data, &sequence.libraries) { + Some(verify) => future_verifications.push(verify.run()), + None => unverifiable_contracts.push(address), + }; + } + + // Verify potential contracts created during the transaction execution + for AdditionalContract { address, init_code, .. } in &tx.additional_contracts { + match verify.get_verify_args(*address, 0, init_code.as_ref(), &sequence.libraries) { + Some(verify) => future_verifications.push(verify.run()), + None => unverifiable_contracts.push(*address), + }; + } + } + + trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", future_verifications.len(), unverifiable_contracts.len()); + + check_unverified(sequence, unverifiable_contracts, verify); + + let num_verifications = future_verifications.len(); + let mut num_of_successful_verifications = 0; + println!("##\nStart verification for ({num_verifications}) contracts"); + for verification in future_verifications { + match verification.await { + Ok(_) => { + num_of_successful_verifications += 1; + } + Err(err) => eprintln!("Error during verification: {err:#}"), + } + } + + if num_of_successful_verifications < num_verifications { + return Err(eyre!("Not all ({num_of_successful_verifications} / {num_verifications}) contracts were verified!")) + } + + println!("All ({num_verifications}) contracts were verified!"); + } + + Ok(()) +} + +fn check_unverified( + sequence: &ScriptSequence, + unverifiable_contracts: Vec
, + verify: VerifyBundle, +) { + if !unverifiable_contracts.is_empty() { + println!( + "\n{}", + format!( + "We haven't found any matching bytecode for the following contracts: {:?}.\n\n{}", + unverifiable_contracts, + "This may occur when resuming a verification, but the underlying source code or compiler version has changed." + ) + .yellow() + .bold(), + ); + + if let Some(commit) = &sequence.commit { + let current_commit = verify + .project_paths + .root + .map(|root| get_commit_hash(&root).unwrap_or_default()) + .unwrap_or_default(); + + if ¤t_commit != commit { + println!("\tScript was broadcasted on commit `{commit}`, but we are at `{current_commit}`."); + } + } + } +} diff --git a/crates/sol-macro-gen/src/lib.rs b/crates/sol-macro-gen/src/lib.rs index 0202827f2..9984c6cce 100644 --- a/crates/sol-macro-gen/src/lib.rs +++ b/crates/sol-macro-gen/src/lib.rs @@ -1,4 +1,4 @@ -//! This crate constains the logic for Rust bindings generating from Solidity contracts +//! This crate contains the logic for Rust bindings generating from Solidity contracts pub mod sol_macro_gen; diff --git a/crates/test-utils/src/rpc.rs b/crates/test-utils/src/rpc.rs index 305c8a1c8..a9ff6a7e6 100644 --- a/crates/test-utils/src/rpc.rs +++ b/crates/test-utils/src/rpc.rs @@ -130,6 +130,10 @@ pub fn next_mainnet_etherscan_api_key() -> String { fn next_url(is_ws: bool, chain: NamedChain) -> String { use NamedChain::*; + if matches!(chain, NamedChain::Base) { + return "https://mainnet.base.org".to_string(); + } + let idx = next() % num_keys(); let is_infura = idx < INFURA_KEYS.len(); @@ -173,6 +177,7 @@ fn next_url(is_ws: bool, chain: NamedChain) -> String { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; use alloy_primitives::address; diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index 70db0a407..11757978a 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -49,8 +49,9 @@ static NEXT_ID: AtomicUsize = AtomicUsize::new(0); /// The default Solc version used when compiling tests. pub const SOLC_VERSION: &str = "0.8.27"; -/// Another Solc version used when compiling tests. Necessary to avoid downloading multiple -/// versions. +/// Another Solc version used when compiling tests. +/// +/// Necessary to avoid downloading multiple versions. pub const OTHER_SOLC_VERSION: &str = "0.8.26"; /// External test builder diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index e08cf3a5a..661eb5c8d 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -253,9 +253,9 @@ impl VerifyBytecodeArgs { if let Some(ref block) = genesis_block { configure_env_block(&mut env, block); - gen_tx.max_fee_per_gas = Some(block.header.base_fee_per_gas.unwrap_or_default()); + gen_tx.max_fee_per_gas = block.header.base_fee_per_gas.map(|g| g as u128); gen_tx.gas = block.header.gas_limit; - gen_tx.gas_price = Some(block.header.base_fee_per_gas.unwrap_or_default()); + gen_tx.gas_price = block.header.base_fee_per_gas.map(|g| g as u128); } configure_tx_env(&mut env, &gen_tx); @@ -468,7 +468,7 @@ impl VerifyBytecodeArgs { &transaction, )?; - // State commited using deploy_with_env, now get the runtime bytecode from the db. + // State committed using deploy_with_env, now get the runtime bytecode from the db. let (fork_runtime_code, onchain_runtime_code) = crate::utils::get_runtime_codes( &mut executor, &provider, diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 6ac5d6a06..2f29a6c67 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -524,6 +524,7 @@ async fn ensure_solc_build_metadata(version: Version) -> Result { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use super::*; use clap::Parser; diff --git a/crates/verify/src/retry.rs b/crates/verify/src/retry.rs index 528fd7497..6067d9d85 100644 --- a/crates/verify/src/retry.rs +++ b/crates/verify/src/retry.rs @@ -20,10 +20,10 @@ pub struct RetryArgs { )] pub retries: u32, - /// Optional delay to apply inbetween verification attempts, in seconds. + /// Optional delay to apply in between verification attempts, in seconds. #[arg( long, - value_parser = RangedU64ValueParser::::new().range(0..=30), + value_parser = RangedU64ValueParser::::new().range(0..=180), default_value = "5", )] pub delay: u32, diff --git a/crates/verify/src/sourcify.rs b/crates/verify/src/sourcify.rs index 1afe53de7..d798ed03c 100644 --- a/crates/verify/src/sourcify.rs +++ b/crates/verify/src/sourcify.rs @@ -3,13 +3,14 @@ use crate::{ verify::{VerifyArgs, VerifyCheckArgs}, zk_provider::CompilerVerificationContext, }; +use alloy_primitives::map::HashMap; use async_trait::async_trait; use eyre::Result; use foundry_common::{fs, retry::Retry}; use futures::FutureExt; use reqwest::Url; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, str::FromStr}; +use std::str::FromStr; pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/"; @@ -117,7 +118,7 @@ impl SourcifyVerificationProvider { let metadata = context.get_target_metadata()?; let imports = context.get_target_imports()?; - let mut files = HashMap::with_capacity(2 + imports.len()); + let mut files = HashMap::with_capacity_and_hasher(2 + imports.len(), Default::default()); let metadata = serde_json::to_string_pretty(&metadata)?; files.insert("metadata.json".to_string(), metadata); diff --git a/crates/verify/src/zk_provider.rs b/crates/verify/src/zk_provider.rs index 674c231c3..5fc1bb3f0 100644 --- a/crates/verify/src/zk_provider.rs +++ b/crates/verify/src/zk_provider.rs @@ -152,7 +152,7 @@ impl CompilerVerificationContext { Self::Solc(c) => &c.compiler_version, // TODO: will refer to the solc version here. Analyze if we can remove // this ambiguity somehow (e.g: by having sepparate paths for solc/zksolc - // and remove this method alltogether) + // and remove this method altogether) Self::ZkSolc(c) => &c.compiler_version.solc, } } diff --git a/crates/wallets/src/multi_wallet.rs b/crates/wallets/src/multi_wallet.rs index 459074aaa..c593e67e3 100644 --- a/crates/wallets/src/multi_wallet.rs +++ b/crates/wallets/src/multi_wallet.rs @@ -2,14 +2,14 @@ use crate::{ utils, wallet_signer::{PendingSigner, WalletSigner}, }; -use alloy_primitives::Address; +use alloy_primitives::{map::AddressHashMap, Address}; use alloy_signer::Signer; use clap::Parser; use derive_builder::Builder; use eyre::Result; use foundry_config::Config; use serde::Serialize; -use std::{collections::HashMap, iter::repeat, path::PathBuf}; +use std::{iter::repeat, path::PathBuf}; /// Container for multiple wallets. #[derive(Debug, Default)] @@ -18,7 +18,7 @@ pub struct MultiWallet { /// Those are lazily unlocked on the first access of the signers. pending_signers: Vec, /// Contains unlocked signers. - signers: HashMap, + signers: AddressHashMap, } impl MultiWallet { @@ -35,12 +35,12 @@ impl MultiWallet { Ok(()) } - pub fn signers(&mut self) -> Result<&HashMap> { + pub fn signers(&mut self) -> Result<&AddressHashMap> { self.maybe_unlock_pending()?; Ok(&self.signers) } - pub fn into_signers(mut self) -> Result> { + pub fn into_signers(mut self) -> Result> { self.maybe_unlock_pending()?; Ok(self.signers) } @@ -162,7 +162,7 @@ pub struct MultiWalletOpts { )] pub mnemonic_indexes: Option>, - /// Use the keystore in the given folder or file. + /// Use the keystore by its filename in the given folder. #[arg( long = "keystore", visible_alias = "keystores", @@ -173,7 +173,7 @@ pub struct MultiWalletOpts { #[builder(default = "None")] pub keystore_paths: Option>, - /// Use a keystore from the default keystores folder (~/.foundry/keystores) by its filename + /// Use a keystore from the default keystores folder (~/.foundry/keystores) by its filename. #[arg( long = "account", visible_alias = "accounts", diff --git a/crates/wallets/src/wallet.rs b/crates/wallets/src/wallet.rs index 14a9f7422..01a232895 100644 --- a/crates/wallets/src/wallet.rs +++ b/crates/wallets/src/wallet.rs @@ -148,6 +148,7 @@ impl From for WalletOpts { } #[cfg(test)] +#[allow(clippy::needless_return)] mod tests { use alloy_signer::Signer; use std::{path::Path, str::FromStr}; diff --git a/crates/zksync/compiler/src/libraries.rs b/crates/zksync/compiler/src/libraries.rs index 55e1047cc..b3db8823c 100644 --- a/crates/zksync/compiler/src/libraries.rs +++ b/crates/zksync/compiler/src/libraries.rs @@ -1,4 +1,4 @@ -//! Handles resolution nd storage of missing libraries emitted by zksolc +//! Handles resolution and storage of missing libraries emitted by zksolc use std::{ fs, @@ -110,7 +110,7 @@ pub fn resolve_libraries( } // remove this batch from each library's missing_library if listed as dependency - // this potentailly allows more libraries to be included in the next batch + // this potentially allows more libraries to be included in the next batch for lib in &mut missing_libraries { lib.missing_libraries.retain(|maybe_missing_lib| { let mut split = maybe_missing_lib.split(':'); diff --git a/crates/zksync/compiler/src/zksolc/mod.rs b/crates/zksync/compiler/src/zksolc/mod.rs index 52f0c21be..974b881ec 100644 --- a/crates/zksync/compiler/src/zksolc/mod.rs +++ b/crates/zksync/compiler/src/zksolc/mod.rs @@ -259,7 +259,7 @@ impl DualCompiledContracts { debug!( name = contract.name, deps = contract.zk_factory_deps.len(), - "new factory depdendency" + "new factory dependency" ); for nested_dep in &contract.zk_factory_deps { diff --git a/crates/zksync/core/src/convert/eip712.rs b/crates/zksync/core/src/convert/eip712.rs index 79dfebc9b..8a95f18e3 100644 --- a/crates/zksync/core/src/convert/eip712.rs +++ b/crates/zksync/core/src/convert/eip712.rs @@ -67,8 +67,8 @@ impl Transaction for Eip712SignableTransaction { self.0.nonce.as_u64() } - fn gas_limit(&self) -> u128 { - self.0.gas_limit.as_u128() + fn gas_limit(&self) -> u64 { + self.0.gas_limit.as_u64() } fn gas_price(&self) -> Option { diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index 1c64a4a1d..e2f63be4d 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -178,7 +178,7 @@ pub struct EstimatedGas { /// Estimated gas price. pub price: u128, /// Estimated gas limit. - pub limit: u128, + pub limit: u64, } /// Estimates the gas parameters for the provided transaction. @@ -222,7 +222,7 @@ pub async fn estimate_gas, T: Transport + Clone>( .await .map_err(|err| eyre!("failed rpc call for estimating fee: {:?}", err))?; - Ok(EstimatedGas { price: gas_price, limit: fee.gas_limit.low_u128() }) + Ok(EstimatedGas { price: gas_price, limit: fee.gas_limit.low_u64() }) } /// Returns true if the provided address is a reserved zkSync system address diff --git a/crates/zksync/core/src/utils.rs b/crates/zksync/core/src/utils.rs index 01e5272ec..85945d474 100644 --- a/crates/zksync/core/src/utils.rs +++ b/crates/zksync/core/src/utils.rs @@ -111,8 +111,8 @@ pub fn fix_l2_gas_price(gas_price: U256) -> U256 { U256::max(gas_price, U256::from(260_000_000)) } -/// Limits the gas_limit propotional to a user's available balance given the gas_price. -/// Additionally, fixes the gas limit to be maxmium of 2^31, which is below the VM gas limit of +/// Limits the gas_limit proportional to a user's available balance given the gas_price. +/// Additionally, fixes the gas limit to be maximum of 2^31, which is below the VM gas limit of /// 2^32. This is required so the bootloader does not throw an error for not having enough balance /// to pay for gas. /// diff --git a/crates/zksync/core/src/vm/db.rs b/crates/zksync/core/src/vm/db.rs index 301167d77..3a2f7371c 100644 --- a/crates/zksync/core/src/vm/db.rs +++ b/crates/zksync/core/src/vm/db.rs @@ -3,10 +3,10 @@ /// This way, we can run transaction on top of the chain that is persisted /// in the Database object. /// This code doesn't do any mutatios to Database: after each transaction run, the Revm -/// is usually collecing all the diffs - and applies them to database itself. -use std::{collections::HashMap, fmt::Debug}; +/// is usually collecting all the diffs - and applies them to database itself. +use std::{collections::HashMap as sHashMap, fmt::Debug}; -use alloy_primitives::{Address, U256 as rU256}; +use alloy_primitives::{map::HashMap, Address, U256 as rU256}; use foundry_cheatcodes_common::record::RecordAccess; use revm::{primitives::Account, Database, EvmContext, InnerEvmContext}; use zksync_basic_types::{L2ChainId, H160, H256, U256}; @@ -29,7 +29,7 @@ pub struct ZKVMData<'a, DB: Database> { // pub journaled_state: &'a mut JournaledState, ecx: &'a mut InnerEvmContext, pub factory_deps: HashMap>, - pub override_keys: HashMap, + pub override_keys: sHashMap, pub accesses: Option<&'a mut RecordAccess>, } diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index e7c9dd63a..f58f92d87 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -1,4 +1,4 @@ -use alloy_primitives::hex; +use alloy_primitives::{hex, map::HashMap}; use itertools::Itertools; use revm::{ interpreter::{CallInputs, CallScheme, CallValue}, @@ -12,7 +12,7 @@ use zksync_types::{ U256, }; -use std::{cmp::min, collections::HashMap, fmt::Debug}; +use std::{cmp::min, fmt::Debug}; use crate::{ convert::{ConvertAddress, ConvertH160, ConvertRU256, ConvertU256}, diff --git a/crates/zksync/core/src/vm/tracers/cheatcode.rs b/crates/zksync/core/src/vm/tracers/cheatcode.rs index 0b2c0b20d..b738adabb 100644 --- a/crates/zksync/core/src/vm/tracers/cheatcode.rs +++ b/crates/zksync/core/src/vm/tracers/cheatcode.rs @@ -1,10 +1,10 @@ use std::{ cell::OnceCell, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, VecDeque}, sync::Arc, }; -use alloy_primitives::{hex, Address, Bytes, U256 as rU256}; +use alloy_primitives::{hex, map::HashMap, Address, Bytes, U256 as rU256}; use foundry_cheatcodes_common::{ expect::ExpectedCallTracker, mock::{MockCallDataContext, MockCallReturnData}, @@ -74,7 +74,7 @@ const SELECTOR_BLOCK_HASH: [u8; 4] = hex!("80b41246"); #[derive(Debug, Default)] pub struct CheatcodeTracerContext<'a> { /// Mocked calls. - pub mocked_calls: HashMap>, + pub mocked_calls: HashMap>>, /// Expected calls recorder. pub expected_calls: Option<&'a mut ExpectedCallTracker>, /// Recorded storage accesses @@ -122,7 +122,7 @@ pub struct CallContext { #[derive(Debug, Default)] pub struct CheatcodeTracer { /// List of mocked calls. - pub mocked_calls: HashMap>, + pub mocked_calls: HashMap>>, /// Tracked for foundry's expected calls. pub expected_calls: ExpectedCallTracker, /// Defines the current call context. @@ -136,7 +136,7 @@ pub struct CheatcodeTracer { impl CheatcodeTracer { /// Create an instance of [CheatcodeTracer]. pub fn new( - mocked_calls: HashMap>, + mocked_calls: HashMap>>, expected_calls: ExpectedCallTracker, result: Arc>, call_context: CallContext, @@ -220,29 +220,39 @@ impl DynTracer> for Cheatcode let call_contract = current.code_address.to_address(); let call_value = U256::from(current.context_u128_value).to_ru256(); - let mocks = self.mocked_calls.get(&call_contract); - if let Some(mocks) = &mocks { + let mut had_mocks = false; + if let Some(mocks) = self.mocked_calls.get_mut(&call_contract) { + had_mocks = true; let ctx = MockCallDataContext { calldata: Bytes::from(call_input.clone()), value: Some(call_value), }; - if let Some(return_data) = mocks.get(&ctx).or_else(|| { - mocks - .iter() + if let Some(return_data_queue) = match mocks.get_mut(&ctx) { + Some(queue) => Some(queue), + None => mocks + .iter_mut() .find(|(mock, _)| { call_input.get(..mock.calldata.len()) == Some(&mock.calldata[..]) && mock.value.map_or(true, |value| value == call_value) }) - .map(|(_, v)| v) - }) { - let return_data = return_data.data.clone().to_vec(); - tracing::info!( - "returning mocked value {:?} for {:?}", - hex::encode(&call_input), - hex::encode(&return_data) - ); - self.farcall_handler.set_immediate_return(return_data); - return; + .map(|(_, v)| v), + } { + if let Some(return_data) = if return_data_queue.len() == 1 { + // If the mocked calls stack has a single element in it, don't empty it + return_data_queue.front().map(|x| x.to_owned()) + } else { + // Else, we pop the front element + return_data_queue.pop_front() + } { + let return_data = return_data.data.clone().to_vec(); + tracing::info!( + "returning mocked value {:?} for {:?}", + hex::encode(&call_input), + hex::encode(&return_data) + ); + self.farcall_handler.set_immediate_return(return_data); + return; + } } } @@ -251,11 +261,8 @@ impl DynTracer> for Cheatcode if self.has_empty_code(storage, call_contract) { // issue a more targeted // error if we already had some mocks there - let had_mocks_message = if mocks.is_some() { - " - please ensure the current calldata is mocked" - } else { - "" - }; + let had_mocks_message = + if had_mocks { " - please ensure the current calldata is mocked" } else { "" }; tracing::error!( target = ?call_contract, diff --git a/crates/zksync/inspectors/src/trace.rs b/crates/zksync/inspectors/src/trace.rs index 15f71d81d..abf9d9eb5 100644 --- a/crates/zksync/inspectors/src/trace.rs +++ b/crates/zksync/inspectors/src/trace.rs @@ -1,5 +1,5 @@ use alloy_primitives::{Address, Bytes, Log, U256}; -use foundry_evm_core::InspectorExt; +use foundry_evm_core::{backend::DatabaseExt, InspectorExt}; use foundry_evm_traces::{ CallTraceArena, GethTraceBuilder, ParityTraceBuilder, TracingInspector, TracingInspectorConfig, }; @@ -186,11 +186,15 @@ where } } -impl InspectorExt for TraceCollector { - fn trace_zksync(&mut self, context: &mut EvmContext, call_traces: Vec) { - fn trace_call_recursive( +impl InspectorExt for TraceCollector { + fn trace_zksync( + &mut self, + context: &mut EvmContext<&mut dyn DatabaseExt>, + call_traces: Vec, + ) { + fn trace_call_recursive( tracer: &mut TracingInspector, - context: &mut EvmContext, + context: &mut EvmContext<&mut dyn DatabaseExt>, call: Call, suppressed_top_call: bool, ) -> u64 { diff --git a/deny.toml b/deny.toml index a1ede500c..303bfec5b 100644 --- a/deny.toml +++ b/deny.toml @@ -63,6 +63,7 @@ allow = [ "Unicode-3.0", "MPL-2.0", "CDDL-1.0", + "Zlib", ] # Allow 1 or more licenses on a per-crate basis, so that particular licenses diff --git a/testdata/README.md b/testdata/README.md index 566da52d1..5af60d647 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -4,9 +4,9 @@ A test suite that tests different aspects of Foundry. ### Structure -- [`core`](core): Tests for fundamental aspects of Foundry -- [`logs`](logs): Tests for Foundry logging capabilities -- [`cheats`](cheats): Tests for Foundry cheatcodes -- [`fuzz`](fuzz): Tests for the Foundry fuzzer -- [`trace`](trace): Tests for the Foundry tracer -- [`fork`](fork): Tests for Foundry forking capabilities +- [`core`](default/core): Tests for fundamental aspects of Foundry +- [`logs`](default/logs): Tests for Foundry logging capabilities +- [`cheats`](default/cheats): Tests for Foundry cheatcodes +- [`fuzz`](default/fuzz): Tests for the Foundry fuzzer +- [`trace`](default/trace): Tests for the Foundry tracer +- [`fork`](default/fork): Tests for Foundry forking capabilities diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index a580b1d47..7c2943a1d 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -6,130 +6,21 @@ pragma solidity >=0.6.2 <0.9.0; pragma experimental ABIEncoderV2; interface Vm { - enum CallerMode { - None, - Broadcast, - RecurrentBroadcast, - Prank, - RecurrentPrank - } - enum AccountAccessKind { - Call, - DelegateCall, - CallCode, - StaticCall, - Create, - SelfDestruct, - Resume, - Balance, - Extcodesize, - Extcodehash, - Extcodecopy - } - enum ForgeContext { - TestGroup, - Test, - Coverage, - Snapshot, - ScriptGroup, - ScriptDryRun, - ScriptBroadcast, - ScriptResume, - Unknown - } - - struct Log { - bytes32[] topics; - bytes data; - address emitter; - } - - struct Rpc { - string key; - string url; - } - - struct EthGetLogs { - address emitter; - bytes32[] topics; - bytes data; - bytes32 blockHash; - uint64 blockNumber; - bytes32 transactionHash; - uint64 transactionIndex; - uint256 logIndex; - bool removed; - } - - struct DirEntry { - string errorMessage; - string path; - uint64 depth; - bool isDir; - bool isSymlink; - } - - struct FsMetadata { - bool isDir; - bool isSymlink; - uint256 length; - bool readOnly; - uint256 modified; - uint256 accessed; - uint256 created; - } - - struct Wallet { - address addr; - uint256 publicKeyX; - uint256 publicKeyY; - uint256 privateKey; - } - - struct FfiResult { - int32 exitCode; - bytes stdout; - bytes stderr; - } - - struct ChainInfo { - uint256 forkId; - uint256 chainId; - } - - struct AccountAccess { - ChainInfo chainInfo; - AccountAccessKind kind; - address account; - address accessor; - bool initialized; - uint256 oldBalance; - uint256 newBalance; - bytes deployedCode; - uint256 value; - bytes data; - bool reverted; - StorageAccess[] storageAccesses; - uint64 depth; - } - - struct StorageAccess { - address account; - bytes32 slot; - bool isWrite; - bytes32 previousValue; - bytes32 newValue; - bool reverted; - } - - struct Gas { - uint64 gasLimit; - uint64 gasTotalUsed; - uint64 gasMemoryUsed; - int64 gasRefunded; - uint64 gasRemaining; - } - + enum CallerMode { None, Broadcast, RecurrentBroadcast, Prank, RecurrentPrank } + enum AccountAccessKind { Call, DelegateCall, CallCode, StaticCall, Create, SelfDestruct, Resume, Balance, Extcodesize, Extcodehash, Extcodecopy } + enum ForgeContext { TestGroup, Test, Coverage, Snapshot, ScriptGroup, ScriptDryRun, ScriptBroadcast, ScriptResume, Unknown } + struct Log { bytes32[] topics; bytes data; address emitter; } + struct Rpc { string key; string url; } + struct EthGetLogs { address emitter; bytes32[] topics; bytes data; bytes32 blockHash; uint64 blockNumber; bytes32 transactionHash; uint64 transactionIndex; uint256 logIndex; bool removed; } + struct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; } + struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; } + struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; } + struct FfiResult { int32 exitCode; bytes stdout; bytes stderr; } + struct ChainInfo { uint256 forkId; uint256 chainId; } + struct AccountAccess { ChainInfo chainInfo; AccountAccessKind kind; address account; address accessor; bool initialized; uint256 oldBalance; uint256 newBalance; bytes deployedCode; uint256 value; bytes data; bool reverted; StorageAccess[] storageAccesses; uint64 depth; } + struct StorageAccess { address account; bytes32 slot; bool isWrite; bytes32 previousValue; bytes32 newValue; bool reverted; } + struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } + struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; @@ -289,14 +180,15 @@ interface Vm { function assumeNoRevert() external pure; function blobBaseFee(uint256 newBlobBaseFee) external; function blobhashes(bytes32[] calldata hashes) external; - function breakpoint(string calldata char) external; - function breakpoint(string calldata char, bool value) external; + function breakpoint(string calldata char) external pure; + function breakpoint(string calldata char, bool value) external pure; function broadcastRawTransaction(bytes calldata data) external; function broadcast() external; function broadcast(address signer) external; function broadcast(uint256 privateKey) external; function chainId(uint256 newChainId) external; function clearMockedCalls() external; + function cloneAccount(address source, address target) external; function closeFile(string calldata path) external; function coinbase(address newCoinbase) external; function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) @@ -305,6 +197,7 @@ interface Vm { returns (address); function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external pure returns (address); function computeCreateAddress(address deployer, uint256 nonce) external pure returns (address); + function contains(string calldata subject, string calldata search) external returns (bool result); function cool(address target) external; function copyFile(string calldata from, string calldata to) external returns (uint64 copied); function copyStorage(address from, address to) external; @@ -321,6 +214,8 @@ interface Vm { function deal(address account, uint256 newBalance) external; function deleteSnapshot(uint256 snapshotId) external returns (bool success); function deleteSnapshots() external; + function deleteStateSnapshot(uint256 snapshotId) external returns (bool success); + function deleteStateSnapshots() external; function deployCode(string calldata artifactPath) external returns (address deployedAddress); function deployCode(string calldata artifactPath, bytes calldata constructorArgs) external @@ -435,6 +330,8 @@ interface Vm { function fee(uint256 newBasefee) external; function ffi(string[] calldata commandInput) external returns (bytes memory result); function fsMetadata(string calldata path) external view returns (FsMetadata memory metadata); + function getArtifactPathByCode(bytes calldata code) external view returns (string memory path); + function getArtifactPathByDeployedCode(bytes calldata deployedCode) external view returns (string memory path); function getBlobBaseFee() external view returns (uint256 blobBaseFee); function getBlobhashes() external view returns (bytes32[] memory hashes); function getBlockNumber() external view returns (uint256 height); @@ -451,6 +348,7 @@ interface Vm { function getNonce(address account) external view returns (uint64 nonce); function getNonce(Wallet calldata wallet) external returns (uint64 nonce); function getRecordedLogs() external returns (Log[] memory logs); + function getWallets() external returns (address[] memory wallets); function indexOf(string calldata input, string calldata key) external pure returns (uint256); function isContext(ForgeContext context) external view returns (bool result); function isDir(string calldata path) external returns (bool result); @@ -472,6 +370,8 @@ interface Vm { external; function mockCall(address callee, bytes calldata data, bytes calldata returnData) external; function mockCall(address callee, uint256 msgValue, bytes calldata data, bytes calldata returnData) external; + function mockCalls(address callee, bytes calldata data, bytes[] calldata returnData) external; + function mockCalls(address callee, uint256 msgValue, bytes calldata data, bytes[] calldata returnData) external; function mockFunction(address callee, address target, bytes calldata data) external; function parseAddress(string calldata stringifiedValue) external pure returns (address parsedValue); function parseBool(string calldata stringifiedValue) external pure returns (bool parsedValue); @@ -532,6 +432,9 @@ interface Vm { function parseTomlKeys(string calldata toml, string calldata key) external pure returns (string[] memory keys); function parseTomlString(string calldata toml, string calldata key) external pure returns (string memory); function parseTomlStringArray(string calldata toml, string calldata key) external pure returns (string[] memory); + function parseTomlTypeArray(string calldata toml, string calldata key, string calldata typeDescription) external pure returns (bytes memory); + function parseTomlType(string calldata toml, string calldata typeDescription) external pure returns (bytes memory); + function parseTomlType(string calldata toml, string calldata key, string calldata typeDescription) external pure returns (bytes memory); function parseTomlUint(string calldata toml, string calldata key) external pure returns (uint256); function parseTomlUintArray(string calldata toml, string calldata key) external pure returns (uint256[] memory); function parseToml(string calldata toml) external pure returns (bytes memory abiEncodedData); @@ -551,8 +454,15 @@ interface Vm { function promptUint(string calldata promptText) external returns (uint256); function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); function randomAddress() external returns (address); + function randomBool() external view returns (bool); + function randomBytes(uint256 len) external view returns (bytes memory); + function randomBytes4() external view returns (bytes4); + function randomBytes8() external view returns (bytes8); + function randomInt() external view returns (int256); + function randomInt(uint256 bits) external view returns (int256); function randomUint() external returns (uint256); function randomUint(uint256 min, uint256 max) external returns (uint256); + function randomUint(uint256 bits) external view returns (uint256); function readCallers() external returns (CallerMode callerMode, address msgSender, address txOrigin); function readDir(string calldata path) external view returns (DirEntry[] memory entries); function readDir(string calldata path, uint64 maxDepth) external view returns (DirEntry[] memory entries); @@ -567,6 +477,8 @@ interface Vm { function record() external; function recordLogs() external; function rememberKey(uint256 privateKey) external returns (address keyAddr); + function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs); + function rememberKeys(string calldata mnemonic, string calldata derivationPath, string calldata language, uint32 count) external returns (address[] memory keyAddrs); function removeDir(string calldata path, bool recursive) external; function removeFile(string calldata path) external; function replace(string calldata input, string calldata from, string calldata to) @@ -579,6 +491,8 @@ interface Vm { function resumeTracing() external view; function revertTo(uint256 snapshotId) external returns (bool success); function revertToAndDelete(uint256 snapshotId) external returns (bool success); + function revertToState(uint256 snapshotId) external returns (bool success); + function revertToStateAndDelete(uint256 snapshotId) external returns (bool success); function revokePersistent(address account) external; function revokePersistent(address[] calldata accounts) external; function roll(uint256 newHeight) external; @@ -668,19 +582,31 @@ interface Vm { function skip(bool skipTest, string calldata reason) external; function sleep(uint256 duration) external; function snapshot() external returns (uint256 snapshotId); + function snapshotGasLastCall(string calldata name) external returns (uint256 gasUsed); + function snapshotGasLastCall(string calldata group, string calldata name) external returns (uint256 gasUsed); + function snapshotState() external returns (uint256 snapshotId); + function snapshotValue(string calldata name, uint256 value) external; + function snapshotValue(string calldata group, string calldata name, uint256 value) external; function split(string calldata input, string calldata delimiter) external pure returns (string[] memory outputs); function startBroadcast() external; function startBroadcast(address signer) external; function startBroadcast(uint256 privateKey) external; + function startDebugTraceRecording() external; function startMappingRecording() external; function startPrank(address msgSender) external; function startPrank(address msgSender, address txOrigin) external; + function startSnapshotGas(string calldata name) external; + function startSnapshotGas(string calldata group, string calldata name) external; function startStateDiffRecording() external; + function stopAndReturnDebugTraceRecording() external returns (DebugStep[] memory step); function stopAndReturnStateDiff() external returns (AccountAccess[] memory accountAccesses); function stopBroadcast() external; function stopExpectSafeMemory() external; function stopMappingRecording() external; function stopPrank() external; + function stopSnapshotGas() external returns (uint256 gasUsed); + function stopSnapshotGas(string calldata name) external returns (uint256 gasUsed); + function stopSnapshotGas(string calldata group, string calldata name) external returns (uint256 gasUsed); function store(address target, bytes32 slot, bytes32 value) external; function toBase64URL(bytes calldata data) external pure returns (string memory); function toBase64URL(string calldata data) external pure returns (string memory); diff --git a/testdata/default/cheats/Addr.t.sol b/testdata/default/cheats/Addr.t.sol index 432c52e69..3f791583d 100644 --- a/testdata/default/cheats/Addr.t.sol +++ b/testdata/default/cheats/Addr.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ArbitraryStorage.t.sol b/testdata/default/cheats/ArbitraryStorage.t.sol index 86910279e..afac01087 100644 --- a/testdata/default/cheats/ArbitraryStorage.t.sol +++ b/testdata/default/cheats/ArbitraryStorage.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -83,10 +83,10 @@ contract AContractArbitraryStorageWithSeedTest is DSTest { function test_arbitrary_storage_with_seed() public { AContract target = new AContract(); vm.setArbitraryStorage(address(target)); - assertEq(target.a(11), 85286582241781868037363115933978803127245343755841464083427462398552335014708); - assertEq(target.b(22), 0x939180Daa938F9e18Ff0E76c112D25107D358B02); - assertEq(target.c(33), -104); - assertEq(target.d(44), 0x6c178fa9c434f142df61a5355cc2b8d07be691b98dabf5b1a924f2bce97a19c7); + assertEq(target.a(11), 112807530564575719000382171275495171195982096112439764207649185248041477080234); + assertEq(target.b(22), 0x9dce87df97C81f2529877E8127b4b8c13E4b2b31); + assertEq(target.c(33), 85); + assertEq(target.d(44), 0x6ceda712fc9d694d72afeea6c44d370b789a18e1a3d640068c11069e421d25f6); } } @@ -104,7 +104,7 @@ contract SymbolicStorageWithSeedTest is DSTest { address addr = 0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8; vm.setArbitraryStorage(addr); bytes32 value = vm.load(addr, bytes32(slot)); - assertEq(uint256(value), 85286582241781868037363115933978803127245343755841464083427462398552335014708); + assertEq(uint256(value), 112807530564575719000382171275495171195982096112439764207649185248041477080234); // Load slot again and make sure we get same value. bytes32 value1 = vm.load(addr, bytes32(slot)); assertEq(uint256(value), uint256(value1)); @@ -115,7 +115,7 @@ contract SymbolicStorageWithSeedTest is DSTest { SymbolicStore myStore = new SymbolicStore(); vm.setArbitraryStorage(address(myStore)); bytes32 value = vm.load(address(myStore), bytes32(uint256(slot))); - assertEq(uint256(value), 85286582241781868037363115933978803127245343755841464083427462398552335014708); + assertEq(uint256(value), 112807530564575719000382171275495171195982096112439764207649185248041477080234); } function testEmptyInitialStorage(uint256 slot) public { diff --git a/testdata/default/cheats/Assert.t.sol b/testdata/default/cheats/Assert.t.sol index 971bb5e27..d6765967c 100644 --- a/testdata/default/cheats/Assert.t.sol +++ b/testdata/default/cheats/Assert.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Assume.t.sol b/testdata/default/cheats/Assume.t.sol index de11d6644..14ed341c9 100644 --- a/testdata/default/cheats/Assume.t.sol +++ b/testdata/default/cheats/Assume.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Bank.t.sol b/testdata/default/cheats/Bank.t.sol index a02fe1667..166fbb16a 100644 --- a/testdata/default/cheats/Bank.t.sol +++ b/testdata/default/cheats/Bank.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Base64.t.sol b/testdata/default/cheats/Base64.t.sol index 0d2249395..fad7bbf4f 100644 --- a/testdata/default/cheats/Base64.t.sol +++ b/testdata/default/cheats/Base64.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Broadcast.t.sol b/testdata/default/cheats/Broadcast.t.sol index 6a099dc6e..bca8cc2ee 100644 --- a/testdata/default/cheats/Broadcast.t.sol +++ b/testdata/default/cheats/Broadcast.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/BroadcastRawTransaction.t.sol b/testdata/default/cheats/BroadcastRawTransaction.t.sol index 43e4ff632..5bd400a9f 100644 --- a/testdata/default/cheats/BroadcastRawTransaction.t.sol +++ b/testdata/default/cheats/BroadcastRawTransaction.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ChainId.t.sol b/testdata/default/cheats/ChainId.t.sol index aa8fa0a13..31a6e5cdf 100644 --- a/testdata/default/cheats/ChainId.t.sol +++ b/testdata/default/cheats/ChainId.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/CloneAccount.t.sol b/testdata/default/cheats/CloneAccount.t.sol new file mode 100644 index 000000000..d584c747c --- /dev/null +++ b/testdata/default/cheats/CloneAccount.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract Source { + uint256 public a; + address public b; + uint256[3] public c; + bool public d; + + constructor() { + a = 100; + b = address(111); + c[0] = 222; + c[1] = 333; + c[2] = 444; + d = true; + } +} + +contract CloneAccountTest is DSTest { + Vm vm = Vm(HEVM_ADDRESS); + + address clone = address(777); + + function setUp() public { + Source src = new Source(); + vm.deal(address(src), 0.123 ether); + vm.cloneAccount(address(src), clone); + } + + function test_clone_account() public { + // Check clone balance. + assertEq(clone.balance, 0.123 ether); + // Check clone storage. + assertEq(Source(clone).a(), 100); + assertEq(Source(clone).b(), address(111)); + assertEq(Source(clone).c(0), 222); + assertEq(Source(clone).c(1), 333); + assertEq(Source(clone).c(2), 444); + assertEq(Source(clone).d(), true); + } +} diff --git a/testdata/default/cheats/Cool.t.sol b/testdata/default/cheats/Cool.t.sol index 82212f1b1..d0750bebf 100644 --- a/testdata/default/cheats/Cool.t.sol +++ b/testdata/default/cheats/Cool.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "lib/ds-test/src/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/CopyStorage.t.sol b/testdata/default/cheats/CopyStorage.t.sol index 895847497..e9195949e 100644 --- a/testdata/default/cheats/CopyStorage.t.sol +++ b/testdata/default/cheats/CopyStorage.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -57,8 +57,8 @@ contract CounterWithSeedTest is DSTest { counter.setA(1000); counter1.setB(address(50)); assertEq(counter.a(), 1000); - assertEq(counter1.a(), 40426841063417815470953489044557166618267862781491517122018165313568904172524); - assertEq(counter.b(), 0x485E9Cc0ef187E54A3AB45b50c3DcE43f2C223B1); + assertEq(counter1.a(), 67350900536747027229585709178274816969402970928486983076982664581925078789474); + assertEq(counter.b(), 0x5A61ACa23C478d83A72425c386Eb5dB083FBd0e4); assertEq(counter1.b(), address(50)); } } diff --git a/testdata/default/cheats/Deal.t.sol b/testdata/default/cheats/Deal.t.sol index ac4776435..a46d9e714 100644 --- a/testdata/default/cheats/Deal.t.sol +++ b/testdata/default/cheats/Deal.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/DeployCode.t.sol b/testdata/default/cheats/DeployCode.t.sol index 330e82651..edf4c78e6 100644 --- a/testdata/default/cheats/DeployCode.t.sol +++ b/testdata/default/cheats/DeployCode.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Derive.t.sol b/testdata/default/cheats/Derive.t.sol index fb1443333..c27456c6e 100644 --- a/testdata/default/cheats/Derive.t.sol +++ b/testdata/default/cheats/Derive.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/EnsNamehash.t.sol b/testdata/default/cheats/EnsNamehash.t.sol index 2d66beea4..965d50500 100644 --- a/testdata/default/cheats/EnsNamehash.t.sol +++ b/testdata/default/cheats/EnsNamehash.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Env.t.sol b/testdata/default/cheats/Env.t.sol index e325df2fa..7edb35dff 100644 --- a/testdata/default/cheats/Env.t.sol +++ b/testdata/default/cheats/Env.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Etch.t.sol b/testdata/default/cheats/Etch.t.sol index fbd0e62b9..33eaaf44e 100644 --- a/testdata/default/cheats/Etch.t.sol +++ b/testdata/default/cheats/Etch.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ExpectCall.t.sol b/testdata/default/cheats/ExpectCall.t.sol index 7d757101a..f2624fd2e 100644 --- a/testdata/default/cheats/ExpectCall.t.sol +++ b/testdata/default/cheats/ExpectCall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ExpectEmit.t.sol b/testdata/default/cheats/ExpectEmit.t.sol index cad184355..b8fe5e458 100644 --- a/testdata/default/cheats/ExpectEmit.t.sol +++ b/testdata/default/cheats/ExpectEmit.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ExpectRevert.t.sol b/testdata/default/cheats/ExpectRevert.t.sol index 60137adfa..18a90bac6 100644 --- a/testdata/default/cheats/ExpectRevert.t.sol +++ b/testdata/default/cheats/ExpectRevert.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Fee.t.sol b/testdata/default/cheats/Fee.t.sol index ad93fed6a..d258eaf13 100644 --- a/testdata/default/cheats/Fee.t.sol +++ b/testdata/default/cheats/Fee.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Ffi.t.sol b/testdata/default/cheats/Ffi.t.sol index 897783d7e..23ac54e6a 100644 --- a/testdata/default/cheats/Ffi.t.sol +++ b/testdata/default/cheats/Ffi.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Fork.t.sol b/testdata/default/cheats/Fork.t.sol index 873fbec13..2f2e627de 100644 --- a/testdata/default/cheats/Fork.t.sol +++ b/testdata/default/cheats/Fork.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Fork2.t.sol b/testdata/default/cheats/Fork2.t.sol index 3e8f68a6c..7b6b42759 100644 --- a/testdata/default/cheats/Fork2.t.sol +++ b/testdata/default/cheats/Fork2.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../logs/console.sol"; diff --git a/testdata/default/cheats/Fs.t.sol b/testdata/default/cheats/Fs.t.sol index cb407641e..b48825259 100644 --- a/testdata/default/cheats/Fs.t.sol +++ b/testdata/default/cheats/Fs.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GasMetering.t.sol b/testdata/default/cheats/GasMetering.t.sol index e439e05be..3cb105d23 100644 --- a/testdata/default/cheats/GasMetering.t.sol +++ b/testdata/default/cheats/GasMetering.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GasSnapshots.t.sol b/testdata/default/cheats/GasSnapshots.t.sol new file mode 100644 index 000000000..98abfa3e4 --- /dev/null +++ b/testdata/default/cheats/GasSnapshots.t.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract GasSnapshotTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + uint256 public slot0; + Flare public flare; + + function setUp() public { + flare = new Flare(); + } + + function testSnapshotGasSectionExternal() public { + vm.startSnapshotGas("testAssertGasExternal"); + flare.run(1); + uint256 gasUsed = vm.stopSnapshotGas(); + + assertGt(gasUsed, 0); + } + + function testSnapshotGasSectionInternal() public { + vm.startSnapshotGas("testAssertGasInternalA"); + slot0 = 1; + vm.stopSnapshotGas(); + + vm.startSnapshotGas("testAssertGasInternalB"); + slot0 = 2; + vm.stopSnapshotGas(); + + vm.startSnapshotGas("testAssertGasInternalC"); + slot0 = 0; + vm.stopSnapshotGas(); + + vm.startSnapshotGas("testAssertGasInternalD"); + slot0 = 1; + vm.stopSnapshotGas(); + + vm.startSnapshotGas("testAssertGasInternalE"); + slot0 = 2; + vm.stopSnapshotGas(); + } + + // Writes to `GasSnapshotTest` group with custom names. + function testSnapshotValueDefaultGroupA() public { + uint256 a = 123; + uint256 b = 456; + uint256 c = 789; + + vm.snapshotValue("a", a); + vm.snapshotValue("b", b); + vm.snapshotValue("c", c); + } + + // Writes to same `GasSnapshotTest` group with custom names. + function testSnapshotValueDefaultGroupB() public { + uint256 d = 123; + uint256 e = 456; + uint256 f = 789; + + vm.snapshotValue("d", d); + vm.snapshotValue("e", e); + vm.snapshotValue("f", f); + } + + // Writes to `CustomGroup` group with custom names. + // Asserts that the order of the values is alphabetical. + function testSnapshotValueCustomGroupA() public { + uint256 o = 123; + uint256 i = 456; + uint256 q = 789; + + vm.snapshotValue("CustomGroup", "q", q); + vm.snapshotValue("CustomGroup", "i", i); + vm.snapshotValue("CustomGroup", "o", o); + } + + // Writes to `CustomGroup` group with custom names. + // Asserts that the order of the values is alphabetical. + function testSnapshotValueCustomGroupB() public { + uint256 x = 123; + uint256 e = 456; + uint256 z = 789; + + vm.snapshotValue("CustomGroup", "z", z); + vm.snapshotValue("CustomGroup", "x", x); + vm.snapshotValue("CustomGroup", "e", e); + } + + // Writes to `GasSnapshotTest` group with `testSnapshotGasDefault` name. + function testSnapshotGasSectionDefaultGroupStop() public { + vm.startSnapshotGas("testSnapshotGasSection"); + + flare.run(256); + + // vm.stopSnapshotGas() will use the last snapshot name. + uint256 gasUsed = vm.stopSnapshotGas(); + assertGt(gasUsed, 0); + } + + // Writes to `GasSnapshotTest` group with `testSnapshotGasCustom` name. + function testSnapshotGasSectionCustomGroupStop() public { + vm.startSnapshotGas("CustomGroup", "testSnapshotGasSection"); + + flare.run(256); + + // vm.stopSnapshotGas() will use the last snapshot name, even with custom group. + uint256 gasUsed = vm.stopSnapshotGas(); + assertGt(gasUsed, 0); + } + + // Writes to `GasSnapshotTest` group with `testSnapshotGasSection` name. + function testSnapshotGasSectionName() public { + vm.startSnapshotGas("testSnapshotGasSectionName"); + + flare.run(256); + + uint256 gasUsed = vm.stopSnapshotGas("testSnapshotGasSectionName"); + assertGt(gasUsed, 0); + } + + // Writes to `CustomGroup` group with `testSnapshotGasSection` name. + function testSnapshotGasSectionGroupName() public { + vm.startSnapshotGas("CustomGroup", "testSnapshotGasSectionGroupName"); + + flare.run(256); + + uint256 gasUsed = vm.stopSnapshotGas("CustomGroup", "testSnapshotGasSectionGroupName"); + assertGt(gasUsed, 0); + } + + // Writes to `GasSnapshotTest` group with `testSnapshotGas` name. + function testSnapshotGasLastCallName() public { + flare.run(1); + + uint256 gasUsed = vm.snapshotGasLastCall("testSnapshotGasLastCallName"); + assertGt(gasUsed, 0); + } + + // Writes to `CustomGroup` group with `testSnapshotGas` name. + function testSnapshotGasLastCallGroupName() public { + flare.run(1); + + uint256 gasUsed = vm.snapshotGasLastCall("CustomGroup", "testSnapshotGasLastCallGroupName"); + assertGt(gasUsed, 0); + } +} + +contract GasComparisonTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + uint256 public slot0; + uint256 public slot1; + + uint256 public cachedGas; + + function testGasComparisonEmpty() public { + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonEmptyA"); + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonEmptyB", b); + + assertEq(a, b); + } + + function testGasComparisonInternalCold() public { + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonInternalColdA"); + slot0 = 1; + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + slot1 = 1; + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonInternalColdB", b); + + vm.assertApproxEqAbs(a, b, 6); + } + + function testGasComparisonInternalWarm() public { + // Warm up the cache. + slot0 = 1; + + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonInternalWarmA"); + slot0 = 2; + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + slot0 = 3; + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonInternalWarmB", b); + + vm.assertApproxEqAbs(a, b, 6); + } + + function testGasComparisonExternal() public { + // Warm up the cache. + TargetB target = new TargetB(); + target.update(1); + + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonExternalA"); + target.update(2); + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + target.update(3); + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonExternalB", b); + + assertEq(a, b); + } + + function testGasComparisonCreate() public { + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonCreateA"); + new TargetC(); + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + new TargetC(); + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonCreateB", b); + + assertEq(a, b); + } + + function testGasComparisonNestedCalls() public { + // Warm up the cache. + TargetA target = new TargetA(); + target.update(1); + + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonNestedCallsA"); + target.update(2); + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + target.update(3); + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonNestedCallsB", b); + + assertEq(a, b); + } + + function testGasComparisonFlare() public { + // Warm up the cache. + Flare flare = new Flare(); + flare.run(1); + + // Start a cheatcode snapshot. + vm.startSnapshotGas("ComparisonGroup", "testGasComparisonFlareA"); + flare.run(256); + uint256 a = vm.stopSnapshotGas(); + + // Start a comparitive Solidity snapshot. + _snapStart(); + flare.run(256); + uint256 b = _snapEnd(); + vm.snapshotValue("ComparisonGroup", "testGasComparisonFlareB", b); + + assertEq(a, b); + } + + // Internal function to start a Solidity snapshot. + function _snapStart() internal { + cachedGas = 1; + cachedGas = gasleft(); + } + + // Internal function to end a Solidity snapshot. + function _snapEnd() internal returns (uint256 gasUsed) { + gasUsed = cachedGas - gasleft() - 138; + cachedGas = 2; + } +} + +contract Flare { + bytes32[] public data; + + function run(uint256 n_) public { + for (uint256 i = 0; i < n_; i++) { + data.push(keccak256(abi.encodePacked(i))); + } + } +} + +contract TargetA { + TargetB public target; + + constructor() { + target = new TargetB(); + } + + function update(uint256 x_) public { + target.update(x_); + } +} + +contract TargetB { + uint256 public x; + + function update(uint256 x_) public { + x = x_; + } +} + +contract TargetC {} diff --git a/testdata/default/cheats/GetArtifactPath.t.sol b/testdata/default/cheats/GetArtifactPath.t.sol new file mode 100644 index 000000000..538c2e6c9 --- /dev/null +++ b/testdata/default/cheats/GetArtifactPath.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity =0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract DummyForGetArtifactPath {} + +contract GetArtifactPathTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testGetArtifactPathByCode() public { + DummyForGetArtifactPath dummy = new DummyForGetArtifactPath(); + bytes memory dummyCreationCode = type(DummyForGetArtifactPath).creationCode; + + string memory root = vm.projectRoot(); + string memory path = vm.getArtifactPathByCode(dummyCreationCode); + + string memory expectedPath = + string.concat(root, "/out/default/GetArtifactPath.t.sol/DummyForGetArtifactPath.json"); + + assertEq(path, expectedPath); + } + + function testGetArtifactPathByDeployedCode() public { + DummyForGetArtifactPath dummy = new DummyForGetArtifactPath(); + bytes memory dummyRuntimeCode = address(dummy).code; + + string memory root = vm.projectRoot(); + string memory path = vm.getArtifactPathByDeployedCode(dummyRuntimeCode); + + string memory expectedPath = + string.concat(root, "/out/default/GetArtifactPath.t.sol/DummyForGetArtifactPath.json"); + + assertEq(path, expectedPath); + } +} diff --git a/testdata/default/cheats/GetBlockTimestamp.t.sol b/testdata/default/cheats/GetBlockTimestamp.t.sol index 383bfa8b0..edeaa0de7 100644 --- a/testdata/default/cheats/GetBlockTimestamp.t.sol +++ b/testdata/default/cheats/GetBlockTimestamp.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GetCode.t.sol b/testdata/default/cheats/GetCode.t.sol index b155a1873..b258b6271 100644 --- a/testdata/default/cheats/GetCode.t.sol +++ b/testdata/default/cheats/GetCode.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity =0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GetDeployedCode.t.sol b/testdata/default/cheats/GetDeployedCode.t.sol index 220e3f3c8..295d2ae8f 100644 --- a/testdata/default/cheats/GetDeployedCode.t.sol +++ b/testdata/default/cheats/GetDeployedCode.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity =0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GetFoundryVersion.t.sol b/testdata/default/cheats/GetFoundryVersion.t.sol index fa8c8e1aa..0ec83a72d 100644 --- a/testdata/default/cheats/GetFoundryVersion.t.sol +++ b/testdata/default/cheats/GetFoundryVersion.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GetLabel.t.sol b/testdata/default/cheats/GetLabel.t.sol index dcbe0812c..c5a5638d3 100644 --- a/testdata/default/cheats/GetLabel.t.sol +++ b/testdata/default/cheats/GetLabel.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/GetNonce.t.sol b/testdata/default/cheats/GetNonce.t.sol index 7eb53f205..d4043a599 100644 --- a/testdata/default/cheats/GetNonce.t.sol +++ b/testdata/default/cheats/GetNonce.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Json.t.sol b/testdata/default/cheats/Json.t.sol index 0604ef907..ff1b62c6e 100644 --- a/testdata/default/cheats/Json.t.sol +++ b/testdata/default/cheats/Json.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Label.t.sol b/testdata/default/cheats/Label.t.sol index d554f637d..4ff5d3860 100644 --- a/testdata/default/cheats/Label.t.sol +++ b/testdata/default/cheats/Label.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/LastCallGas.t.sol b/testdata/default/cheats/LastCallGas.t.sol index 0f5b65e35..bc7ac4263 100644 --- a/testdata/default/cheats/LastCallGas.t.sol +++ b/testdata/default/cheats/LastCallGas.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Load.t.sol b/testdata/default/cheats/Load.t.sol index 0ed1cbbc2..06f4b5bd5 100644 --- a/testdata/default/cheats/Load.t.sol +++ b/testdata/default/cheats/Load.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Mapping.t.sol b/testdata/default/cheats/Mapping.t.sol index 6cd141fa8..82477150a 100644 --- a/testdata/default/cheats/Mapping.t.sol +++ b/testdata/default/cheats/Mapping.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/MemSafety.t.sol b/testdata/default/cheats/MemSafety.t.sol index 096d8ac47..a5c0a5a4f 100644 --- a/testdata/default/cheats/MemSafety.t.sol +++ b/testdata/default/cheats/MemSafety.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/MockCall.t.sol b/testdata/default/cheats/MockCall.t.sol index df7ee89c7..940e5e78c 100644 --- a/testdata/default/cheats/MockCall.t.sol +++ b/testdata/default/cheats/MockCall.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/MockCalls.t.sol b/testdata/default/cheats/MockCalls.t.sol new file mode 100644 index 000000000..2bd4d8bd9 --- /dev/null +++ b/testdata/default/cheats/MockCalls.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract MockCallsTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testMockCallsLastShouldPersist() public { + address mockUser = vm.addr(vm.randomUint()); + address mockErc20 = vm.addr(vm.randomUint()); + bytes memory data = abi.encodeWithSignature("balanceOf(address)", mockUser); + bytes[] memory mocks = new bytes[](2); + mocks[0] = abi.encode(2 ether); + mocks[1] = abi.encode(7.219 ether); + vm.mockCalls(mockErc20, data, mocks); + (, bytes memory ret1) = mockErc20.call(data); + assertEq(abi.decode(ret1, (uint256)), 2 ether); + (, bytes memory ret2) = mockErc20.call(data); + assertEq(abi.decode(ret2, (uint256)), 7.219 ether); + (, bytes memory ret3) = mockErc20.call(data); + assertEq(abi.decode(ret3, (uint256)), 7.219 ether); + } + + function testMockCallsWithValue() public { + address mockUser = vm.addr(vm.randomUint()); + address mockErc20 = vm.addr(vm.randomUint()); + bytes memory data = abi.encodeWithSignature("balanceOf(address)", mockUser); + bytes[] memory mocks = new bytes[](3); + mocks[0] = abi.encode(2 ether); + mocks[1] = abi.encode(1 ether); + mocks[2] = abi.encode(6.423 ether); + vm.mockCalls(mockErc20, 1 ether, data, mocks); + (, bytes memory ret1) = mockErc20.call{value: 1 ether}(data); + assertEq(abi.decode(ret1, (uint256)), 2 ether); + (, bytes memory ret2) = mockErc20.call{value: 1 ether}(data); + assertEq(abi.decode(ret2, (uint256)), 1 ether); + (, bytes memory ret3) = mockErc20.call{value: 1 ether}(data); + assertEq(abi.decode(ret3, (uint256)), 6.423 ether); + } + + function testMockCalls() public { + address mockUser = vm.addr(vm.randomUint()); + address mockErc20 = vm.addr(vm.randomUint()); + bytes memory data = abi.encodeWithSignature("balanceOf(address)", mockUser); + bytes[] memory mocks = new bytes[](3); + mocks[0] = abi.encode(2 ether); + mocks[1] = abi.encode(1 ether); + mocks[2] = abi.encode(6.423 ether); + vm.mockCalls(mockErc20, data, mocks); + (, bytes memory ret1) = mockErc20.call(data); + assertEq(abi.decode(ret1, (uint256)), 2 ether); + (, bytes memory ret2) = mockErc20.call(data); + assertEq(abi.decode(ret2, (uint256)), 1 ether); + (, bytes memory ret3) = mockErc20.call(data); + assertEq(abi.decode(ret3, (uint256)), 6.423 ether); + } +} diff --git a/testdata/default/cheats/MockFunction.t.sol b/testdata/default/cheats/MockFunction.t.sol index 9cf1004ca..6d670024b 100644 --- a/testdata/default/cheats/MockFunction.t.sol +++ b/testdata/default/cheats/MockFunction.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Nonce.t.sol b/testdata/default/cheats/Nonce.t.sol index 5dd8b0c6a..312c2b4d7 100644 --- a/testdata/default/cheats/Nonce.t.sol +++ b/testdata/default/cheats/Nonce.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Parse.t.sol b/testdata/default/cheats/Parse.t.sol index 71d49af6f..65e7561d1 100644 --- a/testdata/default/cheats/Parse.t.sol +++ b/testdata/default/cheats/Parse.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Prank.t.sol b/testdata/default/cheats/Prank.t.sol index f7dd9b714..d833c0513 100644 --- a/testdata/default/cheats/Prank.t.sol +++ b/testdata/default/cheats/Prank.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Prevrandao.t.sol b/testdata/default/cheats/Prevrandao.t.sol index 7011ce3be..aab8e326c 100644 --- a/testdata/default/cheats/Prevrandao.t.sol +++ b/testdata/default/cheats/Prevrandao.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -23,12 +23,12 @@ contract PrevrandaoTest is DSTest { function testPrevrandaoSnapshotFuzzed(uint256 newPrevrandao) public { vm.assume(newPrevrandao != block.prevrandao); uint256 oldPrevrandao = block.prevrandao; - uint256 snapshot = vm.snapshot(); + uint256 snapshotId = vm.snapshotState(); vm.prevrandao(newPrevrandao); assertEq(block.prevrandao, newPrevrandao); - assert(vm.revertTo(snapshot)); + assert(vm.revertToState(snapshotId)); assertEq(block.prevrandao, oldPrevrandao); } } diff --git a/testdata/default/cheats/ProjectRoot.t.sol b/testdata/default/cheats/ProjectRoot.t.sol index 31e68e105..cff3d8375 100644 --- a/testdata/default/cheats/ProjectRoot.t.sol +++ b/testdata/default/cheats/ProjectRoot.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Prompt.t.sol b/testdata/default/cheats/Prompt.t.sol index 33f83fea8..2e623a28e 100644 --- a/testdata/default/cheats/Prompt.t.sol +++ b/testdata/default/cheats/Prompt.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/RandomAddress.t.sol b/testdata/default/cheats/RandomAddress.t.sol index 74c5e2040..61510ed4e 100644 --- a/testdata/default/cheats/RandomAddress.t.sol +++ b/testdata/default/cheats/RandomAddress.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/RandomBytes.t.sol b/testdata/default/cheats/RandomBytes.t.sol new file mode 100644 index 000000000..dbc03a6cc --- /dev/null +++ b/testdata/default/cheats/RandomBytes.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract RandomBytes is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testRandomBytes4() public { + vm.randomBytes4(); + } + + function testRandomBytes8() public { + vm.randomBytes8(); + } + + function testFillrandomBytes() public view { + uint256 len = 16; + vm.randomBytes(len); + } +} diff --git a/testdata/default/cheats/RandomCheatcodes.t.sol b/testdata/default/cheats/RandomCheatcodes.t.sol new file mode 100644 index 000000000..beeee9862 --- /dev/null +++ b/testdata/default/cheats/RandomCheatcodes.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract RandomCheatcodesTest is DSTest { + Vm vm = Vm(HEVM_ADDRESS); + + int128 constant min = -170141183460469231731687303715884105728; + int128 constant max = 170141183460469231731687303715884105727; + + function test_int128() public { + vm.expectRevert("vm.randomInt: number of bits cannot exceed 256"); + int256 val = vm.randomInt(type(uint256).max); + + val = vm.randomInt(128); + assertGe(val, min); + assertLe(val, max); + } + + function testFail_int128() public { + int256 val = vm.randomInt(128); + assertGt(val, max); + } + + function test_address() public { + address fresh_address = vm.randomAddress(); + assert(fresh_address != address(this)); + assert(fresh_address != address(vm)); + } + + function test_randomUintLimit() public { + vm.expectRevert("vm.randomUint: number of bits cannot exceed 256"); + uint256 val = vm.randomUint(type(uint256).max); + } + + function test_randomUints(uint256 x) public { + x = vm.randomUint(0, 256); + uint256 freshUint = vm.randomUint(x); + + assert(0 <= freshUint); + if (x == 256) { + assert(freshUint <= type(uint256).max); + } else { + assert(freshUint <= 2 ** x - 1); + } + } + + function test_randomSymbolicWord() public { + uint256 freshUint192 = vm.randomUint(192); + + assert(0 <= freshUint192); + assert(freshUint192 <= type(uint192).max); + } +} + +contract RandomBytesTest is DSTest { + Vm vm = Vm(HEVM_ADDRESS); + + bytes1 local_byte; + bytes local_bytes; + + function manip_symbolic_bytes(bytes memory b) public { + uint256 middle = b.length / 2; + b[middle] = hex"aa"; + } + + function test_symbolic_bytes_revert() public { + vm.expectRevert(); + bytes memory val = vm.randomBytes(type(uint256).max); + } + + function test_symbolic_bytes_1() public { + uint256 length = uint256(vm.randomUint(1, type(uint8).max)); + bytes memory fresh_bytes = vm.randomBytes(length); + uint256 index = uint256(vm.randomUint(1)); + + local_byte = fresh_bytes[index]; + assertEq(fresh_bytes[index], local_byte); + } + + function test_symbolic_bytes_2() public { + uint256 length = uint256(vm.randomUint(1, type(uint8).max)); + bytes memory fresh_bytes = vm.randomBytes(length); + + local_bytes = fresh_bytes; + assertEq(fresh_bytes, local_bytes); + } + + function test_symbolic_bytes_3() public { + uint256 length = uint256(vm.randomUint(1, type(uint8).max)); + bytes memory fresh_bytes = vm.randomBytes(length); + + manip_symbolic_bytes(fresh_bytes); + assertEq(hex"aa", fresh_bytes[length / 2]); + } + + function test_symbolic_bytes_length(uint8 l) public { + vm.assume(0 < l); + bytes memory fresh_bytes = vm.randomBytes(l); + assertEq(fresh_bytes.length, l); + } +} diff --git a/testdata/default/cheats/RandomUint.t.sol b/testdata/default/cheats/RandomUint.t.sol index 68a65dc0f..c0021030d 100644 --- a/testdata/default/cheats/RandomUint.t.sol +++ b/testdata/default/cheats/RandomUint.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ReadCallers.t.sol b/testdata/default/cheats/ReadCallers.t.sol index e0da8ed0d..dbd198a2d 100644 --- a/testdata/default/cheats/ReadCallers.t.sol +++ b/testdata/default/cheats/ReadCallers.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Record.t.sol b/testdata/default/cheats/Record.t.sol index 152a5ccb5..c2907ebb8 100644 --- a/testdata/default/cheats/Record.t.sol +++ b/testdata/default/cheats/Record.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index a0aa2cb53..98b5843b2 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/RecordDebugTrace.t.sol b/testdata/default/cheats/RecordDebugTrace.t.sol new file mode 100644 index 000000000..ade2e7aaf --- /dev/null +++ b/testdata/default/cheats/RecordDebugTrace.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract MStoreAndMLoadCaller { + uint256 public constant expectedValueInMemory = 999; + + uint256 public memPtr; // the memory pointer being used + + function storeAndLoadValueFromMemory() public returns (uint256) { + uint256 mPtr; + assembly { + mPtr := mload(0x40) // load free pointer + mstore(mPtr, expectedValueInMemory) + mstore(0x40, add(mPtr, 0x20)) + } + + // record & expose the memory pointer location + memPtr = mPtr; + + uint256 result = 123; + assembly { + // override with `expectedValueInMemory` + result := mload(mPtr) + } + return result; + } +} + +contract FirstLayer { + SecondLayer secondLayer; + + constructor(SecondLayer _secondLayer) { + secondLayer = _secondLayer; + } + + function callSecondLayer() public view returns (uint256) { + return secondLayer.endHere(); + } +} + +contract SecondLayer { + uint256 public constant endNumber = 123; + + function endHere() public view returns (uint256) { + return endNumber; + } +} + +contract OutOfGas { + uint256 dummyVal = 0; + + function consumeGas() public { + dummyVal += 1; + } + + function triggerOOG() public { + bytes memory encodedFunctionCall = abi.encodeWithSignature("consumeGas()", ""); + uint256 notEnoughGas = 50; + (bool success,) = address(this).call{gas: notEnoughGas}(encodedFunctionCall); + require(!success, "it should error out of gas"); + } +} + +contract RecordDebugTraceTest is DSTest { + Vm constant cheats = Vm(HEVM_ADDRESS); + /** + * The goal of this test is to ensure the debug steps provide the correct OPCODE with its stack + * and memory input used. The test checke MSTORE and MLOAD and ensure it records the expected + * stack and memory inputs. + */ + + function testDebugTraceCanRecordOpcodeWithStackAndMemoryData() public { + MStoreAndMLoadCaller testContract = new MStoreAndMLoadCaller(); + + cheats.startDebugTraceRecording(); + + uint256 val = testContract.storeAndLoadValueFromMemory(); + assertTrue(val == testContract.expectedValueInMemory()); + + Vm.DebugStep[] memory steps = cheats.stopAndReturnDebugTraceRecording(); + + bool mstoreCalled = false; + bool mloadCalled = false; + + for (uint256 i = 0; i < steps.length; i++) { + Vm.DebugStep memory step = steps[i]; + if ( + step.opcode == 0x52 /*MSTORE*/ && step.stack[0] == testContract.memPtr() // MSTORE offset + && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val + ) { + mstoreCalled = true; + } + + if ( + step.opcode == 0x51 /*MLOAD*/ && step.stack[0] == testContract.memPtr() // MLOAD offset + && step.memoryInput.length == 32 // MLOAD should always load 32 bytes + && uint256(bytes32(step.memoryInput)) == testContract.expectedValueInMemory() // MLOAD value + ) { + mloadCalled = true; + } + } + + assertTrue(mstoreCalled); + assertTrue(mloadCalled); + } + + /** + * This test tests that the cheatcode can correctly record the depth of the debug steps. + * This is test by test -> FirstLayer -> SecondLayer and check that the + * depth of the FirstLayer and SecondLayer are all as expected. + */ + function testDebugTraceCanRecordDepth() public { + SecondLayer second = new SecondLayer(); + FirstLayer first = new FirstLayer(second); + + cheats.startDebugTraceRecording(); + + first.callSecondLayer(); + + Vm.DebugStep[] memory steps = cheats.stopAndReturnDebugTraceRecording(); + + bool goToDepthTwo = false; + bool goToDepthThree = false; + for (uint256 i = 0; i < steps.length; i++) { + Vm.DebugStep memory step = steps[i]; + + if (step.depth == 2) { + assertTrue(step.contractAddr == address(first), "must be first layer on depth 2"); + goToDepthTwo = true; + } + + if (step.depth == 3) { + assertTrue(step.contractAddr == address(second), "must be second layer on depth 3"); + goToDepthThree = true; + } + } + assertTrue(goToDepthTwo && goToDepthThree, "must have been to both first and second layer"); + } + + /** + * The goal of this test is to ensure it can return expected `isOutOfGas` flag. + * It is tested with out of gas result here. + */ + function testDebugTraceCanRecordOutOfGas() public { + OutOfGas testContract = new OutOfGas(); + + cheats.startDebugTraceRecording(); + + testContract.triggerOOG(); + + Vm.DebugStep[] memory steps = cheats.stopAndReturnDebugTraceRecording(); + + bool isOOG = false; + for (uint256 i = 0; i < steps.length; i++) { + Vm.DebugStep memory step = steps[i]; + + if (step.isOutOfGas) { + isOOG = true; + } + } + assertTrue(isOOG, "should OOG"); + } +} diff --git a/testdata/default/cheats/RecordLogs.t.sol b/testdata/default/cheats/RecordLogs.t.sol index 728acdb9b..14ca8dde3 100644 --- a/testdata/default/cheats/RecordLogs.t.sol +++ b/testdata/default/cheats/RecordLogs.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Remember.t.sol b/testdata/default/cheats/Remember.t.sol index b5487c369..b8dbe7e38 100644 --- a/testdata/default/cheats/Remember.t.sol +++ b/testdata/default/cheats/Remember.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/ResetNonce.t.sol b/testdata/default/cheats/ResetNonce.t.sol index 901433609..d8c911587 100644 --- a/testdata/default/cheats/ResetNonce.t.sol +++ b/testdata/default/cheats/ResetNonce.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Roll.t.sol b/testdata/default/cheats/Roll.t.sol index 820cd9887..87f909cdd 100644 --- a/testdata/default/cheats/Roll.t.sol +++ b/testdata/default/cheats/Roll.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/RpcUrls.t.sol b/testdata/default/cheats/RpcUrls.t.sol index f7b980860..7976fa572 100644 --- a/testdata/default/cheats/RpcUrls.t.sol +++ b/testdata/default/cheats/RpcUrls.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/SetBlockhash.t.sol b/testdata/default/cheats/SetBlockhash.t.sol index f6c2af5f6..1274620df 100644 --- a/testdata/default/cheats/SetBlockhash.t.sol +++ b/testdata/default/cheats/SetBlockhash.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/SetNonce.t.sol b/testdata/default/cheats/SetNonce.t.sol index 7f2e419b9..83e3830ab 100644 --- a/testdata/default/cheats/SetNonce.t.sol +++ b/testdata/default/cheats/SetNonce.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/SetNonceUnsafe.t.sol b/testdata/default/cheats/SetNonceUnsafe.t.sol index 723f66ae2..0caf2b4ce 100644 --- a/testdata/default/cheats/SetNonceUnsafe.t.sol +++ b/testdata/default/cheats/SetNonceUnsafe.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Setup.t.sol b/testdata/default/cheats/Setup.t.sol index d694fb2c1..4d6e5954b 100644 --- a/testdata/default/cheats/Setup.t.sol +++ b/testdata/default/cheats/Setup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Sign.t.sol b/testdata/default/cheats/Sign.t.sol index a257d6291..937ebc00a 100644 --- a/testdata/default/cheats/Sign.t.sol +++ b/testdata/default/cheats/Sign.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/SignP256.t.sol b/testdata/default/cheats/SignP256.t.sol index f1b62fe78..b92588ce9 100644 --- a/testdata/default/cheats/SignP256.t.sol +++ b/testdata/default/cheats/SignP256.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Skip.t.sol b/testdata/default/cheats/Skip.t.sol index fb2deadb4..e2b0fc181 100644 --- a/testdata/default/cheats/Skip.t.sol +++ b/testdata/default/cheats/Skip.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Sleep.t.sol b/testdata/default/cheats/Sleep.t.sol index 448d34668..7af548e74 100644 --- a/testdata/default/cheats/Sleep.t.sol +++ b/testdata/default/cheats/Sleep.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Snapshots.t.sol b/testdata/default/cheats/Snapshots.t.sol deleted file mode 100644 index bb7b4e0e6..000000000 --- a/testdata/default/cheats/Snapshots.t.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; - -import "ds-test/test.sol"; -import "cheats/Vm.sol"; - -struct Storage { - uint256 slot0; - uint256 slot1; -} - -contract SnapshotTest is DSTest { - Vm constant vm = Vm(HEVM_ADDRESS); - - Storage store; - - function setUp() public { - store.slot0 = 10; - store.slot1 = 20; - } - - function testSnapshot() public { - uint256 snapshot = vm.snapshot(); - store.slot0 = 300; - store.slot1 = 400; - - assertEq(store.slot0, 300); - assertEq(store.slot1, 400); - - vm.revertTo(snapshot); - assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); - assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); - } - - function testSnapshotRevertDelete() public { - uint256 snapshot = vm.snapshot(); - store.slot0 = 300; - store.slot1 = 400; - - assertEq(store.slot0, 300); - assertEq(store.slot1, 400); - - vm.revertToAndDelete(snapshot); - assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); - assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); - // nothing to revert to anymore - assert(!vm.revertTo(snapshot)); - } - - function testSnapshotDelete() public { - uint256 snapshot = vm.snapshot(); - store.slot0 = 300; - store.slot1 = 400; - - vm.deleteSnapshot(snapshot); - // nothing to revert to anymore - assert(!vm.revertTo(snapshot)); - } - - function testSnapshotDeleteAll() public { - uint256 snapshot = vm.snapshot(); - store.slot0 = 300; - store.slot1 = 400; - - vm.deleteSnapshots(); - // nothing to revert to anymore - assert(!vm.revertTo(snapshot)); - } - - // - function testSnapshotsMany() public { - uint256 preState; - for (uint256 c = 0; c < 10; c++) { - for (uint256 cc = 0; cc < 10; cc++) { - preState = vm.snapshot(); - vm.revertToAndDelete(preState); - assert(!vm.revertTo(preState)); - } - } - } - - // tests that snapshots can also revert changes to `block` - function testBlockValues() public { - uint256 num = block.number; - uint256 time = block.timestamp; - uint256 prevrandao = block.prevrandao; - - uint256 snapshot = vm.snapshot(); - - vm.warp(1337); - assertEq(block.timestamp, 1337); - - vm.roll(99); - assertEq(block.number, 99); - - vm.prevrandao(uint256(123)); - assertEq(block.prevrandao, 123); - - assert(vm.revertTo(snapshot)); - - assertEq(block.number, num, "snapshot revert for block.number unsuccessful"); - assertEq(block.timestamp, time, "snapshot revert for block.timestamp unsuccessful"); - assertEq(block.prevrandao, prevrandao, "snapshot revert for block.prevrandao unsuccessful"); - } -} diff --git a/testdata/default/cheats/StateSnapshots.t.sol b/testdata/default/cheats/StateSnapshots.t.sol new file mode 100644 index 000000000..8751a0409 --- /dev/null +++ b/testdata/default/cheats/StateSnapshots.t.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +struct Storage { + uint256 slot0; + uint256 slot1; +} + +contract StateSnapshotTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + Storage store; + + function setUp() public { + store.slot0 = 10; + store.slot1 = 20; + } + + function testStateSnapshot() public { + uint256 snapshotId = vm.snapshotState(); + store.slot0 = 300; + store.slot1 = 400; + + assertEq(store.slot0, 300); + assertEq(store.slot1, 400); + + vm.revertToState(snapshotId); + assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); + assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); + } + + function testStateSnapshotRevertDelete() public { + uint256 snapshotId = vm.snapshotState(); + store.slot0 = 300; + store.slot1 = 400; + + assertEq(store.slot0, 300); + assertEq(store.slot1, 400); + + vm.revertToStateAndDelete(snapshotId); + assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); + assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); + // nothing to revert to anymore + assert(!vm.revertToState(snapshotId)); + } + + function testStateSnapshotDelete() public { + uint256 snapshotId = vm.snapshotState(); + store.slot0 = 300; + store.slot1 = 400; + + vm.deleteStateSnapshot(snapshotId); + // nothing to revert to anymore + assert(!vm.revertToState(snapshotId)); + } + + function testStateSnapshotDeleteAll() public { + uint256 snapshotId = vm.snapshotState(); + store.slot0 = 300; + store.slot1 = 400; + + vm.deleteStateSnapshots(); + // nothing to revert to anymore + assert(!vm.revertToState(snapshotId)); + } + + // + function testStateSnapshotsMany() public { + uint256 snapshotId; + for (uint256 c = 0; c < 10; c++) { + for (uint256 cc = 0; cc < 10; cc++) { + snapshotId = vm.snapshotState(); + vm.revertToStateAndDelete(snapshotId); + assert(!vm.revertToState(snapshotId)); + } + } + } + + // tests that snapshots can also revert changes to `block` + function testBlockValues() public { + uint256 num = block.number; + uint256 time = block.timestamp; + uint256 prevrandao = block.prevrandao; + + uint256 snapshotId = vm.snapshotState(); + + vm.warp(1337); + assertEq(block.timestamp, 1337); + + vm.roll(99); + assertEq(block.number, 99); + + vm.prevrandao(uint256(123)); + assertEq(block.prevrandao, 123); + + assert(vm.revertToState(snapshotId)); + + assertEq(block.number, num, "snapshot revert for block.number unsuccessful"); + assertEq(block.timestamp, time, "snapshot revert for block.timestamp unsuccessful"); + assertEq(block.prevrandao, prevrandao, "snapshot revert for block.prevrandao unsuccessful"); + } +} + +// TODO: remove this test suite once `snapshot*` has been deprecated in favor of `snapshotState*`. +contract DeprecatedStateSnapshotTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + Storage store; + + function setUp() public { + store.slot0 = 10; + store.slot1 = 20; + } + + function testSnapshotState() public { + uint256 snapshotId = vm.snapshot(); + store.slot0 = 300; + store.slot1 = 400; + + assertEq(store.slot0, 300); + assertEq(store.slot1, 400); + + vm.revertTo(snapshotId); + assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); + assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); + } + + function testSnapshotStateRevertDelete() public { + uint256 snapshotId = vm.snapshot(); + store.slot0 = 300; + store.slot1 = 400; + + assertEq(store.slot0, 300); + assertEq(store.slot1, 400); + + vm.revertToAndDelete(snapshotId); + assertEq(store.slot0, 10, "snapshot revert for slot 0 unsuccessful"); + assertEq(store.slot1, 20, "snapshot revert for slot 1 unsuccessful"); + // nothing to revert to anymore + assert(!vm.revertTo(snapshotId)); + } + + function testSnapshotStateDelete() public { + uint256 snapshotId = vm.snapshot(); + store.slot0 = 300; + store.slot1 = 400; + + vm.deleteSnapshot(snapshotId); + // nothing to revert to anymore + assert(!vm.revertTo(snapshotId)); + } + + function testSnapshotStateDeleteAll() public { + uint256 snapshotId = vm.snapshot(); + store.slot0 = 300; + store.slot1 = 400; + + vm.deleteSnapshots(); + // nothing to revert to anymore + assert(!vm.revertTo(snapshotId)); + } + + // + function testSnapshotStatesMany() public { + uint256 snapshotId; + for (uint256 c = 0; c < 10; c++) { + for (uint256 cc = 0; cc < 10; cc++) { + snapshotId = vm.snapshot(); + vm.revertToAndDelete(snapshotId); + assert(!vm.revertTo(snapshotId)); + } + } + } + + // tests that snapshots can also revert changes to `block` + function testBlockValues() public { + uint256 num = block.number; + uint256 time = block.timestamp; + uint256 prevrandao = block.prevrandao; + + uint256 snapshotId = vm.snapshot(); + + vm.warp(1337); + assertEq(block.timestamp, 1337); + + vm.roll(99); + assertEq(block.number, 99); + + vm.prevrandao(uint256(123)); + assertEq(block.prevrandao, 123); + + assert(vm.revertTo(snapshotId)); + + assertEq(block.number, num, "snapshot revert for block.number unsuccessful"); + assertEq(block.timestamp, time, "snapshot revert for block.timestamp unsuccessful"); + assertEq(block.prevrandao, prevrandao, "snapshot revert for block.prevrandao unsuccessful"); + } +} diff --git a/testdata/default/cheats/Store.t.sol b/testdata/default/cheats/Store.t.sol index 5159a4ab8..9a1ce6101 100644 --- a/testdata/default/cheats/Store.t.sol +++ b/testdata/default/cheats/Store.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/StringUtils.t.sol b/testdata/default/cheats/StringUtils.t.sol index 136164a41..256d65302 100644 --- a/testdata/default/cheats/StringUtils.t.sol +++ b/testdata/default/cheats/StringUtils.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -51,4 +51,10 @@ contract StringManipulationTest is DSTest { assertEq(vm.indexOf(input, key3), 0); assertEq(vm.indexOf(input, key4), type(uint256).max); } + + function testContains() public { + string memory subject = "this is a test"; + assert(vm.contains(subject, "test")); + assert(!vm.contains(subject, "foundry")); + } } diff --git a/testdata/default/cheats/ToString.t.sol b/testdata/default/cheats/ToString.t.sol index 835c85242..f19110e3e 100644 --- a/testdata/default/cheats/ToString.t.sol +++ b/testdata/default/cheats/ToString.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Toml.t.sol b/testdata/default/cheats/Toml.t.sol index a01b29af6..5f0ef5b43 100644 --- a/testdata/default/cheats/Toml.t.sol +++ b/testdata/default/cheats/Toml.t.sol @@ -1,11 +1,85 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; import "../logs/console.sol"; +library TomlStructs { + address constant HEVM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + Vm constant vm = Vm(HEVM_ADDRESS); + + // forge eip712 testdata/default/cheats/Toml.t.sol -R 'cheats=testdata/cheats' -R 'ds-test=testdata/lib/ds-test/src' | grep ^FlatToml + string constant schema_FlatToml = + "FlatToml(uint256 a,int24[][] arr,string str,bytes b,address addr,bytes32 fixedBytes)"; + + // forge eip712 testdata/default/cheats/Toml.t.sol -R 'cheats=testdata/cheats' -R 'ds-test=testdata/lib/ds-test/src' | grep ^NestedToml + string constant schema_NestedToml = + "NestedToml(FlatToml[] members,AnotherFlatToml inner,string name)AnotherFlatToml(bytes4 fixedBytes)FlatToml(uint256 a,int24[][] arr,string str,bytes b,address addr,bytes32 fixedBytes)"; + + function deserializeFlatToml(string memory toml) internal pure returns (ParseTomlTest.FlatToml memory) { + return abi.decode(vm.parseTomlType(toml, schema_FlatToml), (ParseTomlTest.FlatToml)); + } + + function deserializeFlatToml(string memory toml, string memory path) + internal + pure + returns (ParseTomlTest.FlatToml memory) + { + return abi.decode(vm.parseTomlType(toml, path, schema_FlatToml), (ParseTomlTest.FlatToml)); + } + + function deserializeFlatTomlArray(string memory toml, string memory path) + internal + pure + returns (ParseTomlTest.FlatToml[] memory) + { + return abi.decode(vm.parseTomlTypeArray(toml, path, schema_FlatToml), (ParseTomlTest.FlatToml[])); + } + + function deserializeNestedToml(string memory toml) internal pure returns (ParseTomlTest.NestedToml memory) { + return abi.decode(vm.parseTomlType(toml, schema_NestedToml), (ParseTomlTest.NestedToml)); + } + + function deserializeNestedToml(string memory toml, string memory path) + internal + pure + returns (ParseTomlTest.NestedToml memory) + { + return abi.decode(vm.parseTomlType(toml, path, schema_NestedToml), (ParseTomlTest.NestedToml)); + } + + function deserializeNestedTomlArray(string memory toml, string memory path) + internal + pure + returns (ParseTomlTest.NestedToml[] memory) + { + return abi.decode(vm.parseTomlType(toml, path, schema_NestedToml), (ParseTomlTest.NestedToml[])); + } +} + contract ParseTomlTest is DSTest { + using TomlStructs for *; + + struct FlatToml { + uint256 a; + int24[][] arr; + string str; + bytes b; + address addr; + bytes32 fixedBytes; + } + + struct AnotherFlatToml { + bytes4 fixedBytes; + } + + struct NestedToml { + FlatToml[] members; + AnotherFlatToml inner; + string name; + } + Vm constant vm = Vm(HEVM_ADDRESS); string toml; @@ -169,20 +243,20 @@ contract ParseTomlTest is DSTest { assertEq(bytesArray[1], hex"02"); } - struct Nested { + struct NestedStruct { uint256 number; string str; } function test_nestedObject() public { bytes memory data = vm.parseToml(toml, ".nestedObject"); - Nested memory nested = abi.decode(data, (Nested)); + NestedStruct memory nested = abi.decode(data, (NestedStruct)); assertEq(nested.number, 9223372036854775807); // TOML is limited to 64-bit integers assertEq(nested.str, "NEST"); } - function test_advancedJsonPath() public { - bytes memory data = vm.parseToml(toml, ".advancedJsonPath[*].id"); + function test_advancedTomlPath() public { + bytes memory data = vm.parseToml(toml, ".advancedTomlPath[*].id"); uint256[] memory numbers = abi.decode(data, (uint256[])); assertEq(numbers[0], 1); assertEq(numbers[1], 2); @@ -225,6 +299,36 @@ contract ParseTomlTest is DSTest { vm._expectCheatcodeRevert("key \".*\" must return exactly one JSON object"); vm.parseTomlKeys(tomlString, ".*"); } + + // forge eip712 testdata/default/cheats/Toml.t.sol -R 'cheats=testdata/cheats' -R 'ds-test=testdata/lib/ds-test/src' | grep ^FlatToml + string constant schema_FlatToml = + "FlatToml(uint256 a,int24[][] arr,string str,bytes b,address addr,bytes32 fixedBytes)"; + + // forge eip712 testdata/default/cheats/Toml.t.sol -R 'cheats=testdata/cheats' -R 'ds-test=testdata/lib/ds-test/src' | grep ^NestedToml + string constant schema_NestedToml = + "NestedToml(FlatToml[] members,AnotherFlatToml inner,string name)AnotherFlatToml(bytes4 fixedBytes)FlatToml(uint256 a,int24[][] arr,string str,bytes b,address addr,bytes32 fixedBytes)"; + + function test_parseTomlType() public { + string memory readToml = vm.readFile("fixtures/Toml/nested_toml_struct.toml"); + NestedToml memory data = readToml.deserializeNestedToml(); + assertEq(data.members.length, 2); + + FlatToml memory expected = FlatToml({ + a: 200, + arr: new int24[][](0), + str: "some other string", + b: hex"0000000000000000000000000000000000000000", + addr: 0x167D91deaEEE3021161502873d3bcc6291081648, + fixedBytes: 0xed1c7beb1f00feaaaec5636950d6edb25a8d4fedc8deb2711287b64c4d27719d + }); + + assertEq(keccak256(abi.encode(data.members[1])), keccak256(abi.encode(expected))); + assertEq(bytes32(data.inner.fixedBytes), bytes32(bytes4(0x12345678))); + + FlatToml[] memory members = TomlStructs.deserializeFlatTomlArray(readToml, ".members"); + + assertEq(keccak256(abi.encode(members)), keccak256(abi.encode(data.members))); + } } contract WriteTomlTest is DSTest { @@ -238,18 +342,18 @@ contract WriteTomlTest is DSTest { json2 = "example2"; } - struct simpleJson { + struct simpleStruct { uint256 a; string b; } - struct notSimpleJson { + struct nestedStruct { uint256 a; string b; - simpleJson c; + simpleStruct c; } - function test_serializeNotSimpleToml() public { + function test_serializeNestedStructToml() public { string memory json3 = "json3"; string memory path = "fixtures/Toml/write_complex_test.toml"; vm.serializeUint(json3, "a", uint256(123)); @@ -259,14 +363,16 @@ contract WriteTomlTest is DSTest { vm.writeToml(finalJson, path); string memory toml = vm.readFile(path); bytes memory data = vm.parseToml(toml); - notSimpleJson memory decodedData = abi.decode(data, (notSimpleJson)); + nestedStruct memory decodedData = abi.decode(data, (nestedStruct)); + console.log(decodedData.a); + assertEq(decodedData.a, 123); } function test_retrieveEntireToml() public { string memory path = "fixtures/Toml/write_complex_test.toml"; string memory toml = vm.readFile(path); bytes memory data = vm.parseToml(toml, "."); - notSimpleJson memory decodedData = abi.decode(data, (notSimpleJson)); + nestedStruct memory decodedData = abi.decode(data, (nestedStruct)); console.log(decodedData.a); assertEq(decodedData.a, 123); } @@ -294,7 +400,7 @@ contract WriteTomlTest is DSTest { string memory toml = vm.readFile(path); bytes memory data = vm.parseToml(toml); - simpleJson memory decodedData = abi.decode(data, (simpleJson)); + simpleStruct memory decodedData = abi.decode(data, (simpleStruct)); assertEq(decodedData.a, 123); assertEq(decodedData.b, "test"); @@ -303,7 +409,7 @@ contract WriteTomlTest is DSTest { // read again toml = vm.readFile(path); data = vm.parseToml(toml, ".b"); - decodedData = abi.decode(data, (simpleJson)); + decodedData = abi.decode(data, (simpleStruct)); assertEq(decodedData.a, 123); assertEq(decodedData.b, "test"); diff --git a/testdata/default/cheats/Travel.t.sol b/testdata/default/cheats/Travel.t.sol index 733559b29..b46d2e7ad 100644 --- a/testdata/default/cheats/Travel.t.sol +++ b/testdata/default/cheats/Travel.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/UnixTime.t.sol b/testdata/default/cheats/UnixTime.t.sol index a6b683967..29d86699f 100644 --- a/testdata/default/cheats/UnixTime.t.sol +++ b/testdata/default/cheats/UnixTime.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Wallet.t.sol b/testdata/default/cheats/Wallet.t.sol index 5a7035876..d061b55ae 100644 --- a/testdata/default/cheats/Wallet.t.sol +++ b/testdata/default/cheats/Wallet.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/Warp.t.sol b/testdata/default/cheats/Warp.t.sol index 01ebc8e89..42f373c61 100644 --- a/testdata/default/cheats/Warp.t.sol +++ b/testdata/default/cheats/Warp.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/dumpState.t.sol b/testdata/default/cheats/dumpState.t.sol index 74ebd3071..8a8675ca5 100644 --- a/testdata/default/cheats/dumpState.t.sol +++ b/testdata/default/cheats/dumpState.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/getBlockNumber.t.sol b/testdata/default/cheats/getBlockNumber.t.sol index c874e5e2f..ebf240dd8 100644 --- a/testdata/default/cheats/getBlockNumber.t.sol +++ b/testdata/default/cheats/getBlockNumber.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/cheats/loadAllocs.t.sol b/testdata/default/cheats/loadAllocs.t.sol index 358608860..94ce6804c 100644 --- a/testdata/default/cheats/loadAllocs.t.sol +++ b/testdata/default/cheats/loadAllocs.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -16,7 +16,7 @@ contract LoadAllocsTest is DSTest { allocsPath = string.concat(vm.projectRoot(), "/fixtures/Json/test_allocs.json"); // Snapshot the state; We'll restore it in each test that loads allocs inline. - snapshotId = vm.snapshot(); + snapshotId = vm.snapshotState(); // Load the allocs file. vm.loadAllocs(allocsPath); @@ -40,7 +40,7 @@ contract LoadAllocsTest is DSTest { /// @dev Checks that the `loadAllocs` cheatcode persists account info if called inline function testLoadAllocsStatic() public { // Restore the state snapshot prior to the allocs file being loaded. - vm.revertTo(snapshotId); + vm.revertToState(snapshotId); // Load the allocs file vm.loadAllocs(allocsPath); @@ -61,7 +61,7 @@ contract LoadAllocsTest is DSTest { /// @dev Checks that the `loadAllocs` cheatcode overrides existing account information (if present) function testLoadAllocsOverride() public { // Restore the state snapshot prior to the allocs file being loaded. - vm.revertTo(snapshotId); + vm.revertToState(snapshotId); // Populate the alloc'd account's code. vm.etch(ALLOCD, hex"FF"); @@ -88,7 +88,7 @@ contract LoadAllocsTest is DSTest { /// within the allocs/genesis file for the account field (i.e., partial overrides) function testLoadAllocsPartialOverride() public { // Restore the state snapshot prior to the allocs file being loaded. - vm.revertTo(snapshotId); + vm.revertToState(snapshotId); // Populate the alloc'd account's code. vm.etch(ALLOCD_B, hex"FF"); diff --git a/testdata/default/core/Abstract.t.sol b/testdata/default/core/Abstract.t.sol index fee06bbe6..d04d0ff77 100644 --- a/testdata/default/core/Abstract.t.sol +++ b/testdata/default/core/Abstract.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; contract TestFixture { function something() public pure returns (string memory) { diff --git a/testdata/default/core/BadSigAfterInvariant.t.sol b/testdata/default/core/BadSigAfterInvariant.t.sol index 6d303b04b..7b485e24f 100644 --- a/testdata/default/core/BadSigAfterInvariant.t.sol +++ b/testdata/default/core/BadSigAfterInvariant.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/ContractEnvironment.t.sol b/testdata/default/core/ContractEnvironment.t.sol index cc5facd92..452fa8802 100644 --- a/testdata/default/core/ContractEnvironment.t.sol +++ b/testdata/default/core/ContractEnvironment.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/DSStyle.t.sol b/testdata/default/core/DSStyle.t.sol index 400b707b6..1eaf83969 100644 --- a/testdata/default/core/DSStyle.t.sol +++ b/testdata/default/core/DSStyle.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/FailingSetup.t.sol b/testdata/default/core/FailingSetup.t.sol index 37a3b9ac2..d5e24e131 100644 --- a/testdata/default/core/FailingSetup.t.sol +++ b/testdata/default/core/FailingSetup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/FailingTestAfterFailedSetup.t.sol b/testdata/default/core/FailingTestAfterFailedSetup.t.sol index eeb5c207e..c56f4ba5d 100644 --- a/testdata/default/core/FailingTestAfterFailedSetup.t.sol +++ b/testdata/default/core/FailingTestAfterFailedSetup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/LegacyAssertions.t.sol b/testdata/default/core/LegacyAssertions.t.sol index 9bbc56e8e..c35a63417 100644 --- a/testdata/default/core/LegacyAssertions.t.sol +++ b/testdata/default/core/LegacyAssertions.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/core/MultipleAfterInvariant.t.sol b/testdata/default/core/MultipleAfterInvariant.t.sol index 446e76cbb..b23b0996a 100644 --- a/testdata/default/core/MultipleAfterInvariant.t.sol +++ b/testdata/default/core/MultipleAfterInvariant.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/MultipleSetup.t.sol b/testdata/default/core/MultipleSetup.t.sol index 412b3fb4c..73cbaf1a9 100644 --- a/testdata/default/core/MultipleSetup.t.sol +++ b/testdata/default/core/MultipleSetup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/core/PaymentFailure.t.sol b/testdata/default/core/PaymentFailure.t.sol index d4751b2d5..52c42fd37 100644 --- a/testdata/default/core/PaymentFailure.t.sol +++ b/testdata/default/core/PaymentFailure.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/core/Reverting.t.sol b/testdata/default/core/Reverting.t.sol index 36fdd99bc..91022e6ad 100644 --- a/testdata/default/core/Reverting.t.sol +++ b/testdata/default/core/Reverting.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; contract RevertingTest { function testFailRevert() public pure { diff --git a/testdata/default/core/SetupConsistency.t.sol b/testdata/default/core/SetupConsistency.t.sol index dfc6a9ab4..08d766f0f 100644 --- a/testdata/default/core/SetupConsistency.t.sol +++ b/testdata/default/core/SetupConsistency.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fork/ForkSame_1.t.sol b/testdata/default/fork/ForkSame_1.t.sol index bbb73fcaa..949c7ea9e 100644 --- a/testdata/default/fork/ForkSame_1.t.sol +++ b/testdata/default/fork/ForkSame_1.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fork/ForkSame_2.t.sol b/testdata/default/fork/ForkSame_2.t.sol index bbb73fcaa..949c7ea9e 100644 --- a/testdata/default/fork/ForkSame_2.t.sol +++ b/testdata/default/fork/ForkSame_2.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fork/Transact.t.sol b/testdata/default/fork/Transact.t.sol index 0e3d9c9cb..375658772 100644 --- a/testdata/default/fork/Transact.t.sol +++ b/testdata/default/fork/Transact.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fs/Default.t.sol b/testdata/default/fs/Default.t.sol index 5e776e696..e1524963f 100644 --- a/testdata/default/fs/Default.t.sol +++ b/testdata/default/fs/Default.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fs/Disabled.t.sol b/testdata/default/fs/Disabled.t.sol index 4efe9affc..36f05c211 100644 --- a/testdata/default/fs/Disabled.t.sol +++ b/testdata/default/fs/Disabled.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fuzz/Fuzz.t.sol b/testdata/default/fuzz/Fuzz.t.sol index ef4465380..24c8d1750 100644 --- a/testdata/default/fuzz/Fuzz.t.sol +++ b/testdata/default/fuzz/Fuzz.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/FuzzCollection.t.sol b/testdata/default/fuzz/FuzzCollection.t.sol index 9be09bea8..0c98ddc66 100644 --- a/testdata/default/fuzz/FuzzCollection.t.sol +++ b/testdata/default/fuzz/FuzzCollection.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/FuzzFailurePersist.t.sol b/testdata/default/fuzz/FuzzFailurePersist.t.sol index 0823f29fb..378706041 100644 --- a/testdata/default/fuzz/FuzzFailurePersist.t.sol +++ b/testdata/default/fuzz/FuzzFailurePersist.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fuzz/FuzzInt.t.sol b/testdata/default/fuzz/FuzzInt.t.sol index 071727f6d..a47ff2953 100644 --- a/testdata/default/fuzz/FuzzInt.t.sol +++ b/testdata/default/fuzz/FuzzInt.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/FuzzPositive.t.sol b/testdata/default/fuzz/FuzzPositive.t.sol index 952a3b699..7d3639dfe 100644 --- a/testdata/default/fuzz/FuzzPositive.t.sol +++ b/testdata/default/fuzz/FuzzPositive.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/FuzzScrapeBytecode.t.sol b/testdata/default/fuzz/FuzzScrapeBytecode.t.sol deleted file mode 100644 index ffded67f0..000000000 --- a/testdata/default/fuzz/FuzzScrapeBytecode.t.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; - -import "ds-test/test.sol"; - -// https://github.com/foundry-rs/foundry/issues/1168 -contract FuzzerDict { - // Immutables should get added to the dictionary. - address public immutable immutableOwner; - // Regular storage variables should also get added to the dictionary. - address public storageOwner; - - constructor(address _immutableOwner, address _storageOwner) { - immutableOwner = _immutableOwner; - storageOwner = _storageOwner; - } -} - -contract FuzzerDictTest is DSTest { - FuzzerDict fuzzerDict; - - function setUp() public { - fuzzerDict = new FuzzerDict(address(100), address(200)); - } - - // Fuzzer should try `fuzzerDict.immutableOwner()` as input, causing this to fail - function testImmutableOwner(address who) public { - assertTrue(who != fuzzerDict.immutableOwner()); - } - - // Fuzzer should try `fuzzerDict.storageOwner()` as input, causing this to fail - function testStorageOwner(address who) public { - assertTrue(who != fuzzerDict.storageOwner()); - } -} diff --git a/testdata/default/fuzz/FuzzUint.t.sol b/testdata/default/fuzz/FuzzUint.t.sol index 923c2980f..c0cbf6466 100644 --- a/testdata/default/fuzz/FuzzUint.t.sol +++ b/testdata/default/fuzz/FuzzUint.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol index 5387b020d..3d4c51eac 100644 --- a/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantCalldataDictionary.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol b/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol index b8d5dc416..f8330a33c 100644 --- a/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/common/InvariantPreserveState.t.sol b/testdata/default/fuzz/invariant/common/InvariantPreserveState.t.sol index 546980136..bd70dd3ae 100644 --- a/testdata/default/fuzz/invariant/common/InvariantPreserveState.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantPreserveState.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/fuzz/invariant/common/InvariantReentrancy.t.sol b/testdata/default/fuzz/invariant/common/InvariantReentrancy.t.sol index f439b8ce1..74a01f180 100644 --- a/testdata/default/fuzz/invariant/common/InvariantReentrancy.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantReentrancy.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/common/InvariantTest1.t.sol b/testdata/default/fuzz/invariant/common/InvariantTest1.t.sol index 15deccf33..bb62f34c6 100644 --- a/testdata/default/fuzz/invariant/common/InvariantTest1.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantTest1.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/ExcludeContracts.t.sol b/testdata/default/fuzz/invariant/target/ExcludeContracts.t.sol index 60aef60d9..e2e850e31 100644 --- a/testdata/default/fuzz/invariant/target/ExcludeContracts.t.sol +++ b/testdata/default/fuzz/invariant/target/ExcludeContracts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/ExcludeSelectors.t.sol b/testdata/default/fuzz/invariant/target/ExcludeSelectors.t.sol index 526da0c67..e2251f42c 100644 --- a/testdata/default/fuzz/invariant/target/ExcludeSelectors.t.sol +++ b/testdata/default/fuzz/invariant/target/ExcludeSelectors.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/ExcludeSenders.t.sol b/testdata/default/fuzz/invariant/target/ExcludeSenders.t.sol index bd37bd9bb..dda07074d 100644 --- a/testdata/default/fuzz/invariant/target/ExcludeSenders.t.sol +++ b/testdata/default/fuzz/invariant/target/ExcludeSenders.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol b/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol index 7988d5c8a..759810611 100644 --- a/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol +++ b/testdata/default/fuzz/invariant/target/FuzzedTargetContracts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/TargetContracts.t.sol b/testdata/default/fuzz/invariant/target/TargetContracts.t.sol index cdce69153..d24c7eb52 100644 --- a/testdata/default/fuzz/invariant/target/TargetContracts.t.sol +++ b/testdata/default/fuzz/invariant/target/TargetContracts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/TargetInterfaces.t.sol b/testdata/default/fuzz/invariant/target/TargetInterfaces.t.sol index 5cfa85ce1..30b4a05e3 100644 --- a/testdata/default/fuzz/invariant/target/TargetInterfaces.t.sol +++ b/testdata/default/fuzz/invariant/target/TargetInterfaces.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/TargetSelectors.t.sol b/testdata/default/fuzz/invariant/target/TargetSelectors.t.sol index ef3af2142..c74ac7fa1 100644 --- a/testdata/default/fuzz/invariant/target/TargetSelectors.t.sol +++ b/testdata/default/fuzz/invariant/target/TargetSelectors.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/target/TargetSenders.t.sol b/testdata/default/fuzz/invariant/target/TargetSenders.t.sol index 50f37c4e9..6fa4c9a63 100644 --- a/testdata/default/fuzz/invariant/target/TargetSenders.t.sol +++ b/testdata/default/fuzz/invariant/target/TargetSenders.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol b/testdata/default/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol index bf457ab17..86ca6d543 100644 --- a/testdata/default/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol +++ b/testdata/default/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol b/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol index 2957c57de..440f6183f 100644 --- a/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol +++ b/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol b/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol index c12cae74f..162d9cc2e 100644 --- a/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol +++ b/testdata/default/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/fuzz/invariant/targetAbi/TargetArtifacts.t.sol b/testdata/default/fuzz/invariant/targetAbi/TargetArtifacts.t.sol index ea86ab135..28fa14605 100644 --- a/testdata/default/fuzz/invariant/targetAbi/TargetArtifacts.t.sol +++ b/testdata/default/fuzz/invariant/targetAbi/TargetArtifacts.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/inline/FuzzInlineConf.t.sol b/testdata/default/inline/FuzzInlineConf.t.sol index 378931312..73d2de2fc 100644 --- a/testdata/default/inline/FuzzInlineConf.t.sol +++ b/testdata/default/inline/FuzzInlineConf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/inline/InvariantInlineConf.t.sol b/testdata/default/inline/InvariantInlineConf.t.sol index c032911ec..5ac81755e 100644 --- a/testdata/default/inline/InvariantInlineConf.t.sol +++ b/testdata/default/inline/InvariantInlineConf.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/linking/cycle/Cycle.t.sol b/testdata/default/linking/cycle/Cycle.t.sol index 424bc001f..010f55ac3 100644 --- a/testdata/default/linking/cycle/Cycle.t.sol +++ b/testdata/default/linking/cycle/Cycle.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; library Foo { function foo() external { diff --git a/testdata/default/linking/duplicate/Duplicate.t.sol b/testdata/default/linking/duplicate/Duplicate.t.sol index a09c81e4f..d1d0f3278 100644 --- a/testdata/default/linking/duplicate/Duplicate.t.sol +++ b/testdata/default/linking/duplicate/Duplicate.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/linking/nested/Nested.t.sol b/testdata/default/linking/nested/Nested.t.sol index 90100c889..136cb3647 100644 --- a/testdata/default/linking/nested/Nested.t.sol +++ b/testdata/default/linking/nested/Nested.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/linking/simple/Simple.t.sol b/testdata/default/linking/simple/Simple.t.sol index a6a636b6c..85be791fd 100644 --- a/testdata/default/linking/simple/Simple.t.sol +++ b/testdata/default/linking/simple/Simple.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/logs/DebugLogs.t.sol b/testdata/default/logs/DebugLogs.t.sol index 59bb2a188..3e307d173 100644 --- a/testdata/default/logs/DebugLogs.t.sol +++ b/testdata/default/logs/DebugLogs.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/logs/HardhatLogs.t.sol b/testdata/default/logs/HardhatLogs.t.sol index 842a390a4..cc2f2b785 100644 --- a/testdata/default/logs/HardhatLogs.t.sol +++ b/testdata/default/logs/HardhatLogs.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "./console.sol"; diff --git a/testdata/default/logs/console.sol b/testdata/default/logs/console.sol index f67c10bfa..feed58fb3 100644 --- a/testdata/default/logs/console.sol +++ b/testdata/default/logs/console.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity 0.8; library console { address constant CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67); diff --git a/testdata/default/repros/Issue1543.t.sol b/testdata/default/repros/Issue1543.t.sol index e8b4806ed..e58f331c4 100644 --- a/testdata/default/repros/Issue1543.t.sol +++ b/testdata/default/repros/Issue1543.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/repros/Issue2623.t.sol b/testdata/default/repros/Issue2623.t.sol index 1d1c2b35b..31252cae3 100644 --- a/testdata/default/repros/Issue2623.t.sol +++ b/testdata/default/repros/Issue2623.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue2629.t.sol b/testdata/default/repros/Issue2629.t.sol index ffff50722..d46868903 100644 --- a/testdata/default/repros/Issue2629.t.sol +++ b/testdata/default/repros/Issue2629.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue2723.t.sol b/testdata/default/repros/Issue2723.t.sol index 70e522296..b7678df45 100644 --- a/testdata/default/repros/Issue2723.t.sol +++ b/testdata/default/repros/Issue2723.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue2898.t.sol b/testdata/default/repros/Issue2898.t.sol index 23de35bcd..a16adf5a3 100644 --- a/testdata/default/repros/Issue2898.t.sol +++ b/testdata/default/repros/Issue2898.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue2956.t.sol b/testdata/default/repros/Issue2956.t.sol index b69d17fb3..c57b46cc1 100644 --- a/testdata/default/repros/Issue2956.t.sol +++ b/testdata/default/repros/Issue2956.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue2984.t.sol b/testdata/default/repros/Issue2984.t.sol index 1a181ad53..fbcd1ab8c 100644 --- a/testdata/default/repros/Issue2984.t.sol +++ b/testdata/default/repros/Issue2984.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -12,11 +12,11 @@ contract Issue2984Test is DSTest { function setUp() public { fork = vm.createSelectFork("avaxTestnet", 12880747); - snapshot = vm.snapshot(); + snapshot = vm.snapshotState(); } function testForkRevertSnapshot() public { - vm.revertTo(snapshot); + vm.revertToState(snapshot); } function testForkSelectSnapshot() public { diff --git a/testdata/default/repros/Issue3055.t.sol b/testdata/default/repros/Issue3055.t.sol index cacf5282f..90ac8c3b0 100644 --- a/testdata/default/repros/Issue3055.t.sol +++ b/testdata/default/repros/Issue3055.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -9,15 +9,15 @@ contract Issue3055Test is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); function test_snapshot() external { - uint256 snapId = vm.snapshot(); + uint256 snapshotId = vm.snapshotState(); assertEq(uint256(0), uint256(1)); - vm.revertTo(snapId); + vm.revertToState(snapshotId); } function test_snapshot2() public { - uint256 snapshot = vm.snapshot(); + uint256 snapshotId = vm.snapshotState(); assertTrue(false); - vm.revertTo(snapshot); + vm.revertToState(snapshotId); assertTrue(true); } @@ -29,8 +29,8 @@ contract Issue3055Test is DSTest { } function exposed_snapshot3() public { - uint256 snapshot = vm.snapshot(); + uint256 snapshotId = vm.snapshotState(); assertTrue(false); - vm.revertTo(snapshot); + vm.revertToState(snapshotId); } } diff --git a/testdata/default/repros/Issue3077.t.sol b/testdata/default/repros/Issue3077.t.sol index b67316294..3b5e4257a 100644 --- a/testdata/default/repros/Issue3077.t.sol +++ b/testdata/default/repros/Issue3077.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3110.t.sol b/testdata/default/repros/Issue3110.t.sol index f9ca984bd..9f1da8d03 100644 --- a/testdata/default/repros/Issue3110.t.sol +++ b/testdata/default/repros/Issue3110.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3119.t.sol b/testdata/default/repros/Issue3119.t.sol index 3e82985c2..6c0ceb429 100644 --- a/testdata/default/repros/Issue3119.t.sol +++ b/testdata/default/repros/Issue3119.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3189.t.sol b/testdata/default/repros/Issue3189.t.sol index 27ea0ac51..0bcf5ddce 100644 --- a/testdata/default/repros/Issue3189.t.sol +++ b/testdata/default/repros/Issue3189.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3190.t.sol b/testdata/default/repros/Issue3190.t.sol index 4a9add5f5..ede3e50e2 100644 --- a/testdata/default/repros/Issue3190.t.sol +++ b/testdata/default/repros/Issue3190.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3192.t.sol b/testdata/default/repros/Issue3192.t.sol index 1f693b1aa..9c5be8d89 100644 --- a/testdata/default/repros/Issue3192.t.sol +++ b/testdata/default/repros/Issue3192.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3220.t.sol b/testdata/default/repros/Issue3220.t.sol index 30e75027a..5235e44c7 100644 --- a/testdata/default/repros/Issue3220.t.sol +++ b/testdata/default/repros/Issue3220.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3221.t.sol b/testdata/default/repros/Issue3221.t.sol index 628f9d0e1..81398c41f 100644 --- a/testdata/default/repros/Issue3221.t.sol +++ b/testdata/default/repros/Issue3221.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3223.t.sol b/testdata/default/repros/Issue3223.t.sol index cd43cb8ef..6c21b7b3d 100644 --- a/testdata/default/repros/Issue3223.t.sol +++ b/testdata/default/repros/Issue3223.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3347.t.sol b/testdata/default/repros/Issue3347.t.sol index ed9be5f36..e48c1305d 100644 --- a/testdata/default/repros/Issue3347.t.sol +++ b/testdata/default/repros/Issue3347.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3596.t.sol b/testdata/default/repros/Issue3596.t.sol index 04ee470d7..b0c678587 100644 --- a/testdata/default/repros/Issue3596.t.sol +++ b/testdata/default/repros/Issue3596.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3653.t.sol b/testdata/default/repros/Issue3653.t.sol index 8cfcc2d0e..26eb38e4a 100644 --- a/testdata/default/repros/Issue3653.t.sol +++ b/testdata/default/repros/Issue3653.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3661.t.sol b/testdata/default/repros/Issue3661.t.sol index f141a19ab..76b55a222 100644 --- a/testdata/default/repros/Issue3661.t.sol +++ b/testdata/default/repros/Issue3661.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/repros/Issue3674.t.sol b/testdata/default/repros/Issue3674.t.sol index 0b680342a..de5a96005 100644 --- a/testdata/default/repros/Issue3674.t.sol +++ b/testdata/default/repros/Issue3674.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3685.t.sol b/testdata/default/repros/Issue3685.t.sol index 7e8f886d8..f1da5bf69 100644 --- a/testdata/default/repros/Issue3685.t.sol +++ b/testdata/default/repros/Issue3685.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3703.t.sol b/testdata/default/repros/Issue3703.t.sol index b6dc39f26..48651be24 100644 --- a/testdata/default/repros/Issue3703.t.sol +++ b/testdata/default/repros/Issue3703.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3708.t.sol b/testdata/default/repros/Issue3708.t.sol index f5bdf48bf..1e9a337f1 100644 --- a/testdata/default/repros/Issue3708.t.sol +++ b/testdata/default/repros/Issue3708.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3753.t.sol b/testdata/default/repros/Issue3753.t.sol index 7af774baf..2c927c823 100644 --- a/testdata/default/repros/Issue3753.t.sol +++ b/testdata/default/repros/Issue3753.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue3792.t.sol b/testdata/default/repros/Issue3792.t.sol index 723329f93..37f62bc61 100644 --- a/testdata/default/repros/Issue3792.t.sol +++ b/testdata/default/repros/Issue3792.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -16,10 +16,10 @@ contract TestSetup is Config, DSTest { // We now check for keccak256("failed") on the hevm address. // This test should succeed. function testSnapshotStorageShift() public { - uint256 snapshotId = vm.snapshot(); + uint256 snapshotId = vm.snapshotState(); vm.prank(test); - vm.revertTo(snapshotId); + vm.revertToState(snapshotId); } } diff --git a/testdata/default/repros/Issue4402.t.sol b/testdata/default/repros/Issue4402.t.sol index 3bf0f33fb..830b2926e 100644 --- a/testdata/default/repros/Issue4402.t.sol +++ b/testdata/default/repros/Issue4402.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue4586.t.sol b/testdata/default/repros/Issue4586.t.sol index 29284ee1b..c904af1e4 100644 --- a/testdata/default/repros/Issue4586.t.sol +++ b/testdata/default/repros/Issue4586.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue4630.t.sol b/testdata/default/repros/Issue4630.t.sol index 4b9fe9c9b..01eb62650 100644 --- a/testdata/default/repros/Issue4630.t.sol +++ b/testdata/default/repros/Issue4630.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue4640.t.sol b/testdata/default/repros/Issue4640.t.sol index eaa87c12c..1e7d887a9 100644 --- a/testdata/default/repros/Issue4640.t.sol +++ b/testdata/default/repros/Issue4640.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5038.t.sol b/testdata/default/repros/Issue5038.t.sol index 834f82783..51a90bca1 100644 --- a/testdata/default/repros/Issue5038.t.sol +++ b/testdata/default/repros/Issue5038.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5739.t.sol b/testdata/default/repros/Issue5739.t.sol index eafbabd93..6f3494b7e 100644 --- a/testdata/default/repros/Issue5739.t.sol +++ b/testdata/default/repros/Issue5739.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5808.t.sol b/testdata/default/repros/Issue5808.t.sol index 66ea82b30..40efe65a9 100644 --- a/testdata/default/repros/Issue5808.t.sol +++ b/testdata/default/repros/Issue5808.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5929.t.sol b/testdata/default/repros/Issue5929.t.sol index cce676d25..70c5a4f4f 100644 --- a/testdata/default/repros/Issue5929.t.sol +++ b/testdata/default/repros/Issue5929.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5935.t.sol b/testdata/default/repros/Issue5935.t.sol index f783d12da..8ef724412 100644 --- a/testdata/default/repros/Issue5935.t.sol +++ b/testdata/default/repros/Issue5935.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue5948.t.sol b/testdata/default/repros/Issue5948.t.sol index 992099fb1..ae6ee9d50 100644 --- a/testdata/default/repros/Issue5948.t.sol +++ b/testdata/default/repros/Issue5948.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6006.t.sol b/testdata/default/repros/Issue6006.t.sol index dac37cd24..54f0d1137 100644 --- a/testdata/default/repros/Issue6006.t.sol +++ b/testdata/default/repros/Issue6006.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6032.t.sol b/testdata/default/repros/Issue6032.t.sol index 75002a136..2fa05222d 100644 --- a/testdata/default/repros/Issue6032.t.sol +++ b/testdata/default/repros/Issue6032.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6070.t.sol b/testdata/default/repros/Issue6070.t.sol index e699f5ca9..ebf3c7ab1 100644 --- a/testdata/default/repros/Issue6070.t.sol +++ b/testdata/default/repros/Issue6070.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6115.t.sol b/testdata/default/repros/Issue6115.t.sol index 65c5cdaa7..ae65a7dae 100644 --- a/testdata/default/repros/Issue6115.t.sol +++ b/testdata/default/repros/Issue6115.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/repros/Issue6170.t.sol b/testdata/default/repros/Issue6170.t.sol index 543ca3142..78511f4a2 100644 --- a/testdata/default/repros/Issue6170.t.sol +++ b/testdata/default/repros/Issue6170.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6180.t.sol b/testdata/default/repros/Issue6180.t.sol index 591c60bdf..3d08ccbeb 100644 --- a/testdata/default/repros/Issue6180.t.sol +++ b/testdata/default/repros/Issue6180.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6293.t.sol b/testdata/default/repros/Issue6293.t.sol index 303e8fbbe..6d57d1385 100644 --- a/testdata/default/repros/Issue6293.t.sol +++ b/testdata/default/repros/Issue6293.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6355.t.sol b/testdata/default/repros/Issue6355.t.sol index d7830152a..bbc3a4a98 100644 --- a/testdata/default/repros/Issue6355.t.sol +++ b/testdata/default/repros/Issue6355.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -7,11 +7,11 @@ import "cheats/Vm.sol"; // https://github.com/foundry-rs/foundry/issues/6355 contract Issue6355Test is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); - uint256 snapshot; + uint256 snapshotId; Target targ; function setUp() public { - snapshot = vm.snapshot(); + snapshotId = vm.snapshotState(); targ = new Target(); } @@ -21,9 +21,9 @@ contract Issue6355Test is DSTest { } // always fails - function test_shouldFailWithRevertTo() public { + function test_shouldFailWithRevertToState() public { assertEq(3, targ.num()); - vm.revertTo(snapshot); + vm.revertToState(snapshotId); } // always fails diff --git a/testdata/default/repros/Issue6437.t.sol b/testdata/default/repros/Issue6437.t.sol index c18af2dfd..4cf27be7b 100644 --- a/testdata/default/repros/Issue6437.t.sol +++ b/testdata/default/repros/Issue6437.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6501.t.sol b/testdata/default/repros/Issue6501.t.sol index 392f0f1ab..5d631cbe3 100644 --- a/testdata/default/repros/Issue6501.t.sol +++ b/testdata/default/repros/Issue6501.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "../logs/console.sol"; diff --git a/testdata/default/repros/Issue6538.t.sol b/testdata/default/repros/Issue6538.t.sol index 2b8beb578..5b318a04c 100644 --- a/testdata/default/repros/Issue6538.t.sol +++ b/testdata/default/repros/Issue6538.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6554.t.sol b/testdata/default/repros/Issue6554.t.sol index c13ebc4a7..7a5fe7879 100644 --- a/testdata/default/repros/Issue6554.t.sol +++ b/testdata/default/repros/Issue6554.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6616.t.sol b/testdata/default/repros/Issue6616.t.sol index f31a79ee6..262721d86 100644 --- a/testdata/default/repros/Issue6616.t.sol +++ b/testdata/default/repros/Issue6616.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6634.t.sol b/testdata/default/repros/Issue6634.t.sol index 22294f6df..aa94922dd 100644 --- a/testdata/default/repros/Issue6634.t.sol +++ b/testdata/default/repros/Issue6634.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; @@ -58,7 +58,7 @@ contract Issue6634Test is DSTest { assertEq(called.length, 2, "incorrect length"); assertEq(uint256(called[0].kind), uint256(Vm.AccountAccessKind.Call), "first AccountAccess is incorrect kind"); - assertEq(called[0].account, CREATE2_DEPLOYER, "first AccountAccess accout is incorrect"); + assertEq(called[0].account, CREATE2_DEPLOYER, "first AccountAccess account is incorrect"); assertEq(called[0].accessor, accessor, "first AccountAccess accessor is incorrect"); assertEq( uint256(called[1].kind), uint256(Vm.AccountAccessKind.Create), "second AccountAccess is incorrect kind" @@ -84,7 +84,7 @@ contract Issue6634Test is DSTest { assertEq(called.length, 2, "incorrect length"); assertEq(uint256(called[0].kind), uint256(Vm.AccountAccessKind.Call), "first AccountAccess is incorrect kind"); - assertEq(called[0].account, CREATE2_DEPLOYER, "first AccountAccess accout is incorrect"); + assertEq(called[0].account, CREATE2_DEPLOYER, "first AccountAccess account is incorrect"); assertEq(called[0].accessor, accessor, "first AccountAccess accessor is incorrect"); assertEq( uint256(called[1].kind), uint256(Vm.AccountAccessKind.Create), "second AccountAccess is incorrect kind" diff --git a/testdata/default/repros/Issue6643.t.sol b/testdata/default/repros/Issue6643.t.sol index 36b684c13..5c7e1c483 100644 --- a/testdata/default/repros/Issue6643.t.sol +++ b/testdata/default/repros/Issue6643.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6759.t.sol b/testdata/default/repros/Issue6759.t.sol index e528c63e1..ffdcb8893 100644 --- a/testdata/default/repros/Issue6759.t.sol +++ b/testdata/default/repros/Issue6759.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue6966.t.sol b/testdata/default/repros/Issue6966.t.sol index f7a8db700..7e35a869e 100644 --- a/testdata/default/repros/Issue6966.t.sol +++ b/testdata/default/repros/Issue6966.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; diff --git a/testdata/default/repros/Issue7457.t.sol b/testdata/default/repros/Issue7457.t.sol index 3c4080df2..1836c4825 100644 --- a/testdata/default/repros/Issue7457.t.sol +++ b/testdata/default/repros/Issue7457.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue7481.t.sol b/testdata/default/repros/Issue7481.t.sol index 441758b4f..46b24b1d5 100644 --- a/testdata/default/repros/Issue7481.t.sol +++ b/testdata/default/repros/Issue7481.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8004.t.sol b/testdata/default/repros/Issue8004.t.sol index 229dde5ad..278aa1212 100644 --- a/testdata/default/repros/Issue8004.t.sol +++ b/testdata/default/repros/Issue8004.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8006.t.sol b/testdata/default/repros/Issue8006.t.sol index 078117d40..95b16e6f6 100644 --- a/testdata/default/repros/Issue8006.t.sol +++ b/testdata/default/repros/Issue8006.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8168.t.sol b/testdata/default/repros/Issue8168.t.sol index f11cef7bc..9a072ce4b 100644 --- a/testdata/default/repros/Issue8168.t.sol +++ b/testdata/default/repros/Issue8168.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8277.t.sol b/testdata/default/repros/Issue8277.t.sol index aebdbd8ff..48a089575 100644 --- a/testdata/default/repros/Issue8277.t.sol +++ b/testdata/default/repros/Issue8277.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8287.t.sol b/testdata/default/repros/Issue8287.t.sol index 86901c0fd..d1e372bda 100644 --- a/testdata/default/repros/Issue8287.t.sol +++ b/testdata/default/repros/Issue8287.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8383.t.sol b/testdata/default/repros/Issue8383.t.sol index a002b4b3d..339f5b518 100644 --- a/testdata/default/repros/Issue8383.t.sol +++ b/testdata/default/repros/Issue8383.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/repros/Issue8971.t.sol b/testdata/default/repros/Issue8971.t.sol new file mode 100644 index 000000000..37861b483 --- /dev/null +++ b/testdata/default/repros/Issue8971.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract Counter { + uint256 public number; + + function increment() public { + number++; + } +} + +/// @notice Test is mostly related to --isolate. Ensures that state is not affected by reverted +/// call to handler. +contract Handler { + bool public locked; + Counter public counter = new Counter(); + + function doNothing() public {} + + function doSomething() public { + locked = true; + counter.increment(); + this.doRevert(); + } + + function doRevert() public { + revert(); + } +} + +contract Invariant is DSTest { + Handler h; + + function setUp() public { + h = new Handler(); + } + + function targetContracts() public view returns (address[] memory contracts) { + contracts = new address[](1); + contracts[0] = address(h); + } + + function invariant_unchanged() public { + assertEq(h.locked(), false); + assertEq(h.counter().number(), 0); + } +} diff --git a/testdata/default/script/deploy.sol b/testdata/default/script/deploy.sol index 013e009d3..7570c706a 100644 --- a/testdata/default/script/deploy.sol +++ b/testdata/default/script/deploy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import {DSTest} from "lib/ds-test/src/test.sol"; import {Vm} from "cheats/Vm.sol"; diff --git a/testdata/default/spec/ShanghaiCompat.t.sol b/testdata/default/spec/ShanghaiCompat.t.sol index 02856a88f..fd7213b3d 100644 --- a/testdata/default/spec/ShanghaiCompat.t.sol +++ b/testdata/default/spec/ShanghaiCompat.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/trace/ConflictingSignatures.t.sol b/testdata/default/trace/ConflictingSignatures.t.sol index 67dfd5d3a..c8b7066c7 100644 --- a/testdata/default/trace/ConflictingSignatures.t.sol +++ b/testdata/default/trace/ConflictingSignatures.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/default/trace/Trace.t.sol b/testdata/default/trace/Trace.t.sol index d513e8637..19af6dd7c 100644 --- a/testdata/default/trace/Trace.t.sol +++ b/testdata/default/trace/Trace.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/fixtures/GetCode/UnlinkedContract.sol b/testdata/fixtures/GetCode/UnlinkedContract.sol index 41f0b0d76..93c6a8e26 100644 --- a/testdata/fixtures/GetCode/UnlinkedContract.sol +++ b/testdata/fixtures/GetCode/UnlinkedContract.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; library SmolLibrary { function add(uint256 a, uint256 b) public pure returns (uint256 c) { diff --git a/testdata/fixtures/GetCode/WorkingContract.sol b/testdata/fixtures/GetCode/WorkingContract.sol index 3ea502055..7f4cb79af 100644 --- a/testdata/fixtures/GetCode/WorkingContract.sol +++ b/testdata/fixtures/GetCode/WorkingContract.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity ^0.8.18; contract WorkingContract { uint256 public constant secret = 42; diff --git a/testdata/fixtures/Toml/nested_toml_struct.toml b/testdata/fixtures/Toml/nested_toml_struct.toml new file mode 100644 index 000000000..3cef0b7ba --- /dev/null +++ b/testdata/fixtures/Toml/nested_toml_struct.toml @@ -0,0 +1,23 @@ +name = "test" + +[[members]] +a = 100 +arr = [ + [1, -2, -5], + [1000, 2000, 0] +] +str = "some string" +b = "0x" +addr = "0x0000000000000000000000000000000000000000" +fixedBytes = "0x8ae3fc6bd1b150a73ec4afe3ef136fa2f88e9c96131c883c5e4a4714811c1598" + +[[members]] +a = 200 +arr = [] +str = "some other string" +b = "0x0000000000000000000000000000000000000000" +addr = "0x167D91deaEEE3021161502873d3bcc6291081648" +fixedBytes = "0xed1c7beb1f00feaaaec5636950d6edb25a8d4fedc8deb2711287b64c4d27719d" + +[inner] +fixedBytes = "0x12345678" diff --git a/testdata/fixtures/Toml/test.toml b/testdata/fixtures/Toml/test.toml index ce735b8f1..806dc2224 100644 --- a/testdata/fixtures/Toml/test.toml +++ b/testdata/fixtures/Toml/test.toml @@ -43,8 +43,8 @@ bytesStringArray = ["0x01", "0x02"] number = 9223372036854775807 # TOML is limited to 64-bit integers str = "NEST" -[[advancedJsonPath]] +[[advancedTomlPath]] id = 1 -[[advancedJsonPath]] +[[advancedTomlPath]] id = 2 diff --git a/testdata/forge-std-rev b/testdata/forge-std-rev index 239d2091e..c6c6e2da6 100644 --- a/testdata/forge-std-rev +++ b/testdata/forge-std-rev @@ -1 +1 @@ -1714bee72e286e73f76e320d110e0eaf5c4e649d \ No newline at end of file +8f24d6b04c92975e0795b5868aa0d783251cdeaa \ No newline at end of file diff --git a/testdata/multi-version/cheats/GetCode.t.sol b/testdata/multi-version/cheats/GetCode.t.sol index e4a7bd14a..72dae24e6 100644 --- a/testdata/multi-version/cheats/GetCode.t.sol +++ b/testdata/multi-version/cheats/GetCode.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.18; +pragma solidity =0.8.18; import "ds-test/test.sol"; import "cheats/Vm.sol"; diff --git a/testdata/multi-version/cheats/GetCode17.t.sol b/testdata/multi-version/cheats/GetCode17.t.sol index 068a910cf..f8bf4bb2a 100644 --- a/testdata/multi-version/cheats/GetCode17.t.sol +++ b/testdata/multi-version/cheats/GetCode17.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity 0.8.17; +pragma solidity =0.8.17; import "ds-test/test.sol"; import "cheats/Vm.sol";