From 63e043e88c99e6af59cb112aeb2af4932716e059 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:04:41 +0100 Subject: [PATCH] refactor: rewrite revert and precompile decoding (#6222) * wip * rm ugly ass brute force decoding * improve decoding * decode precompiles * chore: rewrite hardhat console patcher * fixes * chore: clippy * further * addtest --- Cargo.lock | 138 ++++--- crates/anvil/src/eth/backend/mem/mod.rs | 69 +--- crates/cast/Cargo.toml | 1 - crates/config/src/utils.rs | 2 +- crates/evm/core/Cargo.toml | 2 +- crates/evm/core/src/abi/mod.rs | 32 +- crates/evm/core/src/backend/mod.rs | 1 - crates/evm/core/src/debug.rs | 11 +- crates/evm/core/src/decode.rs | 358 +++++------------- crates/evm/evm/src/executors/fuzz/mod.rs | 4 +- .../evm/evm/src/executors/invariant/error.rs | 12 +- crates/evm/evm/src/executors/mod.rs | 10 +- crates/evm/evm/src/inspectors/access_list.rs | 2 + crates/evm/evm/src/inspectors/debugger.rs | 27 +- crates/evm/traces/Cargo.toml | 3 +- .../traces/src/{decoder.rs => decoder/mod.rs} | 107 ++---- crates/evm/traces/src/decoder/precompiles.rs | 204 ++++++++++ crates/evm/traces/src/inspector.rs | 8 +- crates/evm/traces/src/lib.rs | 166 ++++---- crates/evm/traces/src/node.rs | 49 ++- crates/evm/traces/src/utils.rs | 4 +- crates/forge/bin/cmd/script/executor.rs | 2 +- crates/forge/bin/cmd/script/mod.rs | 16 +- crates/forge/src/gas_report.rs | 13 +- crates/forge/src/runner.rs | 4 +- crates/forge/tests/cli/script.rs | 2 +- crates/forge/tests/it/core.rs | 4 +- crates/forge/tests/it/invariant.rs | 48 ++- crates/test-utils/src/script.rs | 6 +- .../common/InvariantInnerContract.t.sol | 2 +- .../common/InvariantReentrancy.t.sol | 2 +- .../invariant/common/InvariantTest1.t.sol | 4 +- .../invariant/target/ExcludeContracts.t.sol | 2 +- .../invariant/target/ExcludeSenders.t.sol | 2 +- .../invariant/target/TargetContracts.t.sol | 2 +- .../invariant/target/TargetInterfaces.t.sol | 2 +- .../invariant/target/TargetSelectors.t.sol | 2 +- .../fuzz/invariant/target/TargetSenders.t.sol | 2 +- .../targetAbi/ExcludeArtifacts.t.sol | 2 +- .../targetAbi/TargetArtifactSelectors.t.sol | 2 +- .../targetAbi/TargetArtifactSelectors2.t.sol | 2 +- .../invariant/targetAbi/TargetArtifacts.t.sol | 4 +- 42 files changed, 652 insertions(+), 683 deletions(-) rename crates/evm/traces/src/{decoder.rs => decoder/mod.rs} (73%) create mode 100644 crates/evm/traces/src/decoder/precompiles.rs diff --git a/Cargo.lock b/Cargo.lock index 6ab0577591a5..431560f339b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "alloy-dyn-abi" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "alloy-json-abi" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -110,7 +110,7 @@ dependencies = [ [[package]] name = "alloy-primitives" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "alloy-rlp", "arbitrary", @@ -150,13 +150,13 @@ checksum = "c0391754c09fab4eae3404d19d0d297aa1c670c1775ab51d8a5312afeca23157" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "alloy-sol-macro" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "const-hex", "dunce", @@ -165,7 +165,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "syn-solidity", "tiny-keccak", ] @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "winnow", ] @@ -181,7 +181,7 @@ dependencies = [ [[package]] name = "alloy-sol-types" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "alloy-primitives", "alloy-sol-macro", @@ -550,7 +550,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -561,7 +561,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -722,7 +722,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.38", + "syn 2.0.39", "which", ] @@ -1173,7 +1173,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1664,7 +1664,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1905,7 +1905,7 @@ checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2092,8 +2092,8 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.38", - "toml 0.8.6", + "syn 2.0.39", + "toml 0.8.8", "walkdir", ] @@ -2109,7 +2109,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2134,7 +2134,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.38", + "syn 2.0.39", "tempfile", "thiserror", "tiny-keccak", @@ -2339,7 +2339,7 @@ dependencies = [ "bytes", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2394,7 +2394,7 @@ dependencies = [ "pear", "serde", "tempfile", - "toml 0.8.6", + "toml 0.8.8", "uncased", "version_check", ] @@ -2541,7 +2541,7 @@ dependencies = [ "solang-parser", "thiserror", "tokio", - "toml 0.8.6", + "toml 0.8.8", "tracing", "warp", ] @@ -2557,7 +2557,7 @@ dependencies = [ "pretty_assertions", "solang-parser", "thiserror", - "toml 0.8.6", + "toml 0.8.8", "tracing", "tracing-subscriber", ] @@ -2581,7 +2581,7 @@ dependencies = [ "ethers-providers", "eyre", "foundry-macros", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2731,7 +2731,7 @@ dependencies = [ [[package]] name = "foundry-compilers" version = "0.1.0" -source = "git+https://github.com/foundry-rs/compilers#a4692f5339f4adb39d3598ec8935ac5f125ed675" +source = "git+https://github.com/foundry-rs/compilers#f00eef81a91a393bf92eb13d47a5221f5e50162f" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -2793,7 +2793,7 @@ dependencies = [ "serde_regex", "tempfile", "thiserror", - "toml 0.8.6", + "toml 0.8.8", "toml_edit 0.20.7", "tracing", "walkdir", @@ -2849,11 +2849,11 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-types", - "auto_impl", "const-hex", "ethers", "eyre", "foundry-abi", + "foundry-cheatcodes-defs", "foundry-common", "foundry-compilers", "foundry-config", @@ -2918,6 +2918,7 @@ dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-primitives", + "alloy-sol-types", "const-hex", "ethers", "eyre", @@ -2957,7 +2958,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3115,7 +3116,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4158,9 +4159,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgit2-sys" @@ -4407,7 +4408,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4668,7 +4669,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4772,7 +4773,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4973,7 +4974,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5028,7 +5029,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5131,7 +5132,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5169,7 +5170,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5269,7 +5270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5346,7 +5347,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "version_check", "yansi 1.0.0-rc.1", ] @@ -6338,22 +6339,22 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6397,7 +6398,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6443,7 +6444,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6736,7 +6737,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6804,9 +6805,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -6816,12 +6817,12 @@ dependencies = [ [[package]] name = "syn-solidity" version = "0.4.2" -source = "git+https://github.com/alloy-rs/core/#e76158bb83644e042c4e55c4a382aeb389ae748b" +source = "git+https://github.com/alloy-rs/core/#818071e6f20cec5c0b7fb465fda5e0553b2e0ed4" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6953,7 +6954,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -7067,7 +7068,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -7166,15 +7167,15 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.7", + "toml_edit 0.21.0", ] [[package]] @@ -7202,6 +7203,17 @@ name = "toml_edit" version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.1.0", "serde", @@ -7302,7 +7314,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -7609,9 +7621,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "8.2.5" +version = "8.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e7dc29b3c54a2ea67ef4f953d5ec0c4085035c0ae2d325be1c0d2144bd9f16" +checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" dependencies = [ "anyhow", "git2", @@ -7716,7 +7728,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -7750,7 +7762,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8110,7 +8122,7 @@ checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -8130,7 +8142,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 5fddfe445abb..d313f1dd8172 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -53,19 +53,18 @@ use ethers::{ DefaultFrame, Filter, FilteredParams, GethDebugTracingOptions, GethTrace, Log, OtherFields, Trace, Transaction, TransactionReceipt, H160, }, - utils::{hex, keccak256, rlp}, + utils::{keccak256, rlp}, }; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; -use foundry_common::fmt::format_token; use foundry_evm::{ backend::{DatabaseError, DatabaseResult}, constants::DEFAULT_CREATE2_DEPLOYER_RUNTIME_CODE, - decode::{decode_custom_error_args, decode_revert}, + decode::decode_revert, inspectors::AccessListTracer, revm::{ self, db::CacheDB, - interpreter::{return_ok, InstructionResult}, + interpreter::InstructionResult, primitives::{ Account, BlockEnv, CreateScheme, EVMError, Env, ExecutionResult, InvalidHeader, Output, SpecId, TransactTo, TxEnv, KECCAK_EMPTY, @@ -285,7 +284,7 @@ impl Backend { fork_genesis_infos.clear(); for res in genesis_accounts { - let (address, mut info) = res??; + let (address, mut info) = res.map_err(DatabaseError::display)??; info.balance = self.genesis.balance; db.insert_account(address, info.clone()); @@ -910,54 +909,20 @@ impl Backend { // insert all transactions for (info, receipt) in transactions.into_iter().zip(receipts) { // log some tx info - { - node_info!(" Transaction: {:?}", info.transaction_hash); - if let Some(ref contract) = info.contract_address { - node_info!(" Contract created: {:?}", contract); - } - node_info!(" Gas used: {}", receipt.gas_used()); - match info.exit { - return_ok!() => (), - InstructionResult::OutOfFund => { - node_info!(" Error: reverted due to running out of funds"); - } - InstructionResult::CallTooDeep => { - node_info!(" Error: reverted with call too deep"); - } - InstructionResult::Revert => { - if let Some(ref r) = info.out { - if let Ok(reason) = decode_revert(r.as_ref(), None, None) { - node_info!(" Error: reverted with '{}'", reason); - } else { - match decode_custom_error_args(r, 5) { - // assuming max 5 args - Some(token) => { - node_info!( - " Error: reverted with custom error: {:?}", - format_token(&token) - ); - } - None => { - node_info!( - " Error: reverted with custom error: {}", - hex::encode(r) - ); - } - } - } - } else { - node_info!(" Error: reverted without a reason"); - } - } - InstructionResult::OutOfGas => { - node_info!(" Error: ran out of gas"); - } - reason => { - node_info!(" Error: failed due to {:?}", reason); - } - } - node_info!(""); + node_info!(" Transaction: {:?}", info.transaction_hash); + if let Some(contract) = &info.contract_address { + node_info!(" Contract created: {contract:?}"); + } + node_info!(" Gas used: {}", receipt.gas_used()); + if !info.exit.is_ok() { + let r = decode_revert( + info.out.as_deref().unwrap_or_default(), + None, + Some(info.exit), + ); + node_info!(" Error: reverted with: {r}"); } + node_info!(""); let mined_tx = MinedTransaction { info, diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index e2dfb5358d55..43cf2497fd4b 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -35,7 +35,6 @@ alloy-dyn-abi.workspace = true foundry-compilers = { workspace = true, default-features = false } foundry-block-explorers = { workspace = true } - chrono.workspace = true evm-disassembler = "0.3" eyre.workspace = true diff --git a/crates/config/src/utils.rs b/crates/config/src/utils.rs index e3c845b6b3c3..8fb3289227c6 100644 --- a/crates/config/src/utils.rs +++ b/crates/config/src/utils.rs @@ -203,7 +203,7 @@ pub fn get_available_profiles(toml_path: impl AsRef) -> eyre::Result) { - if input.len() < 4 { - return - } - let selector = unsafe { &mut *(input.get_unchecked_mut(..4) as *mut [u8] as *mut [u8; 4]) }; - if let Some(abigen_selector) = HARDHAT_CONSOLE_SELECTOR_PATCHES.get(selector) { - *selector = *abigen_selector; +pub fn patch_hardhat_console_selector(input: &mut [u8]) { + if let Some(selector) = input.get_mut(..4) { + let selector: &mut [u8; 4] = selector.try_into().unwrap(); + if let Some(generated_selector) = HARDHAT_CONSOLE_SELECTOR_PATCHES.get(selector) { + *selector = *generated_selector; + } } } @@ -27,7 +27,7 @@ pub fn patch_hardhat_console_selector(input: &mut Vec) { /// bindings which `abigen!` creates. `hardhat/console.log` logs its events in functions that accept /// `uint` manually as `abi.encodeWithSignature("log(int)", p0)`, but `abigen!` uses `uint256` for /// its call bindings (`HardhatConsoleCalls`) as generated by solc. -pub static HARDHAT_CONSOLE_SELECTOR_PATCHES: Lazy> = Lazy::new(|| { +static HARDHAT_CONSOLE_SELECTOR_PATCHES: Lazy> = Lazy::new(|| { HashMap::from([ // log(bool,uint256,uint256,address) ([241, 97, 178, 33], [0, 221, 135, 185]), @@ -545,11 +545,11 @@ mod tests { use super::*; #[test] - fn hardhat_console_path_works() { - for (hh, abigen) in HARDHAT_CONSOLE_SELECTOR_PATCHES.iter() { - let mut hh = (*hh).to_vec(); + fn hardhat_console_patch() { + for (hh, generated) in HARDHAT_CONSOLE_SELECTOR_PATCHES.iter() { + let mut hh = *hh; patch_hardhat_console_selector(&mut hh); - assert_eq!((*abigen).to_vec(), hh); + assert_eq!(hh, *generated); } } } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index be6d855b4659..bdfe0a4c110e 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -69,7 +69,6 @@ const GLOBAL_FAILURE_SLOT: B256 = b256!("6661696c65640000000000000000000000000000000000000000000000000000"); /// An extension trait that allows us to easily extend the `revm::Inspector` capabilities -#[auto_impl::auto_impl(&mut, Box)] pub trait DatabaseExt: Database { /// Creates a new snapshot at the current point of execution. /// diff --git a/crates/evm/core/src/debug.rs b/crates/evm/core/src/debug.rs index cbe1b64b2f7b..3b6cab6167e3 100644 --- a/crates/evm/core/src/debug.rs +++ b/crates/evm/core/src/debug.rs @@ -1,4 +1,4 @@ -use crate::{abi::HEVM_ABI, utils::CallKind}; +use crate::utils::CallKind; use alloy_primitives::{Address, U256}; use revm::interpreter::{Memory, OpCode}; use serde::{Deserialize, Serialize}; @@ -178,11 +178,12 @@ impl Display for Instruction { Instruction::Cheatcode(cheat) => write!( f, "VM_{}", - &*HEVM_ABI - .functions() - .find(|func| func.short_signature() == *cheat) + foundry_cheatcodes_defs::Vm::CHEATCODES + .iter() + .map(|c| &c.func) + .find(|c| c.selector_bytes == *cheat) .expect("unknown cheatcode found in debugger") - .name + .id .to_uppercase() ), } diff --git a/crates/evm/core/src/decode.rs b/crates/evm/core/src/decode.rs index 1d941d94e359..1d66f6286b0a 100644 --- a/crates/evm/core/src/decode.rs +++ b/crates/evm/core/src/decode.rs @@ -1,18 +1,15 @@ //! Various utilities to decode test results. -use crate::constants::MAGIC_SKIP; -use alloy_dyn_abi::{DynSolType, DynSolValue, JsonAbiExt}; +use crate::abi::ConsoleEvents; +use alloy_dyn_abi::JsonAbiExt; use alloy_json_abi::JsonAbi; -use alloy_primitives::{B256, U256}; -use alloy_sol_types::{sol_data::String as SolString, SolType}; +use alloy_primitives::B256; +use alloy_sol_types::{SolCall, SolError, SolInterface, SolValue}; use ethers::{abi::RawLog, contract::EthLogDecode, types::Log}; -use foundry_abi::console::ConsoleEvents::{self, *}; -use foundry_common::{fmt::format_token, SELECTOR_LEN}; -use foundry_utils::error::{ERROR_PREFIX, REVERT_PREFIX}; +use foundry_cheatcodes_defs::Vm; +use foundry_common::SELECTOR_LEN; use itertools::Itertools; -use once_cell::sync::Lazy; -use revm::interpreter::{return_ok, InstructionResult}; -use thiserror::Error; +use revm::interpreter::InstructionResult; /// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log` pub fn decode_console_logs(logs: &[Log]) -> Vec { @@ -24,17 +21,19 @@ pub fn decode_console_logs(logs: &[Log]) -> Vec { /// This function returns [None] if it is not a DSTest log or the result of a Hardhat /// `console.log`. pub fn decode_console_log(log: &Log) -> Option { + use ConsoleEvents as CE; + // NOTE: We need to do this conversion because ethers-rs does not // support passing `Log`s let raw_log = RawLog { topics: log.topics.clone(), data: log.data.to_vec() }; let decoded = match ConsoleEvents::decode_log(&raw_log).ok()? { - LogsFilter(inner) => format!("{}", inner.0), - LogBytesFilter(inner) => format!("{}", inner.0), - LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val), - LogNamedBytes32Filter(inner) => { + CE::LogsFilter(inner) => format!("{}", inner.0), + CE::LogBytesFilter(inner) => format!("{}", inner.0), + CE::LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedBytes32Filter(inner) => { format!("{}: {}", inner.key, B256::new(inner.val)) } - LogNamedDecimalIntFilter(inner) => { + CE::LogNamedDecimalIntFilter(inner) => { let (sign, val) = inner.val.into_sign_and_abs(); format!( "{}: {}{}", @@ -43,300 +42,123 @@ pub fn decode_console_log(log: &Log) -> Option { ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap() ) } - LogNamedDecimalUintFilter(inner) => { + CE::LogNamedDecimalUintFilter(inner) => { format!( "{}: {}", inner.key, ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap() ) } - LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val), - LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val), - LogNamedBytesFilter(inner) => { + CE::LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedBytesFilter(inner) => { format!("{}: {}", inner.key, inner.val) } - LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val), - LogNamedArray1Filter(inner) => format!("{}: {:?}", inner.key, inner.val), - LogNamedArray2Filter(inner) => format!("{}: {:?}", inner.key, inner.val), - LogNamedArray3Filter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val), + CE::LogNamedArray1Filter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedArray2Filter(inner) => format!("{}: {:?}", inner.key, inner.val), + CE::LogNamedArray3Filter(inner) => format!("{}: {:?}", inner.key, inner.val), e => e.to_string(), }; Some(decoded) } -/// Possible errors when decoding a revert error string. -#[derive(Debug, Clone, Error)] -pub enum RevertDecodingError { - #[error("Not enough data to decode")] - InsufficientErrorData, - #[error("Unsupported solidity builtin panic")] - UnsupportedSolidityBuiltinPanic, - #[error("Could not decode slice")] - SliceDecodingError, - #[error("Non-native error and not string")] - NonNativeErrorAndNotString, - #[error("Unknown Error Selector")] - UnknownErrorSelector, - #[error("Could not decode cheatcode string")] - UnknownCheatcodeErrorString, - #[error("Bad String decode")] - BadStringDecode, - #[error(transparent)] - AlloyDecodingError(alloy_dyn_abi::Error), -} - -/// Given an ABI encoded error string with the function signature `Error(string)`, it decodes -/// it and returns the revert error message. +/// Tries to decode an error message from the given revert bytes. +/// +/// Note that this is just a best-effort guess, and should not be relied upon for anything other +/// than user output. pub fn decode_revert( err: &[u8], maybe_abi: Option<&JsonAbi>, status: Option, -) -> Result { +) -> String { if err.len() < SELECTOR_LEN { if let Some(status) = status { - if !matches!(status, return_ok!()) { - return Ok(format!("EvmError: {status:?}")) + if !status.is_ok() { + return format!("EvmError: {status:?}") } } - return Err(RevertDecodingError::InsufficientErrorData) - } - - match <[u8; SELECTOR_LEN]>::try_from(&err[..SELECTOR_LEN]).unwrap() { - // keccak(Panic(uint256)) - [78, 72, 123, 113] => { - // ref: https://soliditydeveloper.com/solidity-0.8 - match err[err.len() - 1] { - 1 => { - // assert - Ok("Assertion violated".to_string()) - } - 17 => { - // safemath over/underflow - Ok("Arithmetic over/underflow".to_string()) - } - 18 => { - // divide by 0 - Ok("Division or modulo by 0".to_string()) - } - 33 => { - // conversion into non-existent enum type - Ok("Conversion into non-existent enum type".to_string()) - } - 34 => { - // incorrectly encoded storage byte array - Ok("Incorrectly encoded storage byte array".to_string()) - } - 49 => { - // pop() on empty array - Ok("`pop()` on empty array".to_string()) - } - 50 => { - // index out of bounds - Ok("Index out of bounds".to_string()) - } - 65 => { - // allocating too much memory or creating too large array - Ok("Memory allocation overflow".to_string()) - } - 81 => { - // calling a zero initialized variable of internal function type - Ok("Calling a zero initialized variable of internal function type".to_string()) - } - _ => Err(RevertDecodingError::UnsupportedSolidityBuiltinPanic), - } - } - // keccak(Error(string)) | keccak(CheatcodeError(string)) - REVERT_PREFIX | ERROR_PREFIX => { - DynSolType::abi_decode(&DynSolType::String, &err[SELECTOR_LEN..]) - .map_err(RevertDecodingError::AlloyDecodingError) - .and_then(|v| { - v.clone() - .as_str() - .map(|s| s.to_owned()) - .ok_or(RevertDecodingError::BadStringDecode) - }) - .to_owned() - } - // keccak(expectRevert(bytes)) - [242, 141, 206, 179] => { - let err_data = &err[SELECTOR_LEN..]; - if err_data.len() > 64 { - let len = U256::try_from_be_slice(&err_data[32..64]) - .ok_or(RevertDecodingError::SliceDecodingError)? - .to::(); - if err_data.len() > 64 + len { - let actual_err = &err_data[64..64 + len]; - if let Ok(decoded) = decode_revert(actual_err, maybe_abi, None) { - // check if it's a builtin - return Ok(decoded) - } else if let Ok(as_str) = String::from_utf8(actual_err.to_vec()) { - // check if it's a true string - return Ok(as_str) - } - } - } - Err(RevertDecodingError::NonNativeErrorAndNotString) - } - // keccak(expectRevert(bytes4)) - [195, 30, 176, 224] => { - let err_data = &err[SELECTOR_LEN..]; - if err_data.len() == 32 { - let actual_err = &err_data[..SELECTOR_LEN]; - if let Ok(decoded) = decode_revert(actual_err, maybe_abi, None) { - // it's a known selector - return Ok(decoded) - } - } - Err(RevertDecodingError::UnknownErrorSelector) - } - _ => { - // See if the revert is caused by a skip() call. - if err == MAGIC_SKIP { - return Ok("SKIPPED".to_string()) - } - // try to decode a custom error if provided an abi - if let Some(abi) = maybe_abi { - for abi_error in abi.errors() { - if abi_error.selector() == err[..SELECTOR_LEN] { - // if we don't decode, don't return an error, try to decode as a - // string later - if let Ok(decoded) = abi_error.abi_decode_input(&err[SELECTOR_LEN..], false) - { - let inputs = decoded - .iter() - .map(foundry_common::fmt::format_token) - .collect::>() - .join(", "); - return Ok(format!("{}({inputs})", abi_error.name)) - } - } - } - } - - // optimistically try to decode as string, unknown selector or `CheatcodeError` - let error = DynSolType::abi_decode(&DynSolType::String, err) - .map_err(|_| RevertDecodingError::BadStringDecode) - .and_then(|v| { - v.as_str().map(|s| s.to_owned()).ok_or(RevertDecodingError::BadStringDecode) - }) - .ok(); - - let error = error.filter(|err| err.as_str() != ""); - error - .or_else(|| { - // try decoding as unknown err - SolString::abi_decode(&err[SELECTOR_LEN..], false) - .map(|err_str| format!("{}:{err_str}", hex::encode(&err[..SELECTOR_LEN]))) - .ok() - }) - .or_else(|| { - // try to decode possible variations of custom error types - decode_custom_error(err).map(|token| { - let s = format!("Custom Error {}:", hex::encode(&err[..SELECTOR_LEN])); - - let err_str = format_token(&token); - if err_str.starts_with('(') { - format!("{s}{err_str}") - } else { - format!("{s}({err_str})") - } - }) - }) - .ok_or_else(|| RevertDecodingError::NonNativeErrorAndNotString) + return if err.is_empty() { + "".to_string() + } else { + format!("custom error bytes {}", hex::encode_prefixed(err)) } } -} -/// Tries to optimistically decode a custom solc error, with at most 4 arguments -pub fn decode_custom_error(err: &[u8]) -> Option { - decode_custom_error_args(err, 4) -} + if err == crate::constants::MAGIC_SKIP { + // Also used in forge fuzz runner + return "SKIPPED".to_string() + } -/// Tries to optimistically decode a custom solc error with a maximal amount of arguments -/// -/// This will brute force decoding of custom errors with up to `args` arguments -pub fn decode_custom_error_args(err: &[u8], args: usize) -> Option { - if err.len() <= SELECTOR_LEN { - return None + // Solidity's `Error(string)` or `Panic(uint256)` + if let Ok(e) = alloy_sol_types::GenericContractError::abi_decode(err, false) { + return e.to_string() } - let err = &err[SELECTOR_LEN..]; - /// types we check against - static TYPES: Lazy> = Lazy::new(|| { - vec![ - DynSolType::Address, - DynSolType::Bool, - DynSolType::Uint(256), - DynSolType::Int(256), - DynSolType::Bytes, - DynSolType::String, - ] - }); + let (selector, data) = err.split_at(SELECTOR_LEN); + let selector: &[u8; 4] = selector.try_into().unwrap(); - // check if single param, but only if it's a single word - if err.len() == 32 { - for ty in TYPES.iter() { - if let Ok(decoded) = ty.abi_decode(err) { - return Some(decoded) + match *selector { + // `CheatcodeError(string)` + Vm::CheatcodeError::SELECTOR => { + if let Ok(e) = Vm::CheatcodeError::abi_decode_raw(data, false) { + return e.message } } - return None - } - - // brute force decode all possible combinations - for num in (2..=args).rev() { - for candidate in TYPES.iter().cloned().combinations(num) { - if let Ok(decoded) = DynSolType::abi_decode(&DynSolType::Tuple(candidate), err) { - return Some(decoded) + // `expectRevert(bytes)` + Vm::expectRevert_2Call::SELECTOR => { + if let Ok(e) = Vm::expectRevert_2Call::abi_decode_raw(data, false) { + return decode_revert(&e.revertData[..], maybe_abi, status) + } + } + // `expectRevert(bytes4)` + Vm::expectRevert_1Call::SELECTOR => { + if let Ok(e) = Vm::expectRevert_1Call::abi_decode_raw(data, false) { + return decode_revert(&e.revertData[..], maybe_abi, status) } } + _ => {} } - // try as array - for ty in TYPES.iter().cloned().map(|ty| DynSolType::Array(Box::new(ty))) { - if let Ok(decoded) = ty.abi_decode(err) { - return Some(decoded) + // Custom error from the given ABI + if let Some(abi) = maybe_abi { + if let Some(abi_error) = abi.errors().find(|e| selector == e.selector()) { + // if we don't decode, don't return an error, try to decode as a string later + if let Ok(decoded) = abi_error.abi_decode_input(data, false) { + return format!( + "{}({})", + abi_error.name, + decoded.iter().map(foundry_common::fmt::format_token).format(", ") + ) + } } } - None -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Address, U256}; - use alloy_sol_types::{sol, SolError}; - - #[test] - fn test_decode_custom_error_address() { - sol! { - error AddressErr(address addr); - } - let err = AddressErr { addr: Address::random() }; + // ABI-encoded `string` + if let Ok(s) = String::abi_decode(err, false) { + return s + } - let encoded = err.abi_encode(); - let decoded = decode_custom_error(&encoded).unwrap(); - assert_eq!(decoded, DynSolValue::Address(err.addr)); + // UTF-8-encoded string + if let Ok(s) = std::str::from_utf8(err) { + return s.to_string() } - #[test] - fn test_decode_custom_error_args3() { - sol! { - error MyError(address addr, bool b, uint256 val); - } - let err = MyError { addr: Address::random(), b: true, val: U256::from(100u64) }; + // Generic custom error + format!( + "custom error {}:{}", + hex::encode(selector), + std::str::from_utf8(data).map_or_else(|_| trimmed_hex(data), String::from) + ) +} - let encoded = err.clone().abi_encode(); - let decoded = decode_custom_error(&encoded).unwrap(); - assert_eq!( - decoded, - DynSolValue::Tuple(vec![ - DynSolValue::Address(err.addr), - DynSolValue::Bool(err.b), - DynSolValue::Uint(U256::from(100u64), 256), - ]) - ); +fn trimmed_hex(s: &[u8]) -> String { + let s = hex::encode(s); + let n = 32 * 2; + if s.len() <= n { + s + } else { + format!("{}…{} ({} bytes)", &s[..n / 2], &s[s.len() - n / 2..], s.len()) } } diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 56befa64a5d0..2cdfae9042d8 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -129,9 +129,7 @@ impl<'a> FuzzedExecutor<'a> { // case. let call_res = _counterexample.1.result.clone(); *counterexample.borrow_mut() = _counterexample; - Err(TestCaseError::fail( - decode::decode_revert(&call_res, errors, Some(status)).unwrap_or_default(), - )) + Err(TestCaseError::fail(decode::decode_revert(&call_res, errors, Some(status)))) } } }); diff --git a/crates/evm/evm/src/executors/invariant/error.rs b/crates/evm/evm/src/executors/invariant/error.rs index e948a3211186..5a8346980d98 100644 --- a/crates/evm/evm/src/executors/invariant/error.rs +++ b/crates/evm/evm/src/executors/invariant/error.rs @@ -94,19 +94,11 @@ impl InvariantFuzzError { logs: call_result.logs, traces: call_result.traces, test_error: proptest::test_runner::TestError::Fail( - format!( - "{}, reason: '{}'", - origin, - match &revert_reason { - Ok(s) => s.clone(), - Err(e) => e.to_string(), - } - ) - .into(), + format!("{origin}, reason: {revert_reason}").into(), calldata.to_vec(), ), return_reason: "".into(), - revert_reason: revert_reason.unwrap_or_default(), + revert_reason, addr: invariant_contract.address, func, inner_sequence: inner_sequence.to_vec(), diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 024b9880b198..19b256541a7f 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -94,7 +94,7 @@ impl Executor { let create2_deployer_account = self .backend .basic(DEFAULT_CREATE2_DEPLOYER)? - .ok_or(DatabaseError::MissingAccount(DEFAULT_CREATE2_DEPLOYER))?; + .ok_or_else(|| DatabaseError::MissingAccount(DEFAULT_CREATE2_DEPLOYER))?; // if the deployer is not currently deployed, deploy the default one if create2_deployer_account.code.map_or(true, |code| code.is_empty()) { @@ -397,8 +397,7 @@ impl Executor { } } _ => { - let reason = decode::decode_revert(result.as_ref(), abi, Some(exit_reason)) - .unwrap_or_else(|_| format!("{exit_reason:?}")); + let reason = decode::decode_revert(result.as_ref(), abi, Some(exit_reason)); return Err(EvmError::Execution(Box::new(ExecutionErr { reverted: true, reason, @@ -841,11 +840,10 @@ fn convert_call_result( }) } _ => { - let reason = decode::decode_revert(result.as_ref(), abi, Some(status)) - .unwrap_or_else(|_| format!("{status:?}")); - if reason == "SKIPPED" { + if &result == crate::constants::MAGIC_SKIP { return Err(EvmError::SkipError) } + let reason = decode::decode_revert(&result, abi, Some(status)); Err(EvmError::Execution(Box::new(ExecutionErr { reverted, reason, diff --git a/crates/evm/evm/src/inspectors/access_list.rs b/crates/evm/evm/src/inspectors/access_list.rs index 5deaa800b0cb..e7bf56e07292 100644 --- a/crates/evm/evm/src/inspectors/access_list.rs +++ b/crates/evm/evm/src/inspectors/access_list.rs @@ -35,6 +35,7 @@ impl AccessListTracer { .collect(), } } + pub fn access_list(&self) -> AccessList { AccessList::from( self.access_list @@ -47,6 +48,7 @@ impl AccessListTracer { ) } } + impl Inspector for AccessListTracer { #[inline] fn step( diff --git a/crates/evm/evm/src/inspectors/debugger.rs b/crates/evm/evm/src/inspectors/debugger.rs index 0e6df101f81b..be06fc722559 100644 --- a/crates/evm/evm/src/inspectors/debugger.rs +++ b/crates/evm/evm/src/inspectors/debugger.rs @@ -1,15 +1,15 @@ use alloy_primitives::{Address, Bytes}; +use foundry_common::SELECTOR_LEN; use foundry_evm_core::{ backend::DatabaseExt, constants::CHEATCODE_ADDRESS, debug::{DebugArena, DebugNode, DebugStep, Instruction}, utils::{gas_used, get_create_address, CallKind}, }; -use foundry_utils::error::ErrorExt; use revm::{ interpreter::{ opcode::{self, spec_opcode_gas}, - CallInputs, CreateInputs, Gas, InstructionResult, Interpreter, Memory, + CallInputs, CreateInputs, Gas, InstructionResult, Interpreter, }, EVMData, Inspector, }; @@ -97,14 +97,14 @@ impl Inspector for Debugger { call.context.code_address, call.context.scheme.into(), ); - if CHEATCODE_ADDRESS == call.contract { - self.arena.arena[self.head].steps.push(DebugStep { - memory: Memory::new(), - instruction: Instruction::Cheatcode( - call.input[0..4].try_into().expect("malformed cheatcode call"), - ), - ..Default::default() - }); + + if call.contract == CHEATCODE_ADDRESS { + if let Some(selector) = call.input.get(..SELECTOR_LEN) { + self.arena.arena[self.head].steps.push(DebugStep { + instruction: Instruction::Cheatcode(selector.try_into().unwrap()), + ..Default::default() + }); + } } (InstructionResult::Continue, Gas::new(call.gas_limit), Bytes::new()) @@ -133,12 +133,7 @@ impl Inspector for Debugger { // TODO: Does this increase gas cost? if let Err(err) = data.journaled_state.load_account(call.caller, data.db) { let gas = Gas::new(call.gas_limit); - return ( - InstructionResult::Revert, - None, - gas, - alloy_primitives::Bytes(err.encode_string().0), - ) + return (InstructionResult::Revert, None, gas, foundry_cheatcodes::Error::encode(err)) } let nonce = data.journaled_state.account(call.caller).info.nonce; diff --git a/crates/evm/traces/Cargo.toml b/crates/evm/traces/Cargo.toml index bcafdf0aebd8..d04c5a8db0ec 100644 --- a/crates/evm/traces/Cargo.toml +++ b/crates/evm/traces/Cargo.toml @@ -19,8 +19,9 @@ foundry-evm-core.workspace = true foundry-utils.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } -alloy-json-abi = { workspace = true } +alloy-json-abi.workspace = true alloy-primitives = { workspace = true, features = ["serde", "getrandom", "arbitrary", "rlp"] } +alloy-sol-types.workspace = true ethers = { workspace = true, features = ["ethers-solc"] } revm = { workspace = true, default-features = false, features = [ "std", diff --git a/crates/evm/traces/src/decoder.rs b/crates/evm/traces/src/decoder/mod.rs similarity index 73% rename from crates/evm/traces/src/decoder.rs rename to crates/evm/traces/src/decoder/mod.rs index ca9406afcb58..7430d51275c1 100644 --- a/crates/evm/traces/src/decoder.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -1,11 +1,11 @@ use crate::{ identifier::{AddressIdentity, SingleSignaturesIdentifier, TraceIdentifier}, node::CallTraceNode, - utils, CallTraceArena, RawOrDecodedCall, RawOrDecodedLog, RawOrDecodedReturnData, + utils, CallTraceArena, RawOrDecodedLog, TraceCallData, TraceRetData, }; use alloy_dyn_abi::{DecodedEvent, DynSolValue, EventExt}; use alloy_json_abi::{Event, Function, JsonAbi as Abi}; -use alloy_primitives::{Address, FixedBytes, B256}; +use alloy_primitives::{Address, Selector, B256}; use foundry_common::{abi::get_indexed_event, SELECTOR_LEN}; use foundry_evm_core::{ abi::{CONSOLE_ABI, HARDHAT_CONSOLE_ABI, HEVM_ABI}, @@ -19,6 +19,8 @@ use foundry_utils::types::ToAlloy; use once_cell::sync::OnceCell; use std::collections::{BTreeMap, HashMap}; +mod precompiles; + /// Build a new [CallTraceDecoder]. #[derive(Default)] #[must_use = "builders do nothing unless you call `build` on them"] @@ -83,8 +85,6 @@ impl CallTraceDecoderBuilder { /// different sets might overlap. #[derive(Clone, Default, Debug)] pub struct CallTraceDecoder { - /// Information for decoding precompile calls. - pub precompiles: HashMap, /// Addresses identified to be a specific contract. /// /// The values are in the form `":"`. @@ -94,7 +94,7 @@ pub struct CallTraceDecoder { /// Information whether the contract address has a receive function pub receive_contracts: HashMap, /// A mapping of signatures to their known functions - pub functions: BTreeMap, Vec>, + pub functions: BTreeMap>, /// All known events pub events: BTreeMap<(B256, usize), Vec>, /// All known errors @@ -105,27 +105,6 @@ pub struct CallTraceDecoder { pub verbosity: u8, } -/// Returns an expression of the type `[(Address, Function); N]` -macro_rules! precompiles { - ($($number:literal : $name:ident($( $name_in:ident : $in:expr ),* $(,)?) -> ($( $name_out:ident : $out:expr ),* $(,)?)),+ $(,)?) => {{ - use std::string::String as RustString; - use ethers::abi::ParamType::*; - [$( - ( - alloy_primitives::Address::with_last_byte($number), - #[allow(deprecated)] - ethers::abi::Function { - name: RustString::from(stringify!($name)), - inputs: vec![$(ethers::abi::Param { name: RustString::from(stringify!($name_in)), kind: $in, internal_type: None, }),*], - outputs: vec![$(ethers::abi::Param { name: RustString::from(stringify!($name_out)), kind: $out, internal_type: None, }),*], - constant: None, - state_mutability: ethers::abi::StateMutability::Pure, - }, - ), - )+] - }}; -} - impl CallTraceDecoder { /// Creates a new call trace decoder. /// @@ -140,20 +119,6 @@ impl CallTraceDecoder { fn init() -> Self { Self { - // TODO: These are the Ethereum precompiles. We should add a way to support precompiles - // for other networks, too. - precompiles: precompiles!( - 0x01: ecrecover(hash: FixedBytes(32), v: Uint(256), r: Uint(256), s: Uint(256)) -> (publicAddress: Address), - 0x02: sha256(data: Bytes) -> (hash: FixedBytes(32)), - 0x03: ripemd(data: Bytes) -> (hash: FixedBytes(32)), - 0x04: identity(data: Bytes) -> (data: Bytes), - 0x05: modexp(Bsize: Uint(256), Esize: Uint(256), Msize: Uint(256), BEM: Bytes) -> (value: Bytes), - 0x06: ecadd(x1: Uint(256), y1: Uint(256), x2: Uint(256), y2: Uint(256)) -> (x: Uint(256), y: Uint(256)), - 0x07: ecmul(x1: Uint(256), y1: Uint(256), s: Uint(256)) -> (x: Uint(256), y: Uint(256)), - 0x08: ecpairing(x1: Uint(256), y1: Uint(256), x2: Uint(256), y2: Uint(256), x3: Uint(256), y3: Uint(256)) -> (success: Uint(256)), - 0x09: blake2f(rounds: Uint(4), h: FixedBytes(64), m: FixedBytes(128), t: FixedBytes(16), f: FixedBytes(1)) -> (h: FixedBytes(64)), - ).into_iter().map(|(addr, func)| (addr, func.to_alloy())).collect(), - contracts: Default::default(), labels: [ @@ -241,28 +206,37 @@ impl CallTraceDecoder { } } + /// Decodes all nodes in the specified call trace. pub async fn decode(&self, traces: &mut CallTraceArena) { for node in &mut traces.arena { // Set contract name - if let Some(contract) = self.contracts.get(&node.trace.address).cloned() { - node.trace.contract = Some(contract); + if let Some(contract) = self.contracts.get(&node.trace.address) { + node.trace.contract = Some(contract.clone()); } // Set label - if let Some(label) = self.labels.get(&node.trace.address).cloned() { - node.trace.label = Some(label); + if let Some(label) = self.labels.get(&node.trace.address) { + node.trace.label = Some(label.clone()); } + // Decode events + self.decode_events(node).await; + // Decode call - if let Some(precompile_fn) = self.precompiles.get(&node.trace.address) { - node.decode_precompile(precompile_fn, &self.labels); - } else if let RawOrDecodedCall::Raw(ref bytes) = node.trace.data { - if bytes.len() >= 4 { + // TODO: chain ID argument + if precompiles::decode(&mut node.trace, 1) { + return + } + + if let TraceCallData::Raw(bytes) = &node.trace.data { + if bytes.len() >= SELECTOR_LEN { if let Some(funcs) = self.functions.get(&bytes[..SELECTOR_LEN]) { node.decode_function(funcs, &self.labels, &self.errors, self.verbosity); } else if node.trace.address == DEFAULT_CREATE2_DEPLOYER { - node.trace.data = - RawOrDecodedCall::Decoded("create2".to_string(), String::new(), vec![]); + node.trace.data = TraceCallData::Decoded { + signature: "create2".to_string(), + args: vec![], + }; } else if let Some(identifier) = &self.signature_identifier { if let Some(function) = identifier.write().await.identify_function(&bytes[..SELECTOR_LEN]).await @@ -276,35 +250,24 @@ impl CallTraceDecoder { } } } else { - let has_receive = self - .receive_contracts - .get(&node.trace.address) - .copied() - .unwrap_or_default(); - let func_name = - if bytes.is_empty() && has_receive { "receive" } else { "fallback" }; - - node.trace.data = - RawOrDecodedCall::Decoded(func_name.to_string(), String::new(), Vec::new()); - - if let RawOrDecodedReturnData::Raw(bytes) = &node.trace.output { + let has_receive = + self.receive_contracts.get(&node.trace.address).copied().unwrap_or(false); + let signature = + if bytes.is_empty() && has_receive { "receive()" } else { "fallback()" } + .into(); + node.trace.data = TraceCallData::Decoded { signature, args: Vec::new() }; + + if let TraceRetData::Raw(bytes) = &node.trace.output { if !node.trace.success { - if let Ok(decoded_error) = decode::decode_revert( - &bytes[..], + node.trace.output = TraceRetData::Decoded(decode::decode_revert( + bytes, Some(&self.errors), Some(node.trace.status), - ) { - node.trace.output = RawOrDecodedReturnData::Decoded(format!( - r#""{decoded_error}""# - )); - } + )); } } } } - - // Decode events - self.decode_events(node).await; } } diff --git a/crates/evm/traces/src/decoder/precompiles.rs b/crates/evm/traces/src/decoder/precompiles.rs new file mode 100644 index 000000000000..90e19a98cc50 --- /dev/null +++ b/crates/evm/traces/src/decoder/precompiles.rs @@ -0,0 +1,204 @@ +use crate::{CallTrace, TraceCallData}; +use alloy_primitives::{B256, U256}; +use alloy_sol_types::{abi, sol, SolCall}; +use itertools::Itertools; + +sol! { +/// EVM precompiles interface. For illustration purposes only, as precompiles don't follow the +/// Solidity ABI codec. +/// +/// Parameter names and types are taken from [evm.codes](https://www.evm.codes/precompiled). +interface Precompiles { + struct EcPairingInput { + uint256 x1; + uint256 y1; + uint256 x2; + uint256 y2; + uint256 x3; + uint256 y3; + } + + /* 0x01 */ function ecrecover(bytes32 hash, uint8 v, uint256 r, uint256 s) returns (address publicAddress); + /* 0x02 */ function sha256(bytes data) returns (bytes32 hash); + /* 0x03 */ function ripemd(bytes data) returns (bytes20 hash); + /* 0x04 */ function identity(bytes data) returns (bytes data); + /* 0x05 */ function modexp(uint256 Bsize, uint256 Esize, uint256 Msize, bytes B, bytes E, bytes M) returns (bytes value); + /* 0x06 */ function ecadd(uint256 x1, uint256 y1, uint256 x2, uint256 y2) returns (uint256 x, uint256 y); + /* 0x07 */ function ecmul(uint256 x1, uint256 y1, uint256 s) returns (uint256 x, uint256 y); + /* 0x08 */ function ecpairing(EcPairingInput[] input) returns (bool success); + /* 0x09 */ function blake2f(uint32 rounds, uint64[8] h, uint64[16] m, uint64[2] t, bool f) returns (uint64[8] h); + /* 0x0a */ function pointEvaluation(bytes32 versionedHash, bytes32 z, bytes32 y, bytes1[48] commitment, bytes1[48] proof) returns (bytes value); +} +} +use Precompiles::*; + +macro_rules! tri { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(_) => return false, + } + }; +} + +/// Tries to decode a precompile call. Returns `true` if successful. +pub(super) fn decode(trace: &mut CallTrace, _chain_id: u64) -> bool { + let [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, x @ 0x01..=0x0a] = + trace.address.0 .0 + else { + return false + }; + + let TraceCallData::Raw(data) = &trace.data else { return false }; + + let (signature, args) = match x { + 0x01 => { + let (sig, ecrecoverCall { hash, v, r, s }) = tri!(abi_decode_call(data)); + (sig, vec![hash.to_string(), v.to_string(), r.to_string(), s.to_string()]) + } + 0x02 => (sha256Call::SIGNATURE, vec![data.to_string()]), + 0x03 => (ripemdCall::SIGNATURE, vec![data.to_string()]), + 0x04 => (identityCall::SIGNATURE, vec![data.to_string()]), + 0x05 => (modexpCall::SIGNATURE, tri!(decode_modexp(data))), + 0x06 => { + let (sig, ecaddCall { x1, y1, x2, y2 }) = tri!(abi_decode_call(data)); + (sig, vec![x1.to_string(), y1.to_string(), x2.to_string(), y2.to_string()]) + } + 0x07 => { + let (sig, ecmulCall { x1, y1, s }) = tri!(abi_decode_call(data)); + (sig, vec![x1.to_string(), y1.to_string(), s.to_string()]) + } + 0x08 => (ecpairingCall::SIGNATURE, tri!(decode_ecpairing(data))), + 0x09 => (blake2fCall::SIGNATURE, tri!(decode_blake2f(data))), + 0x0a => (pointEvaluationCall::SIGNATURE, tri!(decode_kzg(data))), + _ => unreachable!(), + }; + + // TODO: Other chain precompiles + + trace.data = TraceCallData::Decoded { signature: signature.to_string(), args }; + + trace.contract = Some("PRECOMPILES".into()); + + true +} + +// Note: we use the ABI decoder, but this is not necessarily ABI-encoded data. It's just a +// convenient way to decode the data. + +fn decode_modexp(data: &[u8]) -> alloy_sol_types::Result> { + let mut decoder = abi::Decoder::new(data, false); + let b_size = decoder.take_offset()?; + let e_size = decoder.take_offset()?; + let m_size = decoder.take_offset()?; + let b = decoder.take_slice_unchecked(b_size)?; + let e = decoder.take_slice_unchecked(e_size)?; + let m = decoder.take_slice_unchecked(m_size)?; + Ok(vec![ + b_size.to_string(), + e_size.to_string(), + m_size.to_string(), + hex::encode_prefixed(b), + hex::encode_prefixed(e), + hex::encode_prefixed(m), + ]) +} + +fn decode_ecpairing(data: &[u8]) -> alloy_sol_types::Result> { + let mut decoder = abi::Decoder::new(data, false); + let mut values = Vec::new(); + // input must be either empty or a multiple of 6 32-byte values + let mut tmp = <[&B256; 6]>::default(); + while !decoder.is_empty() { + for tmp in &mut tmp { + *tmp = decoder.take_word()?; + } + values.push(iter_to_string(tmp.iter().map(|x| U256::from_be_bytes(x.0)))); + } + Ok(values) +} + +fn decode_blake2f<'a>(data: &'a [u8]) -> alloy_sol_types::Result> { + let mut decoder = abi::Decoder::new(data, false); + let rounds = u32::from_be_bytes(decoder.take_slice_unchecked(4)?.try_into().unwrap()); + let u64_le_list = + |x: &'a [u8]| x.chunks_exact(8).map(|x| u64::from_le_bytes(x.try_into().unwrap())); + let h = u64_le_list(decoder.take_slice_unchecked(64)?); + let m = u64_le_list(decoder.take_slice_unchecked(128)?); + let t = u64_le_list(decoder.take_slice_unchecked(16)?); + let f = decoder.take_slice_unchecked(1)?[0]; + Ok(vec![ + rounds.to_string(), + iter_to_string(h), + iter_to_string(m), + iter_to_string(t), + f.to_string(), + ]) +} + +fn decode_kzg(data: &[u8]) -> alloy_sol_types::Result> { + let mut decoder = abi::Decoder::new(data, false); + let versioned_hash = decoder.take_word()?; + let z = decoder.take_word()?; + let y = decoder.take_word()?; + let commitment = decoder.take_slice_unchecked(48)?; + let proof = decoder.take_slice_unchecked(48)?; + Ok(vec![ + versioned_hash.to_string(), + z.to_string(), + y.to_string(), + hex::encode_prefixed(commitment), + hex::encode_prefixed(proof), + ]) +} + +fn abi_decode_call(data: &[u8]) -> alloy_sol_types::Result<(&'static str, T)> { + // raw because there are no selectors here + Ok((T::SIGNATURE, T::abi_decode_raw(data, false)?)) +} + +fn iter_to_string, T: std::fmt::Display>(iter: I) -> String { + format!("[{}]", iter.format(", ")) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::hex; + + #[test] + fn ecpairing() { + // https://github.com/foundry-rs/foundry/issues/5337#issuecomment-1627384480 + let data = hex!( + " + 26bbb723f965460ca7282cd75f0e3e7c67b15817f7cee60856b394936ed02917 + 0fbe873ac672168143a91535450bab6c412dce8dc8b66a88f2da6e245f9282df + 13cd4f0451538ece5014fe6688b197aefcc611a5c6a7c319f834f2188ba04b08 + 126ff07e81490a1b6ae92b2d9e700c8e23e9d5c7f6ab857027213819a6c9ae7d + 04183624c9858a56c54deb237c26cb4355bc2551312004e65fc5b299440b15a3 + 2e4b11aa549ad6c667057b18be4f4437fda92f018a59430ebb992fa3462c9ca1 + 2d4d9aa7e302d9df41749d5507949d05dbea33fbb16c643b22f599a2be6df2e2 + 14bedd503c37ceb061d8ec60209fe345ce89830a19230301f076caff004d1926 + 0967032fcbf776d1afc985f88877f182d38480a653f2decaa9794cbc3bf3060c + 0e187847ad4c798374d0d6732bf501847dd68bc0e071241e0213bc7fc13db7ab + 304cfbd1e08a704a99f5e847d93f8c3caafddec46b7a0d379da69a4d112346a7 + 1739c1b1a457a8c7313123d24d2f9192f896b7c63eea05a9d57f06547ad0cec8 + 001d6fedb032f70e377635238e0563f131670001f6abf439adb3a9d5d52073c6 + 1889afe91e4e367f898a7fcd6464e5ca4e822fe169bccb624f6aeb87e4d060bc + 198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2 + 1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed + 090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b + 12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa + 2dde6d7baf0bfa09329ec8d44c38282f5bf7f9ead1914edd7dcaebb498c84519 + 0c359f868a85c6e6c1ea819cfab4a867501a3688324d74df1fe76556558b1937 + 29f41c6e0e30802e2749bfb0729810876f3423e6f24829ad3e30adb1934f1c8a + 030e7a5f70bb5daa6e18d80d6d447e772efb0bb7fb9d0ffcd54fc5a48af1286d + 0ea726b117e48cda8bce2349405f006a84cdd3dcfba12efc990df25970a27b6d + 30364cd4f8a293b1a04f0153548d3e01baad091c69097ca4e9f26be63e4095b5 + " + ); + let decoded = decode_ecpairing(&data).unwrap(); + // 4 arrays of 6 32-byte values + assert_eq!(decoded.len(), 4); + } +} diff --git a/crates/evm/traces/src/inspector.rs b/crates/evm/traces/src/inspector.rs index 0c82714b531b..cb7e24f9658c 100644 --- a/crates/evm/traces/src/inspector.rs +++ b/crates/evm/traces/src/inspector.rs @@ -1,6 +1,6 @@ use crate::{ - CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, RawOrDecodedCall, RawOrDecodedLog, - RawOrDecodedReturnData, + CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, RawOrDecodedLog, TraceCallData, + TraceRetData, }; use alloy_primitives::{Address, Bytes, Log as RawLog, B256, U256}; use foundry_evm_core::{ @@ -45,7 +45,7 @@ impl Tracer { depth, address, kind, - data: RawOrDecodedCall::Raw(data.into()), + data: TraceCallData::Raw(data.into()), value, status: InstructionResult::Continue, caller, @@ -68,7 +68,7 @@ impl Tracer { trace.status = status; trace.success = success; trace.gas_cost = cost; - trace.output = RawOrDecodedReturnData::Raw(output.into()); + trace.output = TraceRetData::Raw(output.into()); if let Some(address) = address { trace.address = address; diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 1b8507066535..87c4dea2fcf5 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -92,7 +92,7 @@ impl CallTraceArena { .iter() .map(|node| { if node.trace.created() { - if let RawOrDecodedReturnData::Raw(ref bytes) = node.trace.output { + if let TraceRetData::Raw(bytes) = &node.trace.output { return (&node.trace.address, Some(bytes.as_ref())) } } @@ -138,29 +138,23 @@ impl CallTraceArena { // Add step to geth trace struct_logs.push(log); - // Check if the step was a call - match step.op { - Instruction::OpCode(opc) => { - match opc { - // If yes, descend into a child trace - opcode::CREATE | - opcode::CREATE2 | - opcode::DELEGATECALL | - opcode::CALL | - opcode::STATICCALL | - opcode::CALLCODE => { - self.add_to_geth_trace( - storage, - &self.arena[trace_node.children[child_id]], - struct_logs, - opts, - ); - child_id += 1; - } - _ => {} - } - } - Instruction::Cheatcode(_) => {} + // Descend into a child trace if the step was a call + if let Instruction::OpCode( + opcode::CREATE | + opcode::CREATE2 | + opcode::DELEGATECALL | + opcode::CALL | + opcode::STATICCALL | + opcode::CALLCODE, + ) = step.op + { + self.add_to_geth_trace( + storage, + &self.arena[trace_node.children[child_id]], + struct_logs, + opts, + ); + child_id += 1; } } } @@ -253,14 +247,13 @@ impl fmt::Display for CallTraceArena { // Display trace return data let color = trace_color(&node.trace); - write!(writer, "{child}{EDGE}")?; - write!(writer, "{}", color.paint(RETURN))?; + write!(writer, "{child}{EDGE}{}", color.paint(RETURN))?; if node.trace.created() { match &node.trace.output { - RawOrDecodedReturnData::Raw(bytes) => { + TraceRetData::Raw(bytes) => { writeln!(writer, "{} bytes of code", bytes.len())?; } - RawOrDecodedReturnData::Decoded(val) => { + TraceRetData::Decoded(val) => { writeln!(writer, "{val}")?; } } @@ -327,48 +320,54 @@ pub enum LogCallOrder { /// Raw or decoded calldata. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub enum RawOrDecodedCall { - /// Raw calldata +pub enum TraceCallData { + /// Raw calldata bytes. Raw(Bytes), /// Decoded calldata. - /// - /// The first element in the tuple is the function name, second is the function signature and - /// the third element is a vector of decoded parameters. - Decoded(String, String, Vec), + Decoded { + /// The function signature. + signature: String, + /// The function arguments. + args: Vec, + }, } -impl RawOrDecodedCall { - pub fn to_raw(&self) -> Vec { - match self { - RawOrDecodedCall::Raw(raw) => raw.to_vec(), - RawOrDecodedCall::Decoded(_, _, _) => { - vec![] - } - } +impl Default for TraceCallData { + fn default() -> Self { + Self::Raw(Bytes::new()) } } -impl Default for RawOrDecodedCall { - fn default() -> Self { - RawOrDecodedCall::Raw(Default::default()) +impl TraceCallData { + pub fn as_bytes(&self) -> &[u8] { + match self { + TraceCallData::Raw(raw) => raw, + TraceCallData::Decoded { .. } => &[], + } } } /// Raw or decoded return data. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub enum RawOrDecodedReturnData { - /// Raw return data +pub enum TraceRetData { + /// Raw return data. Raw(Bytes), - /// Decoded return data + /// Decoded return data. Decoded(String), } -impl RawOrDecodedReturnData { +impl Default for TraceRetData { + fn default() -> Self { + Self::Raw(Bytes::new()) + } +} + +impl TraceRetData { /// Returns the data as [`Bytes`] pub fn to_bytes(&self) -> Bytes { match self { - RawOrDecodedReturnData::Raw(raw) => raw.clone(), - RawOrDecodedReturnData::Decoded(val) => val.as_bytes().to_vec().into(), + TraceRetData::Raw(raw) => raw.clone(), + TraceRetData::Decoded(val) => val.as_bytes().to_vec().into(), } } @@ -377,23 +376,17 @@ impl RawOrDecodedReturnData { } } -impl Default for RawOrDecodedReturnData { - fn default() -> Self { - RawOrDecodedReturnData::Raw(Default::default()) - } -} - -impl fmt::Display for RawOrDecodedReturnData { +impl fmt::Display for TraceRetData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { - RawOrDecodedReturnData::Raw(bytes) => { + TraceRetData::Raw(bytes) => { if bytes.is_empty() { write!(f, "()") } else { bytes.fmt(f) } } - RawOrDecodedReturnData::Decoded(decoded) => f.write_str(decoded), + TraceRetData::Decoded(decoded) => f.write_str(decoded), } } } @@ -477,10 +470,10 @@ pub struct CallTrace { /// The value transferred in the call pub value: U256, /// The calldata for the call, or the init code for contract creations - pub data: RawOrDecodedCall, + pub data: TraceCallData, /// The return data of the call if this was not a contract creation, otherwise it is the /// runtime bytecode of the created contract - pub output: RawOrDecodedReturnData, + pub output: TraceRetData, /// The gas cost of the call pub gas_cost: u64, /// The status of the trace's call @@ -524,54 +517,53 @@ impl Default for CallTrace { impl fmt::Display for CallTrace { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let address = self.address.to_checksum(None); + write!(f, "[{}] ", self.gas_cost)?; if self.created() { write!( f, - "[{}] {}{} {}@{}", - self.gas_cost, + "{}{} {}@{}", Paint::yellow(CALL), Paint::yellow("new"), - self.label.as_ref().unwrap_or(&"".to_string()), + self.label.as_deref().unwrap_or(""), address - )?; + ) } else { - let (func, inputs) = match &self.data { - RawOrDecodedCall::Raw(bytes) => { + let (func_name, inputs) = match &self.data { + TraceCallData::Raw(bytes) => { // We assume that the fallback function (`data.len() < 4`) counts as decoded // calldata - assert!(bytes.len() >= 4); - (hex::encode(&bytes[0..4]), hex::encode(&bytes[4..])) + let (selector, data) = bytes.split_at(4); + (hex::encode(selector), hex::encode(data)) + } + TraceCallData::Decoded { signature, args } => { + let name = signature.split('(').next().unwrap(); + (name.to_string(), args.join(", ")) } - RawOrDecodedCall::Decoded(func, _, inputs) => (func.clone(), inputs.join(", ")), }; let action = match self.kind { // do not show anything for CALLs CallKind::Call => "", - CallKind::StaticCall => "[staticcall]", - CallKind::CallCode => "[callcode]", - CallKind::DelegateCall => "[delegatecall]", - _ => unreachable!(), + CallKind::StaticCall => " [staticcall]", + CallKind::CallCode => " [callcode]", + CallKind::DelegateCall => " [delegatecall]", + CallKind::Create | CallKind::Create2 => unreachable!(), }; let color = trace_color(self); write!( f, - "[{}] {}::{}{}({}) {}", - self.gas_cost, - color.paint(self.label.as_ref().unwrap_or(&address)), - color.paint(func), - if !self.value == U256::ZERO { - format!("{{value: {}}}", self.value) + "{addr}::{func_name}{opt_value}({inputs}){action}", + addr = color.paint(self.label.as_deref().unwrap_or(&address)), + func_name = color.paint(func_name), + opt_value = if self.value == U256::ZERO { + String::new() } else { - "".to_string() + format!("{{value: {}}}", self.value) }, - inputs, - Paint::yellow(action), - )?; + action = Paint::yellow(action), + ) } - - Ok(()) } } diff --git a/crates/evm/traces/src/node.rs b/crates/evm/traces/src/node.rs index e212f60ca9cd..20d21d73dec4 100644 --- a/crates/evm/traces/src/node.rs +++ b/crates/evm/traces/src/node.rs @@ -1,6 +1,6 @@ use crate::{ - utils, utils::decode_cheatcode_outputs, CallTrace, LogCallOrder, RawOrDecodedCall, - RawOrDecodedLog, RawOrDecodedReturnData, + utils, utils::decode_cheatcode_outputs, CallTrace, LogCallOrder, RawOrDecodedLog, + TraceCallData, TraceRetData, }; use alloy_dyn_abi::{FunctionExt, JsonAbiExt}; use alloy_json_abi::{Function, JsonAbi as Abi}; @@ -76,7 +76,7 @@ impl CallTraceNode { to: self.trace.address.to_ethers(), value: self.trace.value.to_ethers(), gas: self.trace.gas_cost.into(), - input: self.trace.data.to_raw().into(), + input: self.trace.data.as_bytes().to_vec().into(), call_type: self.kind().into(), }) } @@ -84,7 +84,7 @@ impl CallTraceNode { from: self.trace.caller.to_ethers(), value: self.trace.value.to_ethers(), gas: self.trace.gas_cost.into(), - init: self.trace.data.to_raw().into(), + init: self.trace.data.as_bytes().to_vec().into(), }), } } @@ -104,8 +104,8 @@ impl CallTraceNode { // the same name and inputs. let func = &funcs[0]; - if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { - let inputs = if bytes.len() >= SELECTOR_LEN { + if let TraceCallData::Raw(ref bytes) = self.trace.data { + let args = if bytes.len() >= SELECTOR_LEN { if self.trace.address == CHEATCODE_ADDRESS { // Try to decode cheatcode inputs in a more custom way utils::decode_cheatcode_inputs(func, bytes, errors, verbosity).unwrap_or_else( @@ -128,17 +128,16 @@ impl CallTraceNode { }; // add signature to decoded calls for better calls filtering - self.trace.data = - RawOrDecodedCall::Decoded(func.name.clone(), func.signature(), inputs); + self.trace.data = TraceCallData::Decoded { signature: func.signature(), args }; - if let RawOrDecodedReturnData::Raw(bytes) = &self.trace.output { - if !bytes.is_empty() && self.trace.success { + if let TraceRetData::Raw(bytes) = &self.trace.output { + if self.trace.success { if self.trace.address == CHEATCODE_ADDRESS { if let Some(decoded) = funcs .iter() .find_map(|func| decode_cheatcode_outputs(func, bytes, verbosity)) { - self.trace.output = RawOrDecodedReturnData::Decoded(decoded); + self.trace.output = TraceRetData::Decoded(decoded); return } } @@ -149,7 +148,7 @@ impl CallTraceNode { // Functions coming from an external database do not have any outputs // specified, and will lead to returning an empty list of tokens. if !tokens.is_empty() { - self.trace.output = RawOrDecodedReturnData::Decoded( + self.trace.output = TraceRetData::Decoded( tokens .iter() .map(|token| utils::label(token, labels)) @@ -158,11 +157,12 @@ impl CallTraceNode { ); } } - } else if let Ok(decoded_error) = - decode::decode_revert(bytes, Some(errors), Some(self.trace.status)) - { - self.trace.output = - RawOrDecodedReturnData::Decoded(format!(r#""{decoded_error}""#)); + } else { + self.trace.output = TraceRetData::Decoded(decode::decode_revert( + bytes, + Some(errors), + Some(self.trace.status), + )); } } } @@ -174,19 +174,18 @@ impl CallTraceNode { precompile_fn: &Function, labels: &HashMap, ) { - if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { + if let TraceCallData::Raw(ref bytes) = self.trace.data { self.trace.label = Some("PRECOMPILE".to_string()); - self.trace.data = RawOrDecodedCall::Decoded( - precompile_fn.name.clone(), - precompile_fn.signature(), - precompile_fn.abi_decode_input(bytes, false).map_or_else( + self.trace.data = TraceCallData::Decoded { + signature: precompile_fn.signature(), + args: precompile_fn.abi_decode_input(bytes, false).map_or_else( |_| vec![hex::encode(bytes)], |tokens| tokens.iter().map(|token| utils::label(token, labels)).collect(), ), - ); + }; - if let RawOrDecodedReturnData::Raw(ref bytes) = self.trace.output { - self.trace.output = RawOrDecodedReturnData::Decoded( + if let TraceRetData::Raw(ref bytes) = self.trace.output { + self.trace.output = TraceRetData::Decoded( precompile_fn.abi_decode_output(bytes, false).map_or_else( |_| hex::encode(bytes), |tokens| { diff --git a/crates/evm/traces/src/utils.rs b/crates/evm/traces/src/utils.rs index c046fe5d6c16..590ee250b4fd 100644 --- a/crates/evm/traces/src/utils.rs +++ b/crates/evm/traces/src/utils.rs @@ -32,9 +32,7 @@ pub(crate) fn decode_cheatcode_inputs( verbosity: u8, ) -> Option> { match func.name.as_str() { - "expectRevert" => { - decode::decode_revert(data, Some(errors), None).ok().map(|decoded| vec![decoded]) - } + "expectRevert" => Some(vec![decode::decode_revert(data, Some(errors), None)]), "rememberKey" | "addr" | "startBroadcast" | "broadcast" => { // these functions accept a private key as uint256, which should not be // converted to plain text diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index e03f63915290..242971ba5014 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -173,7 +173,7 @@ impl ScriptArgs { return Some(AdditionalContract { opcode: node.kind(), address: node.trace.address, - init_code: node.trace.data.to_raw(), + init_code: node.trace.data.as_bytes().to_vec(), }) } None diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index e6f960ee8dce..f5435dc3cd3d 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -20,8 +20,7 @@ use forge::{ opts::EvmOpts, traces::{ identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, - CallTraceDecoder, CallTraceDecoderBuilder, RawOrDecodedCall, RawOrDecodedReturnData, - TraceKind, Traces, + CallTraceDecoder, CallTraceDecoderBuilder, TraceCallData, TraceKind, TraceRetData, Traces, }, utils::CallKind, }; @@ -367,11 +366,10 @@ impl ScriptArgs { } if !result.success { - let revert_msg = decode::decode_revert(&result.returned[..], None, None) - .map(|err| format!("{err}\n")) - .unwrap_or_else(|_| "Script failed.\n".to_string()); - - eyre::bail!("{}", Paint::red(revert_msg)); + return Err(eyre::eyre!( + "script failed: {}", + decode::decode_revert(&result.returned[..], None, None) + )) } Ok(()) @@ -522,9 +520,9 @@ impl ScriptArgs { let mut unknown_c = 0usize; for node in create_nodes { // Calldata == init code - if let RawOrDecodedCall::Raw(ref init_code) = node.trace.data { + if let TraceCallData::Raw(ref init_code) = node.trace.data { // Output is the runtime code - if let RawOrDecodedReturnData::Raw(ref deployed_code) = node.trace.output { + if let TraceRetData::Raw(ref deployed_code) = node.trace.output { // Only push if it was not present already if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) { bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code)); diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index a8e7cab04b37..baf8b9b97789 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -1,7 +1,7 @@ use crate::{ constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS}, hashbrown::HashSet, - traces::{CallTraceArena, RawOrDecodedCall, TraceKind}, + traces::{CallTraceArena, TraceCallData, TraceKind}, }; use alloy_primitives::U256; use comfy_table::{presets::ASCII_MARKDOWN, *}; @@ -77,22 +77,23 @@ impl GasReport { let contract_info = self.contracts.entry(name.to_string()).or_default(); match &trace.data { - RawOrDecodedCall::Raw(bytes) => { + TraceCallData::Raw(bytes) => { if trace.created() { contract_info.gas = U256::from(trace.gas_cost); contract_info.size = U256::from(bytes.len()); } } - RawOrDecodedCall::Decoded(func, sig, _) => { + TraceCallData::Decoded { signature, .. } => { + let name = signature.split('(').next().unwrap(); // ignore any test/setup functions let should_include = - !(func.is_test() || func.is_invariant_test() || func.is_setup()); + !(name.is_test() || name.is_invariant_test() || name.is_setup()); if should_include { let gas_info = contract_info .functions - .entry(func.clone()) + .entry(name.into()) .or_default() - .entry(sig.clone()) + .entry(signature.clone()) .or_default(); gas_info.calls.push(U256::from(trace.gas_cost)); } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index b57866b26dfd..6458e73fcca9 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -148,11 +148,11 @@ impl<'a> ContractRunner<'a> { Err(EvmError::Execution(err)) => { let ExecutionErr { traces, labels, logs, reason, .. } = *err; error!(reason = ?reason, contract = ?address, "setUp failed"); - (logs, traces, labels, Some(format!("Setup failed: {reason}")), None) + (logs, traces, labels, Some(format!("setup failed: {reason}")), None) } Err(err) => { error!(reason=?err, contract= ?address, "setUp failed"); - (Vec::new(), None, BTreeMap::new(), Some(format!("Setup failed: {err}")), None) + (Vec::new(), None, BTreeMap::new(), Some(format!("setup failed: {err}")), None) } }; traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 3b8a235b1df9..71934d19aa77 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -627,7 +627,7 @@ forgetest_async!(can_deploy_with_create2, |prj: TestProject, cmd: TestCommand| a .assert_nonce_increment([(0, 2)]) .await // Running again results in error, since we're repeating the salt passed to CREATE2 - .run(ScriptOutcome::FailedScript); + .run(ScriptOutcome::ScriptFailed); }); forgetest_async!( diff --git a/crates/forge/tests/it/core.rs b/crates/forge/tests/it/core.rs index dabd7183c722..16aa8e5689b1 100644 --- a/crates/forge/tests/it/core.rs +++ b/crates/forge/tests/it/core.rs @@ -18,7 +18,7 @@ async fn test_core() { vec![( "setUp()", false, - Some("Setup failed: setup failed predictably".to_string()), + Some("setup failed: revert: setup failed predictably".to_string()), None, None, )], @@ -65,7 +65,7 @@ async fn test_core() { vec![( "setUp()", false, - Some("Setup failed: execution error".to_string()), + Some("setup failed: execution error".to_string()), None, None, )], diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index 30d97ed418a7..771a009d9f5f 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -26,7 +26,13 @@ async fn test_invariant() { ), ( "fuzz/invariant/common/InvariantInnerContract.t.sol:InvariantInnerContract", - vec![("invariantHideJesus()", false, Some("jesus betrayed.".into()), None, None)], + vec![( + "invariantHideJesus()", + false, + Some("revert: jesus betrayed".into()), + None, + None, + )], ), ( "fuzz/invariant/common/InvariantReentrancy.t.sol:InvariantReentrancy", @@ -35,11 +41,11 @@ async fn test_invariant() { ( "fuzz/invariant/common/InvariantTest1.t.sol:InvariantTest", vec![ - ("invariant_neverFalse()", false, Some("false.".into()), None, None), + ("invariant_neverFalse()", false, Some("revert: false".into()), None, None), ( "statefulFuzz_neverFalseWithInvariantAlias()", false, - Some("false.".into()), + Some("revert: false".into()), None, None, ), @@ -55,11 +61,23 @@ async fn test_invariant() { ), ( "fuzz/invariant/target/TargetSenders.t.sol:TargetSenders", - vec![("invariantTrueWorld()", false, Some("false world.".into()), None, None)], + vec![( + "invariantTrueWorld()", + false, + Some("revert: false world".into()), + None, + None, + )], ), ( "fuzz/invariant/target/TargetInterfaces.t.sol:TargetWorldInterfaces", - vec![("invariantTrueWorld()", false, Some("false world.".into()), None, None)], + vec![( + "invariantTrueWorld()", + false, + Some("revert: false world".into()), + None, + None, + )], ), ( "fuzz/invariant/target/ExcludeSenders.t.sol:ExcludeSenders", @@ -77,7 +95,13 @@ async fn test_invariant() { "fuzz/invariant/targetAbi/TargetArtifacts.t.sol:TargetArtifacts", vec![ ("invariantShouldPass()", true, None, None, None), - ("invariantShouldFail()", false, Some("false world.".into()), None, None), + ( + "invariantShouldFail()", + false, + Some("revert: false world".into()), + None, + None, + ), ], ), ( @@ -86,7 +110,13 @@ async fn test_invariant() { ), ( "fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol:TargetArtifactSelectors2", - vec![("invariantShouldFail()", false, Some("its false.".into()), None, None)], + vec![( + "invariantShouldFail()", + false, + Some("revert: it's false".into()), + None, + None, + )], ), ]), ); @@ -112,7 +142,7 @@ async fn test_invariant_override() { &results, BTreeMap::from([( "fuzz/invariant/common/InvariantReentrancy.t.sol:InvariantReentrancy", - vec![("invariantNotStolen()", false, Some("stolen.".into()), None, None)], + vec![("invariantNotStolen()", false, Some("revert: stolen".into()), None, None)], )]), ); } @@ -142,7 +172,7 @@ async fn test_invariant_fail_on_revert() { vec![( "statefulFuzz_BrokenInvariant()", false, - Some("failed on revert".into()), + Some("revert: failed on revert".into()), None, None, )], diff --git a/crates/test-utils/src/script.rs b/crates/test-utils/src/script.rs index 7fa87d849a1b..598746f61222 100644 --- a/crates/test-utils/src/script.rs +++ b/crates/test-utils/src/script.rs @@ -257,7 +257,7 @@ pub enum ScriptOutcome { MissingSender, MissingWallet, StaticCallNotAllowed, - FailedScript, + ScriptFailed, UnsupportedLibraries, ErrorSelectForkOnBroadcast, } @@ -272,7 +272,7 @@ impl ScriptOutcome { Self::MissingSender => "You seem to be using Foundry's default sender. Be sure to set your own --sender", Self::MissingWallet => "No associated wallet", Self::StaticCallNotAllowed => "staticcall`s are not allowed after `broadcast`; use `startBroadcast` instead", - Self::FailedScript => "Script failed.", + Self::ScriptFailed => "script failed: ", Self::UnsupportedLibraries => "Multi chain deployment does not support library linking at the moment.", Self::ErrorSelectForkOnBroadcast => "cannot select forks during a broadcast", } @@ -289,7 +289,7 @@ impl ScriptOutcome { ScriptOutcome::StaticCallNotAllowed | ScriptOutcome::UnsupportedLibraries | ScriptOutcome::ErrorSelectForkOnBroadcast | - ScriptOutcome::FailedScript => true, + ScriptOutcome::ScriptFailed => true, } } } diff --git a/testdata/fuzz/invariant/common/InvariantInnerContract.t.sol b/testdata/fuzz/invariant/common/InvariantInnerContract.t.sol index 49e67e7d37d7..6e1100cef3cc 100644 --- a/testdata/fuzz/invariant/common/InvariantInnerContract.t.sol +++ b/testdata/fuzz/invariant/common/InvariantInnerContract.t.sol @@ -45,6 +45,6 @@ contract InvariantInnerContract is DSTest { } function invariantHideJesus() public { - require(jesus.identity_revealed() == false, "jesus betrayed."); + require(jesus.identity_revealed() == false, "jesus betrayed"); } } diff --git a/testdata/fuzz/invariant/common/InvariantReentrancy.t.sol b/testdata/fuzz/invariant/common/InvariantReentrancy.t.sol index 38bb930ced98..cf8cb4d37f94 100644 --- a/testdata/fuzz/invariant/common/InvariantReentrancy.t.sol +++ b/testdata/fuzz/invariant/common/InvariantReentrancy.t.sol @@ -40,6 +40,6 @@ contract InvariantReentrancy is DSTest { } function invariantNotStolen() public { - require(vuln.stolen() == false, "stolen."); + require(vuln.stolen() == false, "stolen"); } } diff --git a/testdata/fuzz/invariant/common/InvariantTest1.t.sol b/testdata/fuzz/invariant/common/InvariantTest1.t.sol index 775b2cffd43d..3e9d84e321f6 100644 --- a/testdata/fuzz/invariant/common/InvariantTest1.t.sol +++ b/testdata/fuzz/invariant/common/InvariantTest1.t.sol @@ -30,10 +30,10 @@ contract InvariantTest is DSTest { } function invariant_neverFalse() public { - require(inv.flag1(), "false."); + require(inv.flag1(), "false"); } function statefulFuzz_neverFalseWithInvariantAlias() public { - require(inv.flag1(), "false."); + require(inv.flag1(), "false"); } } diff --git a/testdata/fuzz/invariant/target/ExcludeContracts.t.sol b/testdata/fuzz/invariant/target/ExcludeContracts.t.sol index 4b44c26680ab..0367bdec22d4 100644 --- a/testdata/fuzz/invariant/target/ExcludeContracts.t.sol +++ b/testdata/fuzz/invariant/target/ExcludeContracts.t.sol @@ -26,6 +26,6 @@ contract ExcludeContracts is DSTest { } function invariantTrueWorld() public { - require(hello.world() == true, "false world."); + require(hello.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/target/ExcludeSenders.t.sol b/testdata/fuzz/invariant/target/ExcludeSenders.t.sol index 34e01519932c..d0399f681bca 100644 --- a/testdata/fuzz/invariant/target/ExcludeSenders.t.sol +++ b/testdata/fuzz/invariant/target/ExcludeSenders.t.sol @@ -40,6 +40,6 @@ contract ExcludeSenders is DSTest { } function invariantTrueWorld() public { - require(hello.world() == true, "false world."); + require(hello.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/target/TargetContracts.t.sol b/testdata/fuzz/invariant/target/TargetContracts.t.sol index 376e8ee15bcb..288b9d8d5cd6 100644 --- a/testdata/fuzz/invariant/target/TargetContracts.t.sol +++ b/testdata/fuzz/invariant/target/TargetContracts.t.sol @@ -27,6 +27,6 @@ contract TargetContracts is DSTest { } function invariantTrueWorld() public { - require(hello2.world() == true, "false world."); + require(hello2.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/target/TargetInterfaces.t.sol b/testdata/fuzz/invariant/target/TargetInterfaces.t.sol index 3f6d39f6ece5..f6e94ad5d83c 100644 --- a/testdata/fuzz/invariant/target/TargetInterfaces.t.sol +++ b/testdata/fuzz/invariant/target/TargetInterfaces.t.sol @@ -67,6 +67,6 @@ contract TargetWorldInterfaces is DSTest { } function invariantTrueWorld() public { - require(proxy.world() == false, "false world."); + require(proxy.world() == false, "false world"); } } diff --git a/testdata/fuzz/invariant/target/TargetSelectors.t.sol b/testdata/fuzz/invariant/target/TargetSelectors.t.sol index 289937efb9bb..1adc889a95c6 100644 --- a/testdata/fuzz/invariant/target/TargetSelectors.t.sol +++ b/testdata/fuzz/invariant/target/TargetSelectors.t.sol @@ -36,6 +36,6 @@ contract TargetSelectors is DSTest { } function invariantTrueWorld() public { - require(hello.world() == true, "false world."); + require(hello.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/target/TargetSenders.t.sol b/testdata/fuzz/invariant/target/TargetSenders.t.sol index 94735404bc8c..0bd1d6b9549c 100644 --- a/testdata/fuzz/invariant/target/TargetSenders.t.sol +++ b/testdata/fuzz/invariant/target/TargetSenders.t.sol @@ -26,6 +26,6 @@ contract TargetSenders is DSTest { } function invariantTrueWorld() public { - require(hello.world() == true, "false world."); + require(hello.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol b/testdata/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol index 80947360ba82..82f56229d294 100644 --- a/testdata/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol +++ b/testdata/fuzz/invariant/targetAbi/ExcludeArtifacts.t.sol @@ -40,6 +40,6 @@ contract ExcludeArtifacts is DSTest { } function invariantShouldPass() public { - require(excluded.world() == true, "false world."); + require(excluded.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol b/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol index 69fa42161903..fae4a4986645 100644 --- a/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol +++ b/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors.t.sol @@ -36,6 +36,6 @@ contract TargetArtifactSelectors is DSTest { } function invariantShouldPass() public { - require(hello.world() == true, "false world."); + require(hello.world() == true, "false world"); } } diff --git a/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol b/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol index bd4c1c62bcc4..d4091d4f3ea9 100644 --- a/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol +++ b/testdata/fuzz/invariant/targetAbi/TargetArtifactSelectors2.t.sol @@ -63,6 +63,6 @@ contract TargetArtifactSelectors2 is DSTest { if (!parent.should_be_true()) { require(!Child(address(parent.child())).changed(), "should have not happened"); } - require(parent.should_be_true() == true, "its false."); + require(parent.should_be_true() == true, "it's false"); } } diff --git a/testdata/fuzz/invariant/targetAbi/TargetArtifacts.t.sol b/testdata/fuzz/invariant/targetAbi/TargetArtifacts.t.sol index e99e3b8671c0..07570fee7bf0 100644 --- a/testdata/fuzz/invariant/targetAbi/TargetArtifacts.t.sol +++ b/testdata/fuzz/invariant/targetAbi/TargetArtifacts.t.sol @@ -35,10 +35,10 @@ contract TargetArtifacts is DSTest { } function invariantShouldPass() public { - require(target2.world() == true || target1.world() == true || hello.world() == true, "false world."); + require(target2.world() == true || target1.world() == true || hello.world() == true, "false world"); } function invariantShouldFail() public { - require(target2.world() == true || target1.world() == true, "false world."); + require(target2.world() == true || target1.world() == true, "false world"); } }