From b0ac83f5be9626e7aac761766023a2353afe34b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:38:54 +0000 Subject: [PATCH 01/25] chore(deps): bump the cargo group across 1 directory with 2 updates Bumps the cargo group with 2 updates in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing) and [ammonia](https://github.com/rust-ammonia/ammonia). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) Updates `ammonia` from 4.1.0 to 4.1.2 - [Release notes](https://github.com/rust-ammonia/ammonia/releases) - [Changelog](https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-ammonia/ammonia/compare/v4.1.0...v4.1.2) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo - dependency-name: ammonia dependency-version: 4.1.2 dependency-type: indirect dependency-group: cargo ... Signed-off-by: dependabot[bot] --- Cargo.lock | 93 +++++++++++++++++++++++------------------------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7c9a5740b582..445b46584712c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "ammonia" -version = "4.1.0" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" dependencies = [ "cssparser", "html5ever", @@ -1982,7 +1982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -2581,6 +2581,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.7.1" @@ -2933,6 +2942,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ + "convert_case 0.6.0", "proc-macro2", "quote", "syn 2.0.101", @@ -2945,7 +2955,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case", + "convert_case 0.7.1", "proc-macro2", "quote", "syn 2.0.101", @@ -3532,7 +3542,7 @@ name = "forge-doc" version = "1.1.0" dependencies = [ "alloy-primitives", - "derive_more 2.0.1", + "derive_more 1.0.0", "eyre", "forge-fmt", "foundry-common", @@ -4075,7 +4085,7 @@ version = "1.1.0" dependencies = [ "alloy-primitives", "alloy-sol-types", - "derive_more 2.0.1", + "derive_more 1.0.0", "foundry-common-fmt", "foundry-macros", "itertools 0.14.0", @@ -4768,8 +4778,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -4924,12 +4934,11 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.31.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", "match_token", ] @@ -5152,7 +5161,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -5549,7 +5558,7 @@ dependencies = [ "lalrpop-util", "petgraph", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "string_cache", "term", "tiny-keccak", @@ -5563,7 +5572,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.9", + "regex-automata", ] [[package]] @@ -5733,9 +5742,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.16.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a8096766c229e8c88a3900c9b44b7e06aa7f7343cc229158c3e58ef8f9973a" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", "tendril", @@ -5750,9 +5759,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "match_token" -version = "0.1.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", @@ -5761,11 +5770,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -6037,12 +6046,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -6299,12 +6307,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owo-colors" version = "4.2.0" @@ -6838,7 +6840,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -7204,17 +7206,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -7225,7 +7218,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] @@ -7234,12 +7227,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -9297,14 +9284,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", From b8cc9593e845d64c4402a778ddcd9e8ed2485794 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:13:24 +0000 Subject: [PATCH 02/25] Update crates/verify/src/provider.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/verify/src/provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index 6246137d4b41b..532491382ab9f 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -181,7 +181,7 @@ impl VerificationProviderType { // 2. If `--verifier etherscan` is explicitly set, enforce the API key requirement. if matches!(self, Self::Etherscan) { - if key.as_ref().is_none_or(|key| key.is_empty()) { + if key.map_or(true, str::is_empty) { eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier") } return Ok(Box::::default()); From 6516b9d593ef4b23e7521fc33cffcff6f3161c43 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:13:45 +0000 Subject: [PATCH 03/25] Update crates/doc/src/writer/as_doc.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/doc/src/writer/as_doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index 04ce26510a927..09a8fbd224264 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -232,7 +232,7 @@ impl AsDoc for Document { let filtered_comments: Comments = (*comments) .iter() - .filter(|c| c.tag != CommentTag::Custom("variant".to_string())) + .filter(|c| c.tag != CommentTag::Custom("variant".to_string()) && c.tag != CommentTag::Param) .cloned() .collect::>() .into(); From 75202d556d50bf75f2e135b499651c8f050ee381 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 5 Nov 2025 18:57:52 +0700 Subject: [PATCH 04/25] Update as_doc.rs (#235) Tidy up formatting in as_doc.rs by removing extraneous blank lines in the Document::as_doc implementation Enhancements: Remove unnecessary blank line before initializing bases Remove unnecessary blank line before writing state variables Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/doc/src/writer/as_doc.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index 09a8fbd224264..f6591116c30a3 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -143,7 +143,6 @@ impl AsDoc for Document { // we need this to find the _relative_ paths let src_target_dir = self.target_src_dir(); - let mut bases = vec![]; let linked = read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); @@ -175,7 +174,6 @@ impl AsDoc for Document { } writer.writeln_doc(&item.comments)?; - if let Some(state_vars) = item.variables() { writer.write_subtitle("State Variables")?; state_vars.into_iter().try_for_each(|(item, comments, code)| { From c03610c6dd5d5c46e61511ffb4cbf74aabcf2890 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:04:50 +0700 Subject: [PATCH 05/25] Update crates/cli/src/utils/suggestions.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/cli/src/utils/suggestions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/utils/suggestions.rs b/crates/cli/src/utils/suggestions.rs index 8f6d7f3cde092..82a14a3b24beb 100644 --- a/crates/cli/src/utils/suggestions.rs +++ b/crates/cli/src/utils/suggestions.rs @@ -17,7 +17,7 @@ where .map(|pv| (strsim::jaro_winkler(v, pv.as_ref()), pv.as_ref().to_owned())) .filter(|(similarity, _)| *similarity > 0.8) .collect(); - candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + candidates.sort_by(|a, b| a.0.total_cmp(&b.0)); candidates.into_iter().map(|(_, pv)| pv).collect() } From 923a3cf9026e5cbff1aff52b69b5cd7b5af8b821 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:05:07 +0700 Subject: [PATCH 06/25] Update crates/common/src/contracts.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/common/src/contracts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index e9f203ebf7539..895b16b3b4532 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -239,7 +239,7 @@ impl ContractsByArtifact { None } }) - .min_by(|(score1, _), (score2, _)| score1.partial_cmp(score2).unwrap_or(std::cmp::Ordering::Equal)) + .min_by(|(score1, _), (score2, _)| score1.total_cmp(score2)) .map(|(_, data)| data) } From be849518bc8fb9436609d3aefa899ec8d1c3b680 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:09:13 +0700 Subject: [PATCH 07/25] fix(forge): adjust gas assertion CounterWithFallback (foundry-rs#14465) (#500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): bump softprops/action-gh-release from 2.6.1 to 3.0.0 (#14396) * chore(deps): bump actions/cache from 5.0.4 to 5.0.5 (#14397) * chore(deps): bump crate-ci/typos from 1.45.0 to 1.45.1 (#14398) * fix(cast): add browser wallet support for erc20 commands (#14395) * fix(cast): add browser wallet support for erc20 commands Add a browser wallet branch to the erc20_send! macro so that cast erc20 transfer/approve/mint/burn --browser works the same way cast send --browser does. The new path builds the transaction via the sol! IERC20 macro, applies user-provided tx params, then fills missing nonce, fees, and gas limit from the provider before handing the request to the browser wallet. Closes #13103 Co-authored-by: John Chase <68833933+joohhnnn@users.noreply.github.com> * fix(cast): default Tempo browser erc20 fee token * fix(cast): scope Tempo erc20 browser auto-detection * style(cast): format erc20 Tempo network helper --------- Co-authored-by: John Chase <68833933+joohhnnn@users.noreply.github.com> * feat(forge): browser wallet support for `create` subcommand (#14394) * fix(deps): update rustls-webpki to fix RUSTSEC-2026-0104 (#14408) chore: update rustls-webpki to fix RUSTSEC-2026-0104 Amp-Thread-ID: https://ampcode.com/threads/T-019db45a-2ee1-771d-8127-2052dc6df2a3 Co-authored-by: Amp * fix: accept 0x-prefixed value inputs (#14406) * fix: accept 0x-prefixed value inputs * use U256::from_str for 0x-prefixed hex parsing, add 0X support and expand tests Amp-Thread-ID: https://ampcode.com/threads/T-019db45a-2ee1-771d-8127-2052dc6df2a3 Co-authored-by: Amp --------- Co-authored-by: zerosnacks Co-authored-by: Amp * ci(npm): use OIDC trusted publishing, remove NPM_TOKEN (#14249) * chore: harden Makefile & CI (#14386) chore: harden Makefile, remove unused deps found w/ cargo shear Amp-Thread-ID: https://ampcode.com/threads/T-019daf55-1b0e-7756-8fff-4506e2e239c8 Co-authored-by: Amp * feat: use ChannelDb for channel persistence (#14355) * feat: use mpp-rs ChannelDb for channel persistence * use foundry-core * rm blank line * warn when old channels.json is found after SQLite migration * rename Channel --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * feat: support console.table (#14338) * feat(abi): add console.table variants to Console ABI * feat(fmt): implement console_table_format * feat(macros): extend ConsoleFmt derive for console.table * feat(inspectors): push msg per line in hardhat_log * chore: run cargo fmt * chore: add todo comment * refactor: use comfy-table for console_table_format Amp-Thread-ID: https://ampcode.com/threads/T-019db45a-2ee1-771d-8127-2052dc6df2a3 Co-authored-by: Amp * refactor(macros): detect table structs by field types instead of name Replace fragile name-based detection (`starts_with("table")`) with a structural check: table call structs have all fields of type `Vec` (Solidity arrays), while regular `log` calls never use array parameters. * refactor(macros): gate table detection on both name prefix and Vec fields Combine the name-based check (`starts_with("table")`) with the structural Vec field check so both must hold. This prevents accidental table rendering for unrelated structs that happen to have all-Vec fields. --------- Co-authored-by: zerosnacks Co-authored-by: Amp Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * chore: bump `foundry-wallets` dep (#14409) * chore: bump `foundry-wallets` dep + set `browser` & `tempo` features * chore: set features * fix(npm): add release environment for OIDC trusted publishing (#14411) Add `environment: release` to both publish-arch and publish-meta jobs to match the npm trusted publisher configuration on npmjs.com. Without the environment claim in the OIDC token, the registry rejects publishes with a misleading E404. Also refreshes bun.lock to fix frozen-lockfile failures. Amp-Thread-ID: https://ampcode.com/threads/T-019db4f4-ee94-735b-b75d-4f1bcbbb3041 Co-authored-by: Amp * fix(npm): remove deprecated `baseUrl` compiler option (#14413) Amp-Thread-ID: https://ampcode.com/threads/T-019db557-8d02-7723-bfd6-68c4107b9433 Co-authored-by: Amp * chore: bump alloy 2.0.1 (#14417) * fix(wallets): add gas limit margin to handle WebAuthn/P256 signing (#14416) fix(wallets): add gas limit margin to handle WebAuthn/P256 signing * fix(evm): sync gas params when updating executor spec (#14420) * fix(evm): sync gas params when updating executor spec * fix: make clippy happy --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(lint): add incorrect ERC721 interface lint (#14412) * fix(common): enable `foundry-wallets` browser/tempo feats (#14421) * chore(tests): bump forge-std version (#14422) chore: bump forge-std version used for tests Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(anvil-server): distinguish empty and notification batches (#14405) Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * feat(anvil): add debug_traceBlockByHash and debug_traceBlockByNumber (#14391) * feat(anvil): add `debug_traceBlockByHash` and `debug_traceBlockByNumber` RPC endpoints Implement the two missing geth debug_ block tracing endpoints that trace all transactions in a block at once, returning per-tx results. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: fmt --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * chore(foundryup): remove tempo fork support (#14324) * chore: remove tempo fork support Tempo is now fully upstream in the main Foundry repo, so the separate network handling is no longer needed. * Apply suggestion from @zerosnacks Update Foundryup version * warn when foundryup --network is ignored * Update foundryup/foundryup Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * Update foundryup/foundryup Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(lint): add missing visit methods to LateLintVisitor (#14276) * fix(lint): add missing visit methods to LateLintVisitor * fix(lint): dispatch deprecated nested late lint hooks * fix(lint): use by-value nested late lint hooks * test(lint): cover late visitor hooks --------- Co-authored-by: figtracer * fix: remove `network: tempo` from Tempo template (#14424) remove network: tempo from template * feat(anvil): RPC methods for tempo's `TipFeeManager` in `anvil_*` namespace (#14414) * feat(anvil): RPC methods for tempo's `TipFeeManager` in`anvil_*` namespace * feat(anvil): add tests + mint TIP20 tokens to admin before adding FeeAMM liquidity --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix(anvil): separate queued txs in txpool RPC responses (#14425) * refactor(cli): unify `NetworkVariant` with `NetworkConfigs` (#14426) * refactor(cli): unify `NetworkVariant` with `NetworkConfigs` * chore: clean-ups + tests * fix: after fig's review * fix: skip serialization if `network` is `None` * chore: pin to foundry-wallets release (#14429) pin to foundry-wallets release * fix(config): respect custom Etherscan URL in cast/forge commands (#14319) * fix(config): respect custom Etherscan URL in cast/forge commands Amp-Thread-ID: https://ampcode.com/threads/T-019db9f9-01dd-729a-9f7a-ed150aabf130 Co-authored-by: Amp Co-authored-by: Gustavo Figueiredo * fix lint * fix clippy * chore: update Cargo.lock Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019db9f9-01dd-729a-9f7a-ed150aabf130 * fix: update Cargo.lock from upstream base Amp-Thread-ID: https://ampcode.com/threads/T-019db9f9-01dd-729a-9f7a-ed150aabf130 Co-authored-by: Amp --------- Co-authored-by: Amp Co-authored-by: Gustavo Figueiredo * feat(common): add MPP WebSocket transport (#14404) * feat(common): add MPP WebSocket transport * docs * use alloy's wsbakcend::from_socket * use main repo * remove patches * bump rustls-webpki * fix: only use `MppWsConnect` when MPP key is available * fix: install default rustls crypto provider * fix: clean imports * fix: MPP known host check * fix: MPP only if known endpoint && key available --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * fix(script): preserve exit reason in failed revert decode (#13985) * fix(script): preserve exit reason in failed revert decode * test(script): cover exit reason output in script failures * fix(script): initialize exit_reason in ScriptResult default --------- Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> * fix(anvil): fix flaky test_increase_time_by_zero test (#14430) Co-authored-by: Amp * feat(lint): add incorrect ERC20 interface lint (#14428) * feat(lint): add incorrect ERC20 interface lint * test(lint): cover direct-name IERC20 case * fix(lint): revert IncorrectERC20Interface test to use empty IERC20 base Expanding IERC20 with full function signatures caused solc compilation failures (Error 4822: Overriding function return types differ) because IERC20Incorrect inherits from IERC20 and overrides with incompatible return types. Revert to the empty-base pattern used by IncorrectERC721Interface. Amp-Thread-ID: https://ampcode.com/threads/T-019dbb72-89c7-740d-b6cf-b87a1509d3e3 Co-authored-by: Amp * test: clarify incorrect ERC20 interface fixtures * test: align ERC20 fixture with ERC721 --------- Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp * fix(cheatcodes): reject nested debug trace recording (#14423) * fix(cheatcodes): reject nested debug trace recording * test(cheatcodes): restore RecordDebugTrace formatting * chore: bump revm v38 (#14436) * deps: bump tempo to TIP-1016 (rev 2e6e9d1) Patches revm to bluealloy/revm@a1a1824 and op-revm to foundry-rs/op-revm@66388e6 for the InitialAndFloorGas handler change. Patches reth-core to paradigmxyz/reth-core@6b12498 for the Bytecode::new_analyzed unsafe change. Adds gas_limit() and state_gas_used() to AnvilStorageProvider to satisfy the updated PrecompileStorageProvider trait. Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d97c8-3033-770a-bd6c-714c2a8393d5 * fix: resolve CI failures from patched revm - Replace deprecated OpCode::new_unchecked with new_or_unknown - Add gas_refunded field to PrecompileOutput struct literals - Allow foundry-rs/op-revm and paradigmxyz/reth-core git sources in deny.toml Co-Authored-By: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d97c8-3033-770a-bd6c-714c2a8393d5 * fix: align revm/op-revm/tempo revs with tip1016 branch Updates patch revisions to match tempo/tip1016 HEAD: - revm: a1a1824 → ea8d1f5 - op-revm: 66388e6 → 780b812 - tempo: 2e6e9d1 → 4092dfe Fixes CreateInputs::new reservoir parameter for the new revm. Co-Authored-By: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d97c8-3033-770a-bd6c-714c2a8393d5 * fix: pin tempo to tip1016 HEAD (807b57c) Tempo's [patch.crates-io] now includes all revm sub-crates, preventing the invariant workflow's patch propagation from clobbering foundry's patches. Co-Authored-By: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d97c8-3033-770a-bd6c-714c2a8393d5 * bump to later commit in https://github.com/tempoxyz/tempo/pull/2684/commits * chore: bump revm 37→38, tempo crates to tip1016 head - revm 37.0.0 → 38.0.0 (crates.io release, no more git patches) - revm-inspectors 0.38.1 → 0.39.0 - alloy-evm 0.32.0 → 0.33.1 - tempo crates rev f873f0e → c095527 (tip1016 head) - foundry-fork-db, op-revm, foundry-rs/optimism bumped via branches - fix OpCode::new_or_unknown → new_unchecked (API change in revm 38) - add missing Evm::cfg_env impl (new trait method in alloy-evm 0.33.1) Co-Authored-By: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> * bump alloy-evm dep to 0.33.2 * chore: bump revm v38 using upstream OP * fix: deny.toml * fix(deps): merge conflict rustls * style: drop cmnt Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * fix: restore cargo-shear --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: zerosnacks Co-authored-by: Federico Gimenez Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> * chore: group dependabot updates by ecosystem (#14438) Group minor and patch version updates per ecosystem into single PRs to reduce PR noise. Major version bumps still get individual PRs for careful review. Before: up to 15 PRs/week (5 per ecosystem) After: up to 6 PRs/week (1 grouped + 1 major per ecosystem, worst case) Amp-Thread-ID: https://ampcode.com/threads/T-019dbe78-a376-7248-a549-6ddbe2c960d4 Co-authored-by: George Niculae Co-authored-by: Amp * refactor(evm): remove useless OP EVM wrapper (#14440) Co-authored-by: Copilot * chore(deps): bump the actions-weekly group with 4 updates (#14441) Bumps the actions-weekly group with 4 updates: [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request), [github/codeql-action](https://github.com/github/codeql-action), [taiki-e/install-action](https://github.com/taiki-e/install-action) and [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action). Updates `peter-evans/create-pull-request` from 8.1.0 to 8.1.1 - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/c0f553fe549906ede9cf27b5156039d195d2ece0...5f6978faf089d4d20b00c7766989d076bb2fc7f1) Updates `github/codeql-action` from 4.35.1 to 4.35.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225) Updates `taiki-e/install-action` from 2.75.16 to 2.75.17 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/a2352fc6ce487f030a3aa709482d57823eadfb37...58e862542551f667fa44c8a2a4a1d64ad477c96a) Updates `taiki-e/cache-cargo-install-action` from 3.0.5 to 3.0.6 - [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases) - [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/a8b9ecf8e0c0ea09d7481cfc583a5203ecd585b5...f9eed3e4680f27610dc6d8c67be1b88593f7dade) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 8.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-weekly - dependency-name: github/codeql-action dependency-version: 4.35.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-weekly - dependency-name: taiki-e/install-action dependency-version: 2.75.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-weekly - dependency-name: taiki-e/cache-cargo-install-action dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-weekly ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(tests): update gas report snapshot after gas params sync fix (#14439) The deployment cost for CounterWithFallback changed from 132471 to 132459 after #14420 started properly syncing gas parameters when updating the executor spec via set_spec_id. Amp-Thread-ID: https://ampcode.com/threads/T-019dbea7-a331-7269-80a8-b935a7bdaa49 Co-authored-by: Amp * feat: log broken invariant as soon as it is found (#14433) * feat: log broken invariant as soon as it is found * fix * nest failure counts inside metrics * chore: bump foundry-compilers to 0.20.0, foundry-block-explorers to 0.23.0, foundry-fork-db to 0.26.0 (#14443) * chore: bump foundry-compilers to 0.20.0 Amp-Thread-ID: https://ampcode.com/threads/T-019dbf04-adac-755f-b685-5fa64d792acb Co-authored-by: Amp * chore: bump foundry-compilers to 0.20.0, foundry-block-explorers to 0.23.0 Amp-Thread-ID: https://ampcode.com/threads/T-019dbf01-f2de-711e-8edf-581a979f9f0d Co-authored-by: Amp * chore: bump foundry-fork-db to 0.26.0 Amp-Thread-ID: https://ampcode.com/threads/T-019dbf01-f2de-711e-8edf-581a979f9f0d Co-authored-by: Amp * chore: enable zstd feature for foundry-fork-db Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019dbf01-f2de-711e-8edf-581a979f9f0d --------- Co-authored-by: Amp * chore: remove unmaintained devcontainer (#14449) deprecate unmaintained devcontainer * chore: update security contact email (#14450) update link * feat(lint): add block-timestamp lint (#14431) * feat: add ignored_error_codes_from config option (#13841) * chore(deny): remove deprecated repos from whitelist (#14455) * chore(deps): bump rui314/setup-mold from 725a8794d15fc7563f59595bd9556495c0564878 to 9c9c13bf4c3f1adef0cc596abc155580bcb04444 (#14442) chore(deps): bump rui314/setup-mold Bumps [rui314/setup-mold](https://github.com/rui314/setup-mold) from 725a8794d15fc7563f59595bd9556495c0564878 to 9c9c13bf4c3f1adef0cc596abc155580bcb04444. - [Commits](https://github.com/rui314/setup-mold/compare/725a8794d15fc7563f59595bd9556495c0564878...9c9c13bf4c3f1adef0cc596abc155580bcb04444) --- updated-dependencies: - dependency-name: rui314/setup-mold dependency-version: 9c9c13bf4c3f1adef0cc596abc155580bcb04444 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update flake.lock (#14458) Co-authored-by: github-actions[bot] * fix(forge): adjust gas assertion `CounterWithFallback` (#14465) * chore: update latest benchmarks (#14467) * ci: split MPP e2e into its own workflow (#14468) * ci: split MPP e2e into its own workflow Move the MPP e2e step from ci-tempo.yml into a standalone ci-mpp.yml workflow so transient HTTP 402 failures from the MPP RPC do not block the Tempo CI workflow. Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp * ci: rename sanity-check job to tempo-check Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp * ci: rename mpp-e2e job to mpp-check Amp-Thread-ID: https://ampcode.com/threads/T-019dceb8-61e5-734f-b047-17665b4ea7d3 Co-authored-by: Amp --------- Co-authored-by: Amp * Improve GH actions (#14473) * fix(benches): add repos + extra args support to prevent blocking errors (#14470) * fix(benches): add repos + extra args support to prevent blocking errors * fix(ci): set `inputs.repos` default to empty * fix: remove `--verbose` flags * fix: exclude `uniswap/v4-core` `TickMathTestTest` --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: figtracer Co-authored-by: John Chase <68833933+joohhnnn@users.noreply.github.com> Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: Amp Co-authored-by: zerosnacks Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Co-authored-by: Nuno David <74260683+ndavd@users.noreply.github.com> Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com> Co-authored-by: Louis Peter Sitoe Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: MD Islam Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: mk0walsk Co-authored-by: Suuuuuuperrrrr fred Co-authored-by: Arsh Co-authored-by: Perico Perica Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Co-authored-by: Federico Gimenez Co-authored-by: George Niculae Co-authored-by: Copilot Co-authored-by: github-actions[bot] --- .devcontainer/Dockerfile.dev | 84 -- .devcontainer/devcontainer.json | 49 - .github/dependabot.yml | 15 + .github/scripts/tempo-mpp.sh | 18 +- .github/workflows/benchmarks.yml | 52 +- .github/workflows/bump-forge-std.yml | 2 +- .github/workflows/ci-mpp.yml | 55 + .github/workflows/ci-tempo.yml | 17 +- .github/workflows/ci.yml | 16 +- .github/workflows/crate-checks.yml | 6 +- .github/workflows/docker-publish.yml | 1 + .github/workflows/docs.yml | 7 +- .github/workflows/npm.yml | 11 +- .github/workflows/release.yml | 15 +- .github/workflows/test-flaky.yml | 6 +- .github/workflows/test-isolate.yml | 6 +- .github/workflows/test.yml | 8 +- Cargo.lock | 983 +++++++++--------- Cargo.toml | 137 ++- Makefile | 41 +- SECURITY.md | 2 +- benches/LATEST.md | 79 +- benches/src/lib.rs | 96 +- benches/src/main.rs | 12 +- crates/anvil/core/src/eth/mod.rs | 28 + crates/anvil/server/src/handler.rs | 86 +- crates/anvil/server/src/pubsub.rs | 78 +- crates/anvil/src/eth/api.rs | 100 +- crates/anvil/src/eth/backend/executor.rs | 1 + crates/anvil/src/eth/backend/fork.rs | 34 +- crates/anvil/src/eth/backend/mem/mod.rs | 185 +++- crates/anvil/src/eth/backend/tempo.rs | 8 + crates/anvil/src/eth/sign.rs | 6 + crates/anvil/src/evm.rs | 51 +- crates/anvil/tests/it/anvil_api.rs | 3 +- crates/anvil/tests/it/fork.rs | 1 + crates/anvil/tests/it/tempo.rs | 123 +++ crates/anvil/tests/it/traces.rs | 86 +- crates/anvil/tests/it/txpool.rs | 68 ++ crates/cast/Cargo.toml | 3 +- crates/cast/src/args.rs | 6 +- crates/cast/src/call_spec.rs | 12 +- crates/cast/src/cmd/batch_mktx.rs | 16 +- crates/cast/src/cmd/batch_send.rs | 16 +- crates/cast/src/cmd/creation_code.rs | 7 +- crates/cast/src/cmd/da_estimate.rs | 6 +- crates/cast/src/cmd/erc20.rs | 113 +- crates/cast/src/cmd/send.rs | 14 +- crates/cast/src/cmd/storage.rs | 15 +- crates/cast/src/opts.rs | 3 +- crates/cast/src/tx.rs | 16 +- crates/cast/tests/cli/main.rs | 49 + crates/cheatcodes/src/evm.rs | 4 + crates/cheatcodes/src/fs.rs | 1 + crates/cli/Cargo.toml | 3 +- crates/cli/src/opts/evm.rs | 7 +- crates/cli/src/opts/mod.rs | 2 - crates/cli/src/opts/network.rs | 27 - crates/cli/src/utils/abi.rs | 3 +- crates/cli/src/utils/mod.rs | 20 +- crates/common/Cargo.toml | 4 + crates/common/fmt/Cargo.toml | 1 + crates/common/fmt/src/console.rs | 107 ++ crates/common/fmt/src/lib.rs | 2 +- crates/common/fmt/src/ui.rs | 17 +- crates/common/src/abi.rs | 7 +- crates/common/src/provider/mpp/mod.rs | 1 + crates/common/src/provider/mpp/persist.rs | 410 +++----- crates/common/src/provider/mpp/session.rs | 38 +- crates/common/src/provider/mpp/transport.rs | 10 +- crates/common/src/provider/mpp/ws.rs | 492 +++++++++ .../common/src/provider/runtime_transport.rs | 46 +- crates/common/src/tempo/mod.rs | 11 + crates/config/src/lib.rs | 20 +- crates/evm/abi/src/Console.json | 2 +- crates/evm/abi/src/console.py | 8 +- crates/evm/core/src/bytecode.rs | 1 + crates/evm/core/src/evm/op.rs | 170 +-- crates/evm/evm/src/executors/invariant/mod.rs | 113 +- crates/evm/evm/src/executors/mod.rs | 33 +- crates/evm/evm/src/inspectors/logs.rs | 4 +- crates/evm/fuzz/src/invariant/mod.rs | 5 +- crates/evm/networks/Cargo.toml | 5 +- crates/evm/networks/src/lib.rs | 235 ++++- crates/forge/Cargo.toml | 2 +- .../forge/assets/tempo/workflowTemplate.yml | 1 - crates/forge/src/cmd/clone.rs | 19 +- crates/forge/src/cmd/create.rs | 139 ++- crates/forge/src/cmd/init.rs | 4 +- crates/forge/src/runner.rs | 9 +- crates/forge/tests/cli/cmd.rs | 68 +- crates/forge/tests/cli/config.rs | 3 + crates/forge/tests/cli/script.rs | 41 + crates/lint/README.md | 5 +- crates/lint/src/linter/late.rs | 216 +++- crates/lint/src/sol/info/block_timestamp.rs | 70 ++ crates/lint/src/sol/info/mod.rs | 6 +- .../src/sol/med/incorrect_erc20_interface.rs | 112 ++ .../src/sol/med/incorrect_erc721_interface.rs | 121 +++ crates/lint/src/sol/med/mod.rs | 8 + crates/lint/testdata/BlockTimestamp.sol | 84 ++ crates/lint/testdata/BlockTimestamp.stderr | 96 ++ .../lint/testdata/IncorrectERC20Interface.sol | 44 + .../testdata/IncorrectERC20Interface.stderr | 120 +++ .../testdata/IncorrectERC721Interface.sol | 52 + .../testdata/IncorrectERC721Interface.stderr | 152 +++ crates/macros/src/console_fmt.rs | 46 +- crates/primitives/src/transaction/envelope.rs | 69 +- crates/primitives/src/transaction/receipt.rs | 30 +- crates/primitives/src/transaction/request.rs | 14 +- crates/script/Cargo.toml | 2 +- crates/script/src/execute.rs | 13 +- crates/script/src/lib.rs | 4 + crates/script/src/runner.rs | 8 +- crates/verify/src/etherscan/flatten.rs | 2 +- deny.toml | 6 +- flake.lock | 18 +- foundryup/foundryup | 66 +- npm/bun.lock | 15 +- npm/scripts/publish.mjs | 10 - npm/tsconfig.json | 1 - .../default/cheats/RecordDebugTrace.t.sol | 15 + testdata/forge-std-rev | 2 +- 123 files changed, 4728 insertions(+), 1626 deletions(-) delete mode 100644 .devcontainer/Dockerfile.dev delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci-mpp.yml delete mode 100644 crates/cli/src/opts/network.rs create mode 100644 crates/common/src/provider/mpp/ws.rs create mode 100644 crates/lint/src/sol/info/block_timestamp.rs create mode 100644 crates/lint/src/sol/med/incorrect_erc20_interface.rs create mode 100644 crates/lint/src/sol/med/incorrect_erc721_interface.rs create mode 100644 crates/lint/testdata/BlockTimestamp.sol create mode 100644 crates/lint/testdata/BlockTimestamp.stderr create mode 100644 crates/lint/testdata/IncorrectERC20Interface.sol create mode 100644 crates/lint/testdata/IncorrectERC20Interface.stderr create mode 100644 crates/lint/testdata/IncorrectERC721Interface.sol create mode 100644 crates/lint/testdata/IncorrectERC721Interface.stderr diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev deleted file mode 100644 index aea6d85497e7f..0000000000000 --- a/.devcontainer/Dockerfile.dev +++ /dev/null @@ -1,84 +0,0 @@ -FROM ubuntu:22.04 - -ARG USERNAME=foundry -ARG USER_UID=1000 -ARG USER_GID=$USER_UID -ARG PYTHON_VERSION=3.14 -ARG NODE_MAJOR=20 -ARG VYPER_VERSION=0.4.3 - -ENV DEBIAN_FRONTEND=noninteractive -ENV CARGO_TERM_COLOR=always \ - RUST_BACKTRACE=full - -WORKDIR /workspace - -RUN apt-get update && apt-get install -y --no-install-recommends \ - # Build tools - build-essential \ - clang \ - lld \ - pkg-config \ - # Network/SSL - curl \ - ca-certificates \ - gnupg \ - libssl-dev \ - # Version control & utils - git \ - sudo \ - unzip \ - # Python - python${PYTHON_VERSION} \ - python3-pip \ - python${PYTHON_VERSION}-venv \ - # Add Node.js repo - && mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ - # Update again after adding repo and install Node.js - && apt-get update && apt-get install -y --no-install-recommends \ - nodejs \ - # Clean up apt cache - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Ensure python points to the installed python version -RUN ln -sf /usr/bin/python${PYTHON_VERSION} /usr/bin/python && \ - ln -sf /usr/bin/python${PYTHON_VERSION} /usr/bin/python3 - -# Create non-root user with sudo privileges -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \ - # Setup sudo without password prompt - && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME \ - # Add user to the sudo group (standard practice) - && usermod -aG sudo $USERNAME - -# Switch to the non-root user -USER $USERNAME -WORKDIR /home/$USERNAME - -# --- User-specific installations --- - -# Install Bun -ENV BUN_INSTALL="/home/$USERNAME/.bun" -ENV PATH="$BUN_INSTALL/bin:$PATH" -RUN curl -fsSL https://bun.sh/install | bash - -# Install Rust & cargo-nextest -ENV CARGO_HOME="/home/$USERNAME/.cargo" -ENV RUSTUP_HOME="/home/$USERNAME/.rustup" -ENV PATH="$CARGO_HOME/bin:$PATH" -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ - && cargo install cargo-nextest --locked - -# Install Vyper using pip -# Ensure pip user install directory is in PATH -ENV PYTHONUSERBASE="/home/$USERNAME/.local" -ENV PATH="$PYTHONUSERBASE/bin:$PATH" -RUN pip3 install --user vyper==${VYPER_VERSION} - -# Switch back to the main workspace directory -WORKDIR /workspace - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 1c2eb26f5a9f6..0000000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,49 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. -{ - "name": "Foundry Development", - "build": { - "context": "..", - "dockerfile": "Dockerfile.dev" - }, - - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": true, - "configureZshAsDefaultShell": true, - "installOhMyZsh": true, - "upgradePackages": true - } - }, - - "forwardPorts": [], - - "postCreateCommand": "rustup default stable && rustup update", - - "customizations": { - "vscode": { - "extensions": [ - "rust-lang.rust-analyzer", - "serayuzgur.crates", - "tamasfe.even-better-toml", - "ms-python.python", - "dbaeumer.vscode-eslint", - "oven.bun-vscode" - ], - "settings": { - "rust-analyzer.checkOnSave": true, - "rust-analyzer.cargo.features": "all" - } - } - }, - - "remoteUser": "foundry", - - "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", - - "workspaceFolder": "/workspace", - - "mounts": [ - "source=${localEnv:HOME}/.cargo/registry,target=/home/foundry/.cargo/registry,type=bind,consistency=cached", - "source=${localEnv:HOME}/.cargo/git,target=/home/foundry/.cargo/git,type=bind,consistency=cached" - ] -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1cd9930619634..ad7b297a7a874 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,11 @@ updates: interval: "weekly" cooldown: default-days: 7 + groups: + actions-weekly: + applies-to: "version-updates" + patterns: ["*"] + update-types: ["minor", "patch"] - package-ecosystem: "cargo" directory: "/" @@ -14,6 +19,11 @@ updates: cooldown: default-days: 7 open-pull-requests-limit: 5 + groups: + cargo-weekly: + applies-to: "version-updates" + patterns: ["*"] + update-types: ["minor", "patch"] - package-ecosystem: "npm" directory: "/npm" @@ -22,3 +32,8 @@ updates: cooldown: default-days: 7 open-pull-requests-limit: 5 + groups: + npm-weekly: + applies-to: "version-updates" + patterns: ["*"] + update-types: ["minor", "patch"] diff --git a/.github/scripts/tempo-mpp.sh b/.github/scripts/tempo-mpp.sh index 1a98b1ed9c494..0d34c4a3b5fa5 100755 --- a/.github/scripts/tempo-mpp.sh +++ b/.github/scripts/tempo-mpp.sh @@ -110,7 +110,7 @@ BLOCK2=$("$CAST" block-number --rpc-url "$RPC_MPP") AFTER2=$("$CAST" erc20 balance "$TOKEN" "$WALLET" --rpc-url "$RPC" | awk '{print $1}') SPENT2=$((BEFORE2 - AFTER2)) echo "Block: $BLOCK2" -echo "Spent: $SPENT2 units (should be 0 — channel reused from ~/.tempo/foundry/channels.json)" +echo "Spent: $SPENT2 units (should be 0 — channel reused from ~/.tempo/channels.db)" # 6. forge script via MPP echo "" @@ -129,9 +129,9 @@ contract MppCheck is Script { } } SOL -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$FORGE" script "$TMPDIR/script/Mpp.s.sol" --rpc-url "$RPC_MPP" --root "$TMPDIR" -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 7. forge test with vm.createSelectFork via MPP @@ -149,15 +149,15 @@ contract MppForkTest is Test { } } SOL -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$FORGE" test --match-test test_fork_via_mpp --root "$TMPDIR" -vvv -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 8. anvil fork via MPP echo "" echo "=== 8. anvil --fork-url (via MPP) ===" -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$ANVIL" --fork-url "$RPC_MPP" --port 8555 --silent & ANVIL_PID=$! for _ in $(seq 1 30); do @@ -167,15 +167,15 @@ done echo "chain-id: $("$CAST" chain-id --rpc-url http://localhost:8555)" kill $ANVIL_PID 2>/dev/null wait $ANVIL_PID 2>/dev/null -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 9. chisel fork via MPP echo "" echo "=== 9. chisel --fork-url (via MPP) ===" -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo 'block.number' | "$CHISEL" --fork-url "$RPC_MPP" 2>&1 | grep -E "Decimal|Type" -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" echo "" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 96af1f4ab9a1e..9b7f834c420c7 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -15,15 +15,44 @@ on: type: string default: "stable,nightly" repos: - description: "Comma-separated list of repos to benchmark (e.g., ithacaxyz/account:main,Vectorized/solady)" + description: "Comma-separated repos to benchmark. Each entry: org/repo[:rev][ ] (e.g. vectorized/solady:v0.1.26 --nmc BrokenTest). Leave empty to use the per-benchmark default repo lists." required: false type: string - default: "ithacaxyz/account:v0.3.2,Vectorized/solady:v0.1.22" + default: "" env: - ITHACAXYZ_ACCOUNT: "ithacaxyz/account:v0.3.2" - VECTORIZED_SOLADY: "Vectorized/solady:v0.1.22" - DEFAULT_REPOS: "ithacaxyz/account:v0.3.2,Vectorized/solady:v0.1.22" + # Repos to benchmark per step. Each comma-separated entry has the form + # org/repo[:rev][ ] + # where anything after the first whitespace is appended to every benchmark + # command for that repo (use this to skip a broken test contract via e.g. + # `--nmc BrokenTest`, so a single failing test does not fail the whole CI). + TEST_REPOS: >- + ithacaxyz/account:v0.5.7, + vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest|Base58Test', + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', + sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting + + ISOLATE_TEST_REPOS: >- + ithacaxyz/account:v0.5.7 --nmc SimulateExecuteTest, + vectorized/solady:v0.1.26 --nmc 'SafeTransferLibTest|LifebuoyTest|LibBitTest|Base58Test', + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75 --nmc 'TickMathTestTest', + sparkdotfi/spark-psm:v1.0.0 --nmc PSMInvariants_TimeBasedRateSetting_WithTransfers_WithPocketSetting + + BUILD_REPOS: >- + ithacaxyz/account:v0.5.7, + vectorized/solady:v0.1.26, + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, + sparkdotfi/spark-psm:v1.0.0 + + COVERAGE_REPOS: >- + ithacaxyz/account:v0.5.7, + aave/aave-v4:v0.5.11, + uniswap/v4-core:46c6834698c48bc4a463a86d8420f4eb1d7f3b75, + sparkdotfi/spark-psm:v1.0.0 + RUSTC_WRAPPER: "sccache" jobs: @@ -48,7 +77,7 @@ jobs: with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 @@ -61,7 +90,7 @@ jobs: printf '%s\n' "$GITHUB_WORKSPACE/.foundry/bin" >> "$GITHUB_PATH" - name: Build benchmark binary - run: cargo build --release --bin foundry-bench + run: cargo build --locked --release --bin foundry-bench - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -78,7 +107,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.DEFAULT_REPOS }} + REPOS: ${{ github.event.inputs.repos || env.TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -90,7 +119,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.VECTORIZED_SOLADY }} + REPOS: ${{ github.event.inputs.repos || env.ISOLATE_TEST_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -102,7 +131,7 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} - REPOS: ${{ github.event.inputs.repos || env.DEFAULT_REPOS }} + REPOS: ${{ github.event.inputs.repos || env.BUILD_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ @@ -114,10 +143,11 @@ jobs: env: FOUNDRY_DIR: ${{ github.workspace }}/.foundry VERSIONS: ${{ github.event.inputs.versions || 'stable,nightly' }} + REPOS: ${{ github.event.inputs.repos || env.COVERAGE_REPOS }} run: | ./target/release/foundry-bench --output-dir ./benches --force-install \ --versions "$VERSIONS" \ - --repos ${{ env.ITHACAXYZ_ACCOUNT }} \ + --repos "$REPOS" \ --benchmarks forge_coverage \ --output-file forge_coverage_bench.md diff --git a/.github/workflows/bump-forge-std.yml b/.github/workflows/bump-forge-std.yml index 9cae3e0da1302..e95acd2881d31 100644 --- a/.github/workflows/bump-forge-std.yml +++ b/.github/workflows/bump-forge-std.yml @@ -23,7 +23,7 @@ jobs: - name: Fetch and update forge-std tag run: curl 'https://api.github.com/repos/foundry-rs/forge-std/tags' | jq '.[0].commit.sha' -jr > testdata/forge-std-rev - name: Create pull request - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: commit-message: "chore: bump forge-std version used for tests" title: "chore(tests): bump forge-std version" diff --git a/.github/workflows/ci-mpp.yml b/.github/workflows/ci-mpp.yml new file mode 100644 index 0000000000000..00ec35f8f8aba --- /dev/null +++ b/.github/workflows/ci-mpp.yml @@ -0,0 +1,55 @@ +name: CI MPP + +permissions: {} + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTC_WRAPPER: "sccache" + +jobs: + mpp-check: + runs-on: depot-ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + # Checkout the repository + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + + # Build and install binaries + - name: Build and install Foundry binaries + run: | + cargo build --profile dev --locked -p forge -p cast -p anvil -p chisel + echo "${{ github.workspace }}/target/debug" >> "$GITHUB_PATH" + + - name: Run MPP e2e test + env: + TEMPO_KEYS_TOML_B64: ${{ secrets.TEMPO_KEYS_TOML_B64 }} + MPP_API_KEY: ${{ secrets.MPP_API_KEY }} + MPP_DEPOSIT: "1000000" + run: | + if [ -z "${TEMPO_KEYS_TOML_B64:-}" ]; then + echo "::warning::TEMPO_KEYS_TOML_B64 secret not set, skipping MPP e2e" + exit 0 + fi + mkdir -p ~/.tempo/wallet + echo "$TEMPO_KEYS_TOML_B64" | tr -d '[:space:]' | base64 -d > ~/.tempo/wallet/keys.toml + ./.github/scripts/tempo-mpp.sh "$(which cast | xargs dirname)" diff --git a/.github/workflows/ci-tempo.yml b/.github/workflows/ci-tempo.yml index 942f595a34977..b4bc98391e72d 100644 --- a/.github/workflows/ci-tempo.yml +++ b/.github/workflows/ci-tempo.yml @@ -36,7 +36,7 @@ env: RUSTC_WRAPPER: "sccache" jobs: - sanity-check: + tempo-check: runs-on: depot-ubuntu-latest timeout-minutes: 60 permissions: @@ -59,21 +59,6 @@ jobs: cargo build --profile dev --locked -p forge -p cast -p anvil -p chisel echo "${{ github.workspace }}/target/debug" >> "$GITHUB_PATH" - - name: Run MPP e2e test - if: github.event_name == 'push' || github.event_name == 'pull_request' - env: - TEMPO_KEYS_TOML_B64: ${{ secrets.TEMPO_KEYS_TOML_B64 }} - MPP_API_KEY: ${{ secrets.MPP_API_KEY }} - MPP_DEPOSIT: "1000000" - run: | - if [ -z "${TEMPO_KEYS_TOML_B64:-}" ]; then - echo "::warning::TEMPO_KEYS_TOML_B64 secret not set, skipping MPP e2e" - exit 0 - fi - mkdir -p ~/.tempo/wallet - echo "$TEMPO_KEYS_TOML_B64" | tr -d '[:space:]' | base64 -d > ~/.tempo/wallet/keys.toml - ./.github/scripts/tempo-mpp.sh "$(which cast | xargs dirname)" - # TODO(upstream): re-enable when flaky devnet faucet is fixed # - name: Run Tempo check on devnet # if: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c332240fea036..9eb90a76cdbfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,10 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - run: cargo test --workspace --doc + - run: cargo test --workspace --doc --locked typos: runs-on: depot-ubuntu-latest @@ -61,7 +61,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d # v1.45.0 + - uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 shellcheck: runs-on: depot-ubuntu-latest @@ -89,10 +89,10 @@ jobs: with: toolchain: nightly components: clippy - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - run: cargo clippy --workspace --all-targets --all-features + - run: cargo clippy --workspace --all-targets --all-features --locked env: RUSTFLAGS: -Dwarnings @@ -123,7 +123,7 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: forge fmt @@ -154,12 +154,12 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/crate-checks.yml b/.github/workflows/crate-checks.yml index 781d63759045e..eb865bddc10e3 100644 --- a/.github/workflows/crate-checks.yml +++ b/.github/workflows/crate-checks.yml @@ -28,13 +28,13 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # v2.75.16 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: cargo-hack - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - run: cargo hack check + - run: cargo hack check --locked issue: name: Open an issue diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 3372e816328b7..5b7cf1631fea8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -108,3 +108,4 @@ jobs: labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 push: true + no-cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8a3dcbb8c7d3..45d00708394f5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,6 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: full - RUSTC_WRAPPER: "sccache" jobs: docs: @@ -30,11 +29,9 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: nightly - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Build documentation - run: cargo doc --workspace --all-features --no-deps --document-private-items + run: cargo doc --workspace --all-features --no-deps --document-private-items --locked env: RUSTDOCFLAGS: --cfg docsrs -D warnings --show-type-layout --generate-link-to-definition --enable-index-page -Zunstable-options - name: Setup Pages diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 9b9b798435c90..fdc5e5716c577 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -31,6 +31,7 @@ jobs: contents: read actions: read id-token: write + environment: release name: ${{ matrix.tool }}-${{ matrix.os }}-${{ matrix.arch }} runs-on: ubuntu-latest strategy: @@ -151,9 +152,6 @@ jobs: id: release-version working-directory: ./npm env: - PROVENANCE: true - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_REGISTRY_URL: ${{ env.NPM_REGISTRY_URL }} ARTIFACT_DIR: ${{ steps.paths.outputs.artifact_dir }} run: | @@ -224,8 +222,6 @@ jobs: PROVENANCE: true VERSION_NAME: ${{ steps.release-version.outputs.RELEASE_VERSION }} RELEASE_VERSION: ${{ steps.release-version.outputs.RELEASE_VERSION }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set -euo pipefail @@ -245,12 +241,11 @@ jobs: contents: read actions: read id-token: write + environment: release needs: publish-arch name: Publish Meta Package runs-on: ubuntu-latest env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_VERSION: ${{ needs.publish-arch.outputs.RELEASE_VERSION }} steps: - name: Checkout @@ -286,6 +281,4 @@ jobs: PROVENANCE: true VERSION_NAME: ${{ env.RELEASE_VERSION }} RELEASE_VERSION: ${{ env.RELEASE_VERSION }} - NPM_TOKEN: ${{ env.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ env.NODE_AUTH_TOKEN }} NPM_REGISTRY_URL: ${{ env.NPM_REGISTRY_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ad324e7ef9c1..9cb1cf51dda24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,12 +148,7 @@ jobs: with: toolchain: stable targets: ${{ matrix.target }} - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - - uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 - if: ${{ contains(matrix.runner, 'depot') }} - - run: printf 'RUSTC_WRAPPER=sccache\n' >> "$GITHUB_ENV" - if: ${{ contains(matrix.runner, 'depot') }} + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Apple M1 setup if: matrix.target == 'aarch64-apple-darwin' @@ -163,7 +158,7 @@ jobs: - name: cross setup if: contains(matrix.target, 'musl') - uses: taiki-e/cache-cargo-install-action@a8b9ecf8e0c0ea09d7481cfc583a5203ecd585b5 # v3.0.5 + uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6 with: git: https://github.com/cross-rs/cross rev: baf457efc2555225af47963475bd70e8d2f5993f @@ -179,7 +174,7 @@ jobs: shell: bash run: | set -eo pipefail - flags=(--target $TARGET --profile $RUST_PROFILE --bins + flags=(--locked --target $TARGET --profile $RUST_PROFILE --bins --no-default-features --features "$RUST_FEATURES") # `jemalloc` is not fully supported on MSVC or aarch64 Linux. @@ -279,7 +274,7 @@ jobs: # Creates the release for this specific version - name: Create release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: name: ${{ needs.prepare.outputs.release_name }} tag_name: ${{ needs.prepare.outputs.tag_name }} @@ -294,7 +289,7 @@ jobs: # tagged `nightly` for compatibility with `foundryup` - name: Update nightly release if: ${{ env.IS_NIGHTLY == 'true' }} - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: name: "Nightly" tag_name: "nightly" diff --git a/.github/workflows/test-flaky.yml b/.github/workflows/test-flaky.yml index af4415b1e8ba1..d6244f826887e 100644 --- a/.github/workflows/test-flaky.yml +++ b/.github/workflows/test-flaky.yml @@ -32,8 +32,8 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # v2.75.16 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -50,7 +50,7 @@ jobs: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_API_KEY }} ARBITRUM_RPC: ${{ secrets.ARBITRUM_RPC }} ETH_SEPOLIA_RPC: ${{ secrets.ETH_SEPOLIA_RPC }} - run: cargo nextest run --profile flaky --no-fail-fast + run: cargo nextest run --locked --profile flaky --no-fail-fast # If any of the jobs fail, this will create a normal-priority issue to signal so. issue: diff --git a/.github/workflows/test-isolate.yml b/.github/workflows/test-isolate.yml index afd91d3d72c05..141e3a049a73b 100644 --- a/.github/workflows/test-isolate.yml +++ b/.github/workflows/test-isolate.yml @@ -36,8 +36,8 @@ jobs: - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master with: toolchain: stable - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # v2.75.16 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -54,7 +54,7 @@ jobs: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_API_KEY }} ARBITRUM_RPC: ${{ secrets.ARBITRUM_RPC }} ETH_SEPOLIA_RPC: ${{ secrets.ETH_SEPOLIA_RPC }} - run: cargo nextest run --profile flaky --features=isolate-by-default --no-fail-fast + run: cargo nextest run --locked --profile flaky --features=isolate-by-default --no-fail-fast # If nextest fails, create a high-priority issue for isolation failures. issue-isolate: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9ccb850dbc5c..eaa46c8e79f7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,8 +72,8 @@ jobs: with: toolchain: stable target: ${{ matrix.target }} - - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - uses: taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # v2.75.16 + - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 + - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 with: tool: nextest @@ -97,7 +97,7 @@ jobs: run: pip --version && pip install vyper==0.4.3 - name: Foundry test cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.foundry/cache @@ -121,4 +121,4 @@ jobs: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_API_KEY }} ARBITRUM_RPC: ${{ secrets.ARBITRUM_RPC }} ETH_SEPOLIA_RPC: ${{ secrets.ETH_SEPOLIA_RPC }} - run: cargo nextest run ${{ matrix.flags }} + run: cargo nextest run --locked ${{ matrix.flags }} diff --git a/Cargo.lock b/Cargo.lock index d15f7c092e642..71d56653d7a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -77,21 +77,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85805c194576017df6c11057504e1d60b36f3913f8e365945486931f6ee81e40" +checksum = "a547705d5c1b42575a0542bae2ba45bc62a6154be86611afaef1c0ab5c38598e" dependencies = [ "alloy-consensus", "alloy-contract", "alloy-core", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-genesis", "alloy-network", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer", "alloy-signer-local", "alloy-transport", @@ -116,14 +116,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dbe4e5e9107bf6854e7550b666ca654ff2027eabf8153913e2e31ac4b089779" +checksum = "ae8c24c95e90c1608c2d91cff1b451d796474168d3310ccc8b7cd12502ca8169" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-trie", "alloy-tx-macros", "auto_impl", @@ -133,7 +133,7 @@ dependencies = [ "either", "k256", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "secp256k1 0.30.0", "serde", "serde_json", @@ -143,23 +143,23 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88fc7bbfb98cf5605a35aadf0ba43a7d9f1608d6f220d05e4fbd5144d3b0b625" +checksum = "7d211ad0ef468a70a7a829e49683ff59ad25f02b4ab3764344c4c2663329a52c" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", ] [[package]] name = "alloy-contract" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c16fa30b623e40a5b216da00f3b61870f5cbe863b59816ac1ecc2489515a40" +checksum = "c59d55233ac14aa7fa6bcdcad45ba305e90c556065e0947cd9f243c4469e7c2d" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -238,12 +238,12 @@ dependencies = [ [[package]] name = "alloy-eip5792" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9252074d8b7028bae6844827f51ed8f8a036367604dce9d30d16cedfcc53577" +checksum = "250ba1168b8a049185a68c4dfa7f2a6a4046bd26fcc8c68632caeb216a5e12dc" dependencies = [ "alloy-primitives", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", "serde_json", ] @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb4919fa34b268842f434bfafa9c09136ab7b1a87ce0dd40a61befa35b5408c" +checksum = "ae69eaa5096b47ffe97e6a5d6bde7e7fa2dec106af22a9315621d11039c3de3c" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -310,7 +310,7 @@ dependencies = [ "alloy-eip7928", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "auto_impl", "borsh", "c-kzg", @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "alloy-ens" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cbf60d85481b12fcc06e29fbe14584fdc5743a34ba7719037f210decd020761" +checksum = "34a8c1330ad33c95b5958573bca9a1ad0b419a51d76bb4c521556fbba8539b8d" dependencies = [ "alloy-contract", "alloy-primitives", @@ -339,12 +339,12 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.32.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f107d0e588e5d25fcf2db216390445d5804b875a22a419407ad0389b925bb4d" +checksum = "6fc4b83cb672156663e6094d098beb509965b7fe684bb3d6e44bb9ca2e9ae714" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-hardforks", "alloy-primitives", "alloy-rpc-types-engine", @@ -359,13 +359,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e111e22c1a2133e9ebfd9051ea0eaf63559594d2f50d43cbc6762fbb95fc3c2" +checksum = "39789db0b3f3bbef0e6549c87bc6842b73886ebabee1405b6941685b1cc34083" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-trie", "borsh", "serde", @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b6af6f374c1eeef8ab8dc26232cd440db167322a4207a3debd3d1ee565ca47" +checksum = "662b525af73e86b2167dae923261c8edf440ba7e1426b30a8b993177bc214c02" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -415,19 +415,19 @@ dependencies = [ [[package]] name = "alloy-network" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a3f5a7f3678b71d33fcc45b714fab8928dbc647d5aff2145e72032d5c849bb" +checksum = "c657c2d9751d3c7d94990554b231e5372c3c2e4bad842806280b6151a0d6a05d" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-any", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer", "alloy-sol-types", "async-trait", @@ -441,24 +441,24 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb50dc1fb0e0b2c8748d5bee1aa7acdd18f9e036311bc93a71d97be624030317" +checksum = "59e7c4bb0ebbd6d7406d2808968f43c0d5186c69c5e58cedcbee7380f4cd1fcf" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", ] [[package]] name = "alloy-op-evm" -version = "0.30.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +version = "0.31.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "alloy-op-hardforks" version = "0.4.7" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-chains", "alloy-hardforks", @@ -513,13 +513,13 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ba5468f78c8893be2d68a7f2fda61753336e5653f006af19781001b5f99e6c" +checksum = "c4fea0fc2628cdbc851aaa333124f9d8ab9f567ab8d4c20202819db13aa1a534" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-json-rpc", "alloy-network", "alloy-network-primitives", @@ -559,9 +559,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffcefb5d3391a320eadb95d398e4135f8cc35c7bf29a6bdb357eadcfc5ee5638" +checksum = "edc7b42e514613c717887dc77bb58d35e845557ebd63a18c3f92a77094e4891f" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -603,9 +603,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222fd4efff0fb9a25184684742c44fe9fa9a16c4ab5bf97583e71c86598ef8f0" +checksum = "d5ee7b51752c68fb95f21705e402700750e692b1d21ccc294ac48fadc8655d53" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -629,9 +629,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974df1e56405c27cb8242381f45d8b212ba9df5006046ccf704764a2a4634366" +checksum = "8fa76988f54105ad4398828e8aaf1a39b3f07f91fb79091529056689514ee8c2" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -640,44 +640,44 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-rpc-types-txpool", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", ] [[package]] name = "alloy-rpc-types-anvil" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06bc10b0dca4f5bfc3cd30ed46eab5d651b5bb2cd300d683bdcdf5d2bfe6e82c" +checksum = "3d276bea4e92e4991269d31b9abd3e722eed2565b82036478a4416adb8dd4992" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", ] [[package]] name = "alloy-rpc-types-any" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949c0f16a94ae33cdb1139b8dbf9e34d7f26ebfe97962e2a4d620b5f65f48fe4" +checksum = "1f1a9a3bda9be7f6515316eb792710532411878bbfc88934973f4b371376b00d" dependencies = [ "alloy-consensus-any", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", "serde_json", ] [[package]] name = "alloy-rpc-types-beacon" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8f7fa8ca056bb797a368aeed329e6ace6b62ee4271432ac36ab8ae87a5e60d" +checksum = "caf5d68ddca890854fb78291cbde06115473ded00b2337d0f815e92c0c1f8003" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rpc-types-engine", "derive_more", @@ -689,9 +689,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301249e3c9e43661cfd7ebbb4746a00af6ce1ef58b5c968451882cd60438417d" +checksum = "ea21739e232c221779741eba7e7b9bc19ad8ff777b72736647ae519f5c9f6f33" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -702,37 +702,37 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e59bc947935732cae5b072753e5e034c0b70a8b031c2839f45e2659ba07df9ae" +checksum = "5f05338cfb4ee5508ff76f01c88142cab8a4579db74b7d9432936c26e4f11374" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "derive_more", "ethereum_ssz", "ethereum_ssz_derive", "jsonwebtoken", - "rand 0.8.5", + "rand 0.8.6", "serde", "strum", ] [[package]] name = "alloy-rpc-types-eth" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc280a41931bd419af86e9e859dd9726b73313aaa2e479b33c0e344f4b892ddb" +checksum = "dda4ece0050154ab278241aeffade58916b04f38254832e8cb6e4671c6e72ed2" dependencies = [ "alloy-consensus", "alloy-consensus-any", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network-primitives", "alloy-primitives", "alloy-rlp", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-sol-types", "itertools 0.14.0", "serde", @@ -743,13 +743,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede0458c51bef23620aa6bd01a0b4f608be7bcb61d98e91b8530208ae545f3c2" +checksum = "f5905ac3663b0859d67b82d912acce20887d20682a0cadde79c8a763b133a515" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", "serde_json", "thiserror 2.0.18", @@ -757,13 +757,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f4df183248b57f3e0b99054b1b6786769d3fdff6d01a702234068140c8ba76" +checksum = "f7fbf71892d4df9cae8d35dc96f15d522384bb93806205465e2c8c012b7f0a34" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "serde", ] @@ -780,9 +780,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4848831ff994c88b1c32b7df9c4c1c3eedea4b535bde5eb3c421ef0bdc5ac052" +checksum = "beaa5c581a67e2743d95b4849eb9cfeb90866429cdaa6d8f6b75eb988b2d0cd9" dependencies = [ "alloy-primitives", "serde", @@ -791,9 +791,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b8ad9890b212e224291024b1aecfeef72127d27a2f6eebc5e347c40275c4bf" +checksum = "c5da9ae50f9b48d7b4e2e5cde87175257be7e5e56909a7794720597c1d9806f6" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -865,9 +865,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c67d2372aada343130d41e249b59a3cef29b1678dcd3fd80f1c2c4d6b5318f2" +checksum = "49b794002d57fd2f71b4c87298a41ca24dfc0f2cf6630d95106a477e451747ba" dependencies = [ "alloy-consensus", "alloy-network", @@ -878,7 +878,7 @@ dependencies = [ "coins-bip39", "eth-keystore", "k256", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", "zeroize", ] @@ -991,9 +991,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b7b755e64ae6b5de0d762ed2c780e072167ea5e542076a559e00314352a0bf" +checksum = "19dec9bfb59647254afdecbb5ddcddd7ba02edcd48ffa40510bddfbed0be1634" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -1014,9 +1014,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29980e69119444ed26b75e7ee5bed2043870f904a64318297e55800db686564" +checksum = "2035f3c4d6bee20624da2dcf765d469b292398e48d766ffade61b0fcf8b4d45d" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -1030,9 +1030,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b27802653330740c88c28394cdaf1d55190b15b48ef9b99a946f37c9cdb19fa" +checksum = "cfad7aa9206fcb831ae401b6a1c893a402b8eed74f9c8ffbb7a7323afb0d9a4c" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -1050,9 +1050,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b71dc951db66795cfb52eef835f64cf15163bc93b656e061b457ce5ebff370" +checksum = "a5aa8ff49386df3e008b73c7fb0a5479410e8493fdb86a8b916877a16e8aead9" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1085,9 +1085,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8228b9236479ff16b03041b64b86c2bd4e53da1caa45d59b5868cd1571131e" +checksum = "3520337f3d3d063a7fe20f47aaa62d695e3dc0372b34f601560dee24e76988b9" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1229,7 +1229,7 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-genesis", "alloy-network", @@ -1241,7 +1241,7 @@ dependencies = [ "alloy-rpc-types", "alloy-rpc-types-beacon", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -1274,7 +1274,7 @@ dependencies = [ "op-alloy-rpc-types", "op-revm", "parking_lot", - "rand 0.8.5", + "rand 0.8.6", "rand 0.9.4", "reqwest 0.13.2", "revm", @@ -1302,12 +1302,12 @@ dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-eip5792", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "bytes", "foundry-common", "foundry-evm", @@ -1630,7 +1630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1640,7 +1640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1650,7 +1650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2086,9 +2086,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -2111,11 +2111,12 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.6" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", @@ -2126,6 +2127,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "aws-smithy-types" version = "1.4.7" @@ -2380,6 +2392,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -2730,7 +2751,7 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-dyn-abi", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-ens", "alloy-evm", "alloy-hardforks", @@ -2742,7 +2763,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-beacon", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer", "alloy-signer-local", "alloy-sol-types", @@ -2764,6 +2785,7 @@ dependencies = [ "foundry-config", "foundry-debugger", "foundry-evm", + "foundry-evm-networks", "foundry-test-utils", "foundry-wallets", "futures", @@ -2771,7 +2793,7 @@ dependencies = [ "op-alloy-consensus", "op-alloy-flz", "op-alloy-network", - "rand 0.8.5", + "rand 0.8.6", "rand 0.9.4", "rayon", "regex", @@ -2934,7 +2956,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -3089,7 +3111,7 @@ dependencies = [ "hmac", "once_cell", "pbkdf2 0.12.2", - "rand 0.8.5", + "rand 0.8.6", "sha2 0.10.9", "thiserror 1.0.69", ] @@ -3214,7 +3236,7 @@ dependencies = [ "cfg-if", "commonware-macros", "paste", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "thiserror 2.0.18", ] @@ -3245,7 +3267,7 @@ dependencies = [ "num-rational", "num-traits", "p256", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_core 0.6.4", "sha2 0.10.9", @@ -3321,7 +3343,7 @@ dependencies = [ "num-traits", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", "tokio", "zeroize", @@ -3422,11 +3444,12 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -3635,6 +3658,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -3936,10 +3968,20 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", +] + [[package]] name = "dirs" version = "6.0.0" @@ -4284,7 +4326,7 @@ dependencies = [ "hex", "hmac", "pbkdf2 0.11.0", - "rand 0.8.5", + "rand 0.8.6", "scrypt", "serde", "serde_json", @@ -4376,6 +4418,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fast-float2" version = "0.2.3" @@ -4475,7 +4529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand 0.8.6", "rustc-hex", "static_assertions", ] @@ -4693,7 +4747,7 @@ dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-json-abi", "alloy-network", @@ -4830,15 +4884,15 @@ dependencies = [ [[package]] name = "foundry-block-explorers" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff814624bb21bfe43b70fb736ab39527b405d04cdc94d90b7e182fba28b25ec7" +checksum = "c41211d5c69c92070d2e2174eff99ffb259d0d568d2e4f789d2a7251b4416ccd" dependencies = [ "alloy-chains", "alloy-json-abi", "alloy-primitives", "foundry-compilers", - "reqwest 0.12.28", + "reqwest 0.13.2", "semver 1.0.28", "serde", "serde_json", @@ -4916,7 +4970,7 @@ version = "1.6.0" dependencies = [ "alloy-chains", "alloy-dyn-abi", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-ens", "alloy-json-abi", "alloy-network", @@ -4932,7 +4986,6 @@ dependencies = [ "dotenvy", "dunce", "eyre", - "foundry-block-explorers", "foundry-cli-markdown", "foundry-common", "foundry-compilers", @@ -4978,7 +5031,7 @@ dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-json-abi", "alloy-json-rpc", "alloy-network", @@ -4994,7 +5047,7 @@ dependencies = [ "alloy-transport", "alloy-transport-ipc", "alloy-transport-ws", - "anstream 1.0.0", + "anstream 0.6.21", "anstyle", "axum", "chrono", @@ -5009,6 +5062,8 @@ dependencies = [ "foundry-common-fmt", "foundry-compilers", "foundry-config", + "foundry-wallets", + "futures", "itertools 0.14.0", "jiff", "mpp", @@ -5019,6 +5074,7 @@ dependencies = [ "regex", "reqwest 0.13.2", "revm", + "rustls", "semver 1.0.28", "serde", "serde_json", @@ -5028,6 +5084,7 @@ dependencies = [ "tempo-primitives", "thiserror 2.0.18", "tokio", + "tokio-tungstenite 0.28.0", "toml", "tower", "tracing", @@ -5046,8 +5103,9 @@ dependencies = [ "alloy-network", "alloy-primitives", "alloy-rpc-types", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "chrono", + "comfy-table", "eyre", "foundry-macros", "op-alloy-consensus", @@ -5062,9 +5120,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.19.14" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e639f98fe54d1cc0011a4bdb2eb1d838b379c9f004991ae7555a4cc09e8da32a" +checksum = "8a46823427db611c6b97506ee8f26b4424058366abcc9a97ac5c4c3419798518" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5081,7 +5139,7 @@ dependencies = [ "semver 1.0.28", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "solar-compiler", "svm-rs", "svm-rs-builds", @@ -5094,9 +5152,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.19.14" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ec96df20055211f4e46b5a61fa479b2ea7d1ce0659818e0359afadfcded8d2" +checksum = "c29c6a49e82684909549e8e3a63a9dea4c92dfeb1b5b3a5e3ad683db42324ac3" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -5104,9 +5162,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.19.14" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a206e475b5dd1a77dc33cd917cde4846148f5136729a24edb3a16ab431b90a" +checksum = "02d3e554932001133f433473b3bdb37c74b4a648750ab61cd7966e7db0ead16d" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5125,27 +5183,25 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.19.14" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74883db8036522fa21d0853c21ac318e165ec88e141f1ef1d6f7b4dfa841ff7" +checksum = "583168b6df6b73cc3de8d205a922dca58a9c0b4c2774f6b115b3ff7e79d23900" dependencies = [ "alloy-json-abi", "alloy-primitives", "foundry-compilers-artifacts-solc", "foundry-compilers-core", - "path-slash", "semver 1.0.28", "serde", ] [[package]] name = "foundry-compilers-core" -version = "0.19.14" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab384daeaea5c33cad8c3c094a1eb6f98e70922e18380c660980c74c19e362b" +checksum = "b9f80dd5d9f3ed065e412ecef2da3904ea21708d53f79fac29a3547c10ae947f" dependencies = [ "alloy-primitives", - "cfg-if", "dunce", "fs_extra", "path-slash", @@ -5285,7 +5341,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-sol-types", "anvil", "auto_impl", @@ -5381,7 +5437,7 @@ name = "foundry-evm-networks" version = "1.6.0" dependencies = [ "alloy-chains", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-op-hardforks", "alloy-primitives", @@ -5389,6 +5445,7 @@ dependencies = [ "foundry-evm-hardforks", "revm", "serde", + "serde_json", ] [[package]] @@ -5431,9 +5488,9 @@ dependencies = [ [[package]] name = "foundry-fork-db" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fb3be6425c3f9c58761c96abf6511d3bc7275b85e060b253d81623351ce721" +checksum = "f2ce9e91abb900e4ab5346d06b97c3784ae6ed4fc44b9fa22d3dc712b7e21be9" dependencies = [ "alloy-chains", "alloy-primitives", @@ -5449,6 +5506,7 @@ dependencies = [ "tokio", "tracing", "url", + "zstd", ] [[package]] @@ -5485,7 +5543,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer", "derive_more", "op-alloy-consensus", @@ -5544,7 +5602,8 @@ dependencies = [ [[package]] name = "foundry-wallets" version = "0.1.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=7f401c1397af90a0a94ef7424a48bbf3dc0248cc#7f401c1397af90a0a94ef7424a48bbf3dc0248cc" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f2cc1ff7ea002addf41bd9c5785cd34f6a7b9658998c50035cc860ffebc3a9" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -5568,6 +5627,7 @@ dependencies = [ "eth-keystore", "eyre", "rpassword", + "rusqlite", "serde", "serde_json", "tempo-primitives", @@ -5970,6 +6030,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -6119,6 +6188,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -6155,7 +6233,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -6820,6 +6898,21 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "kqueue" version = "1.1.1" @@ -6920,6 +7013,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libusb1-sys" version = "0.7.0" @@ -7329,13 +7433,17 @@ dependencies = [ [[package]] name = "mpp" -version = "0.9.3" -source = "git+https://github.com/tempoxyz/mpp-rs?rev=310c9a1f3fe485fa9c7a877fd4da6b7a6ab34815#310c9a1f3fe485fa9c7a877fd4da6b7a6ab34815" +version = "0.10.0" +source = "git+https://github.com/tempoxyz/mpp-rs?rev=554d20112eb014bd223d54de7f152ca59b2aa4fd#554d20112eb014bd223d54de7f152ca59b2aa4fd" dependencies = [ "alloy", + "async-stream", "base64 0.22.1", + "futures-core", + "futures-util", "hex", "hmac", + "http 1.4.0", "rand 0.9.4", "reqwest 0.12.28", "serde", @@ -7346,6 +7454,8 @@ dependencies = [ "tempo-primitives", "thiserror 2.0.18", "time", + "tokio", + "tokio-tungstenite 0.26.2", "uuid 1.23.1", ] @@ -7709,7 +7819,7 @@ dependencies = [ [[package]] name = "op-alloy" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "op-alloy-consensus", "op-alloy-network", @@ -7721,19 +7831,19 @@ dependencies = [ [[package]] name = "op-alloy-consensus" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "bytes", "derive_more", - "reth-codecs 0.2.0", - "reth-zstd-compressors 0.2.0", + "reth-codecs", + "reth-zstd-compressors", "serde", "serde_with", "thiserror 2.0.18", @@ -7748,7 +7858,7 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-consensus", "alloy-network", @@ -7761,7 +7871,7 @@ dependencies = [ [[package]] name = "op-alloy-provider" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-network", "alloy-primitives", @@ -7775,18 +7885,18 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network", "alloy-network-primitives", "alloy-primitives", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "derive_more", "op-alloy-consensus", - "reth-rpc-traits 0.2.0", + "reth-rpc-traits", "serde", "serde_json", "thiserror 2.0.18", @@ -7795,14 +7905,14 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" version = "0.24.0" -source = "git+https://github.com/foundry-rs/optimism?branch=develop#0dc3c00bd1b3b2976f53503ba92fe84c602deca6" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-engine", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "derive_more", "ethereum_ssz", "ethereum_ssz_derive", @@ -7815,9 +7925,8 @@ dependencies = [ [[package]] name = "op-revm" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bcf58dcec47d4e9af496935af52dee3d4b35ede142b46371438fc0ac77434e" +version = "19.0.0" +source = "git+https://github.com/ethereum-optimism/optimism?rev=42f5117c2e7de0614cd3b96f274d0a3078f9701c#42f5117c2e7de0614cd3b96f274d0a3078f9701c" dependencies = [ "auto_impl", "revm", @@ -8103,7 +8212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -8227,9 +8336,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -8675,9 +8784,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -8979,7 +9088,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -9028,12 +9137,12 @@ dependencies = [ [[package]] name = "reth-chainspec" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-chains", "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-genesis", "alloy-primitives", @@ -9042,64 +9151,32 @@ dependencies = [ "derive_more", "reth-ethereum-forks", "reth-network-peers", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "serde_json", ] -[[package]] -name = "reth-codecs" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29541038ab108b2e9d527c66b565e717e252e4eef6675377fc21f9ba587f792" -dependencies = [ - "alloy-consensus", - "alloy-eips 2.0.0", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "bytes", - "modular-bitfield", - "parity-scale-codec", - "reth-codecs-derive 0.2.0", - "reth-zstd-compressors 0.2.0", - "serde", -] - [[package]] name = "reth-codecs" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79b3247ae4fbb1d4d35ce83a11fc596428a4c6ea836c98a75a55340192578a4" +source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-genesis", "alloy-primitives", "alloy-trie", "bytes", "modular-bitfield", "parity-scale-codec", - "reth-codecs-derive 0.3.0", - "reth-zstd-compressors 0.3.0", + "reth-codecs-derive", + "reth-zstd-compressors", "serde", ] -[[package]] -name = "reth-codecs-derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5646d4aff98bd51050fc920bc3ffdff209f2343def9ed31b56eea13c4245c4da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "reth-codecs-derive" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5dbae40c272b8a1b4fcc08ee2d4e77d3b0ccdb187c1313f412a73ff54ff2a2" +source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" dependencies = [ "proc-macro2", "quote", @@ -9108,34 +9185,34 @@ dependencies = [ [[package]] name = "reth-consensus" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", "alloy-primitives", "auto_impl", "reth-execution-types", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "thiserror 2.0.18", ] [[package]] name = "reth-consensus-common" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "reth-chainspec", "reth-consensus", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", ] [[package]] name = "reth-db-api" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9144,10 +9221,10 @@ dependencies = [ "derive_more", "metrics", "modular-bitfield", - "reth-codecs 0.3.0", + "reth-codecs", "reth-db-models", "reth-ethereum-primitives", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-prune-types", "reth-stages-types", "reth-storage-errors", @@ -9158,38 +9235,38 @@ dependencies = [ [[package]] name = "reth-db-models" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "bytes", "modular-bitfield", - "reth-codecs 0.3.0", - "reth-primitives-traits 0.3.0", + "reth-codecs", + "reth-primitives-traits", "serde", ] [[package]] name = "reth-ethereum-consensus" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "reth-chainspec", "reth-consensus", "reth-consensus-common", "reth-execution-types", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "tracing", ] [[package]] name = "reth-ethereum-forks" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -9201,25 +9278,25 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rpc-types-eth", - "reth-codecs 0.3.0", - "reth-primitives-traits 0.3.0", + "reth-codecs", + "reth-primitives-traits", "serde", ] [[package]] name = "reth-evm" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-primitives", "auto_impl", @@ -9228,7 +9305,7 @@ dependencies = [ "rayon", "reth-execution-errors", "reth-execution-types", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-storage-api", "reth-storage-errors", "reth-trie-common", @@ -9237,11 +9314,11 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-primitives", "alloy-rpc-types-engine", @@ -9250,15 +9327,15 @@ dependencies = [ "reth-ethereum-primitives", "reth-evm", "reth-execution-types", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-storage-errors", "revm", ] [[package]] name = "reth-execution-errors" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-evm", "alloy-primitives", @@ -9270,17 +9347,17 @@ dependencies = [ [[package]] name = "reth-execution-types" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-primitives", "alloy-rlp", "derive_more", "reth-ethereum-primitives", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-trie-common", "revm", "serde", @@ -9289,8 +9366,8 @@ dependencies = [ [[package]] name = "reth-network-peers" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9300,39 +9377,13 @@ dependencies = [ "url", ] -[[package]] -name = "reth-primitives-traits" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96ffdb2ce0cdcd814d39428dc1e9b660c85d85e0b75eb465a1ed0943a48c7bd" -dependencies = [ - "alloy-consensus", - "alloy-eips 2.0.0", - "alloy-genesis", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-trie", - "bytes", - "derive_more", - "once_cell", - "quanta", - "revm-bytecode 9.0.0", - "revm-primitives 22.1.0", - "revm-state 10.0.0", - "secp256k1 0.30.0", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "reth-primitives-traits" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc759fd87c3f65440e5d3bfa3107fe8a13a61a6807cd485c62c49d63c7bf6717" +source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-genesis", "alloy-primitives", "alloy-rlp", @@ -9345,10 +9396,10 @@ dependencies = [ "modular-bitfield", "once_cell", "quanta", - "reth-codecs 0.3.0", - "revm-bytecode 10.0.0", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "reth-codecs", + "revm-bytecode", + "revm-primitives", + "revm-state", "secp256k1 0.30.0", "serde", "thiserror 2.0.18", @@ -9356,13 +9407,13 @@ dependencies = [ [[package]] name = "reth-prune-types" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-primitives", "derive_more", "modular-bitfield", - "reth-codecs 0.3.0", + "reth-codecs", "serde", "strum", "thiserror 2.0.18", @@ -9371,12 +9422,12 @@ dependencies = [ [[package]] name = "reth-revm" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-primitives", "alloy-rlp", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-storage-api", "reth-storage-errors", "revm", @@ -9384,8 +9435,8 @@ dependencies = [ [[package]] name = "reth-rpc-convert" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9397,23 +9448,8 @@ dependencies = [ "dyn-clone", "jsonrpsee-types", "reth-evm", - "reth-primitives-traits 0.3.0", - "reth-rpc-traits 0.3.0", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-rpc-traits" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c870f120b2e179e44906b7b288b0dea6577573010272adda8fff536e86fedd05" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-signer", - "reth-primitives-traits 0.2.0", + "reth-primitives-traits", + "reth-rpc-traits", "thiserror 2.0.18", ] @@ -9428,27 +9464,27 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", "alloy-signer", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "thiserror 2.0.18", ] [[package]] name = "reth-stages-types" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-primitives", "bytes", "modular-bitfield", - "reth-codecs 0.3.0", + "reth-codecs", "reth-trie-common", "serde", ] [[package]] name = "reth-static-file-types" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-primitives", "derive_more", @@ -9461,11 +9497,11 @@ dependencies = [ [[package]] name = "reth-storage-api" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rpc-types-engine", "auto_impl", @@ -9473,7 +9509,7 @@ dependencies = [ "reth-db-models", "reth-ethereum-primitives", "reth-execution-types", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-prune-types", "reth-stages-types", "reth-storage-errors", @@ -9484,70 +9520,60 @@ dependencies = [ [[package]] name = "reth-storage-errors" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-primitives", "alloy-rlp", "derive_more", - "reth-codecs 0.3.0", - "reth-primitives-traits 0.3.0", + "reth-codecs", + "reth-primitives-traits", "reth-prune-types", "reth-static-file-types", "revm-database-interface", - "revm-state 11.0.0", + "revm-state", "thiserror 2.0.18", ] [[package]] name = "reth-trie-common" -version = "2.0.0" -source = "git+https://github.com/paradigmxyz/reth?rev=0b33057#0b33057414c8815ed499e0aa837ae52d4d366150" +version = "2.1.0" +source = "git+https://github.com/paradigmxyz/reth?rev=7839f3d#7839f3d876b32842b059ca8171242b807ba1fc80" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-trie", "arrayvec", "bytes", "derive_more", "itertools 0.14.0", "nybbles", - "reth-codecs 0.3.0", - "reth-primitives-traits 0.3.0", + "reth-codecs", + "reth-primitives-traits", "revm-database", "serde", "serde_with", ] -[[package]] -name = "reth-zstd-compressors" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3882441cf1d51fe24dfc6df9919e6f17edfdb6b121050bd34348e925e61c7af1" -dependencies = [ - "zstd", -] - [[package]] name = "reth-zstd-compressors" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fa54c341d926ec9b0f7fc0c87f831aa4959de699e69caab1a0bfd914326c09" +source = "git+https://github.com/paradigmxyz/reth-core?rev=6b12498#6b12498871bc1b1d42c6dcf28968c271660de8c0" dependencies = [ "zstd", ] [[package]] name = "revm" -version = "37.0.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11353f234577a63048066df974d8a56e8c090d4de8b5f7d5f2a0d0c3a1ffaa2d" +checksum = "91202d39dbe8e8d10e9e8f2b76c30da68ecd1d25be69ba6d853ad0d03a3a398a" dependencies = [ - "revm-bytecode 10.0.0", + "revm-bytecode", "revm-context", "revm-context-interface", "revm-database", @@ -9556,20 +9582,8 @@ dependencies = [ "revm-inspector", "revm-interpreter", "revm-precompile", - "revm-primitives 23.0.0", - "revm-state 11.0.0", -] - -[[package]] -name = "revm-bytecode" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86e468df3cf5cf59fa7ef71a3e9ccabb76bb336401ea2c0674f563104cf3c5e" -dependencies = [ - "bitvec", - "phf 0.13.1", - "revm-primitives 22.1.0", - "serde", + "revm-primitives", + "revm-state", ] [[package]] @@ -9580,95 +9594,95 @@ checksum = "bdbb3a3d735efa94c91f2ef6bf20a35f99a77bc78f3e25bd758336901bdf9661" dependencies = [ "bitvec", "phf 0.13.1", - "revm-primitives 23.0.0", + "revm-primitives", "serde", ] [[package]] name = "revm-context" -version = "16.0.0" +version = "16.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0e3d41127977e351ed795689147e6e59b0fa88e387840f921e46c440bb2fb44" +checksum = "c5f68d928d8b228e0faeb1c6ed75c4fde7d124f1ddf9119b67e7a0ad4041237d" dependencies = [ "bitvec", "cfg-if", "derive-where", - "revm-bytecode 10.0.0", + "revm-bytecode", "revm-context-interface", "revm-database-interface", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-context-interface" -version = "17.0.0" +version = "17.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd61f0f2f646ae74a75e12e7f1d57554868e2dc5c83fc9a761cb95735cb58309" +checksum = "1f3758e6167c4ba7a59a689c519a047edaefcd4c37d74f279b93ed87bc8aece4" dependencies = [ "alloy-eip2930", "alloy-eip7702", "auto_impl", "either", "revm-database-interface", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-database" -version = "13.0.0" +version = "13.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f97178ab32358be770d09649d6fa86303923cab7afb95577353e7305a04193" +checksum = "c281a1f11d3bcb8c0bba1199ed6bcb001d1aeb3d4fb366819e14f88723989a4e" dependencies = [ "alloy-eips 1.8.3", - "revm-bytecode 10.0.0", + "revm-bytecode", "revm-database-interface", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-database-interface" -version = "11.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e48ddde8f5ef2adaf1e920ccbf57fa6ef2c63c11e3e37d7d9137657873eecc" +checksum = "d89efb9832a4e3742bb4ded5f7fe5bf905e8860e69427d4dfec153484fc6d304" dependencies = [ "auto_impl", "either", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", "thiserror 2.0.18", ] [[package]] name = "revm-handler" -version = "18.0.0" +version = "18.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18b03319aee860ecc4631c9180abb0828480f5a4c39507fc735a417bd906984" +checksum = "783e903d6922b7f5f9a940d1bb229530502d2924b1aed9d5ca5a94ebf065d460" dependencies = [ "auto_impl", "derive-where", - "revm-bytecode 10.0.0", + "revm-bytecode", "revm-context", "revm-context-interface", "revm-database-interface", "revm-interpreter", "revm-precompile", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-inspector" -version = "18.0.0" +version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33e8327376dff859eb18dea3623aa55ef5d580fe38fa0fd4bb92bd95ace60ad" +checksum = "8216ad58422090d0daa9eb430e0a081f7ad07e7fd30681dee71f8420c99624e0" dependencies = [ "auto_impl", "either", @@ -9676,17 +9690,17 @@ dependencies = [ "revm-database-interface", "revm-handler", "revm-interpreter", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", "serde_json", ] [[package]] name = "revm-inspectors" -version = "0.38.1" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e41c7dc3d85c6186e57ec1438a426e7ddfac579d8255930cf8ed324a6c317e79" +checksum = "731b682530a732ef9c189ef831589128e2ce34d4a306c956322ae2dffe009715" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -9704,22 +9718,22 @@ dependencies = [ [[package]] name = "revm-interpreter" -version = "35.0.0" +version = "35.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5f438f47d40d9830c0498fa3ca16a447b3148ab7b78742cbb1b27a51a50963" +checksum = "1ece9f41b69658c15d748288a4dbdfc06a63f3ce93d983af440de3f1631dce6a" dependencies = [ - "revm-bytecode 10.0.0", + "revm-bytecode", "revm-context-interface", - "revm-primitives 23.0.0", - "revm-state 11.0.0", + "revm-primitives", + "revm-state", "serde", ] [[package]] name = "revm-precompile" -version = "33.0.0" +version = "34.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c07bfcc9361f7b23970a68cbd17bfe255e70182cfb1a8896d06832217fc5439" +checksum = "a346a8cc6c8c39bd65306641c692191299c0a7b63d38810e39e8fe9b92378660" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -9734,24 +9748,12 @@ dependencies = [ "k256", "p256", "revm-context-interface", - "revm-primitives 23.0.0", + "revm-primitives", "ripemd", "secp256k1 0.31.1", "sha2 0.10.9", ] -[[package]] -name = "revm-primitives" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfb5ce6cf18b118932bcdb7da05cd9c250f2cb9f64131396b55f3fe3537c35" -dependencies = [ - "alloy-primitives", - "num_enum", - "once_cell", - "serde", -] - [[package]] name = "revm-primitives" version = "23.0.0" @@ -9766,27 +9768,14 @@ dependencies = [ [[package]] name = "revm-state" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29404707763da607e5d6e4771cb203998c28159279c2f64cc32de08d2814651" -dependencies = [ - "alloy-eip7928", - "bitflags 2.11.1", - "revm-bytecode 9.0.0", - "revm-primitives 22.1.0", - "serde", -] - -[[package]] -name = "revm-state" -version = "11.0.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e480426a7d76b458789e4a1be3ffbce9df798f0145f0520c1cdf967755cfcbf" +checksum = "c32490ed687dba31c3c882beb8c20408bdd30ef96690d8f145b0ee9a87040bfe" dependencies = [ "alloy-eip7928", "bitflags 2.11.1", - "revm-bytecode 10.0.0", - "revm-primitives 23.0.0", + "revm-bytecode", + "revm-primitives", "serde", ] @@ -9905,7 +9894,7 @@ dependencies = [ "parity-scale-codec", "primitive-types", "proptest", - "rand 0.8.5", + "rand 0.8.6", "rand 0.9.4", "rlp", "ruint-macro", @@ -9930,6 +9919,20 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rusqlite" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -9942,7 +9945,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -10070,9 +10073,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -10251,7 +10254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys 0.10.1", "serde", ] @@ -10551,6 +10554,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + [[package]] name = "sha3" version = "0.10.8" @@ -11295,16 +11309,16 @@ dependencies = [ [[package]] name = "tempo-alloy" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-consensus", "alloy-contract", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "alloy-signer-local", "alloy-sol-types", "alloy-transport", @@ -11322,9 +11336,9 @@ dependencies = [ [[package]] name = "tempo-chainspec" version = "1.5.3" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-evm", "alloy-genesis", "alloy-hardforks", @@ -11341,23 +11355,25 @@ dependencies = [ [[package]] name = "tempo-consensus" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-consensus", "alloy-evm", + "alloy-primitives", "reth-chainspec", "reth-consensus", "reth-consensus-common", "reth-ethereum-consensus", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "tempo-chainspec", "tempo-primitives", + "thiserror 2.0.18", ] [[package]] name = "tempo-contracts" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-contract", "alloy-primitives", @@ -11368,7 +11384,7 @@ dependencies = [ [[package]] name = "tempo-evm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11381,7 +11397,7 @@ dependencies = [ "reth-consensus", "reth-evm", "reth-evm-ethereum", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-revm", "tempo-chainspec", "tempo-consensus", @@ -11395,7 +11411,7 @@ dependencies = [ [[package]] name = "tempo-precompiles" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy", "alloy-evm", @@ -11415,7 +11431,7 @@ dependencies = [ [[package]] name = "tempo-precompiles-macros" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy", "proc-macro2", @@ -11426,15 +11442,15 @@ dependencies = [ [[package]] name = "tempo-primitives" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-consensus", - "alloy-eips 2.0.0", + "alloy-eips 2.0.1", "alloy-network", "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-serde 2.0.0", + "alloy-serde 2.0.1", "aws-lc-rs", "base64 0.22.1", "derive_more", @@ -11442,10 +11458,10 @@ dependencies = [ "modular-bitfield", "once_cell", "p256", - "reth-codecs 0.3.0", + "reth-codecs", "reth-db-api", "reth-ethereum-primitives", - "reth-primitives-traits 0.3.0", + "reth-primitives-traits", "reth-rpc-convert", "revm", "serde", @@ -11457,7 +11473,7 @@ dependencies = [ [[package]] name = "tempo-revm" version = "1.6.0" -source = "git+https://github.com/tempoxyz/tempo?rev=bb08bb9#bb08bb905b6ee13fafe046aa9531aea2cdf60651" +source = "git+https://github.com/tempoxyz/tempo?rev=1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69#1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11681,9 +11697,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -11729,6 +11745,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.26.2", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -11886,7 +11914,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -12100,6 +12128,23 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -12326,7 +12371,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -12793,9 +12838,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", "jni 0.22.4", @@ -12809,9 +12854,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -12822,14 +12867,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index f902786070d81..c27286e7d6ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,53 +338,51 @@ foundry-evm-sancov = { path = "crates/evm/sancov" } foundry-evm-traces = { path = "crates/evm/traces" } foundry-macros = { path = "crates/macros" } foundry-test-utils = { path = "crates/test-utils" } +foundry-wallets = { version = "0.1.0", default-features = false } foundry-linking = { path = "crates/linking" } foundry-primitives = { path = "crates/primitives" } # solc & compilation utilities -foundry-block-explorers = { version = "0.22.0", default-features = false } -foundry-compilers = { version = "0.19.14", default-features = false, features = [ +foundry-block-explorers = { version = "0.23.0", default-features = false } +foundry-compilers = { version = "0.20.0", default-features = false, features = [ "rustls", "svm-solc", ] } -foundry-fork-db = "0.25.0" +foundry-fork-db = { version = "0.26.0", features = ["zstd"] } solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar = { package = "solar-compiler", version = "=0.1.8", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ "rustls", ] } -## foundry-core -foundry-wallets = { git = "https://github.com/foundry-rs/foundry-core", rev = "7f401c1397af90a0a94ef7424a48bbf3dc0248cc", default-features = false } - ## alloy -alloy-consensus = { version = "2.0.0", default-features = false } -alloy-contract = { version = "2.0.0", default-features = false } -alloy-eips = { version = "2.0.0", default-features = false } -alloy-eip5792 = { version = "2.0.0", default-features = false } -alloy-ens = { version = "2.0.0", default-features = false } -alloy-genesis = { version = "2.0.0", default-features = false } -alloy-json-rpc = { version = "2.0.0", default-features = false } -alloy-network = { version = "2.0.0", default-features = false } -alloy-provider = { version = "2.0.0", default-features = false } -alloy-pubsub = { version = "2.0.0", default-features = false } -alloy-rpc-client = { version = "2.0.0", default-features = false } -alloy-rpc-types = { version = "2.0.0", default-features = true } -alloy-rpc-types-beacon = { version = "2.0.0", default-features = true } -alloy-rpc-types-engine = { version = "2.0.0", default-features = false } -alloy-rpc-types-eth = { version = "2.0.0", default-features = false } -alloy-serde = { version = "2.0.0", default-features = false } -alloy-signer = { version = "2.0.0", default-features = false } -alloy-signer-aws = { version = "2.0.0", default-features = false } -alloy-signer-gcp = { version = "2.0.0", default-features = false } -alloy-signer-ledger = { version = "2.0.0", default-features = false } -alloy-signer-local = { version = "2.0.0", default-features = false } -alloy-signer-trezor = { version = "2.0.0", default-features = false } -alloy-signer-turnkey = { version = "2.0.0", default-features = false } -alloy-transport = { version = "2.0.0", default-features = false } -alloy-transport-http = { version = "2.0.0", default-features = false } -alloy-transport-ipc = { version = "2.0.0", default-features = false } -alloy-transport-ws = { version = "2.0.0", default-features = false } +alloy-consensus = { version = "2.0.1", default-features = false } +alloy-contract = { version = "2.0.1", default-features = false } +alloy-eips = { version = "2.0.1", default-features = false } +alloy-eip5792 = { version = "2.0.1", default-features = false } +alloy-ens = { version = "2.0.1", default-features = false } +alloy-genesis = { version = "2.0.1", default-features = false } +alloy-json-rpc = { version = "2.0.1", default-features = false } +alloy-network = { version = "2.0.1", default-features = false } +alloy-provider = { version = "2.0.1", default-features = false } +alloy-pubsub = { version = "2.0.1", default-features = false } +alloy-rpc-client = { version = "2.0.1", default-features = false } +alloy-rpc-types = { version = "2.0.1", default-features = true } +alloy-rpc-types-beacon = { version = "2.0.1", default-features = true } +alloy-rpc-types-engine = { version = "2.0.1", default-features = false } +alloy-rpc-types-eth = { version = "2.0.1", default-features = false } +alloy-serde = { version = "2.0.1", default-features = false } +alloy-signer = { version = "2.0.1", default-features = false } +alloy-signer-aws = { version = "2.0.1", default-features = false } +alloy-signer-gcp = { version = "2.0.1", default-features = false } +alloy-signer-ledger = { version = "2.0.1", default-features = false } +alloy-signer-local = { version = "2.0.1", default-features = false } +alloy-signer-trezor = { version = "2.0.1", default-features = false } +alloy-signer-turnkey = { version = "2.0.1", default-features = false } +alloy-transport = { version = "2.0.1", default-features = false } +alloy-transport-http = { version = "2.0.1", default-features = false } +alloy-transport-ipc = { version = "2.0.1", default-features = false } +alloy-transport-ws = { version = "2.0.1", default-features = false } alloy-hardforks = { version = "0.4.7", default-features = false } alloy-op-hardforks = { version = "0.4.7", default-features = false } @@ -412,16 +410,16 @@ op-alloy-rpc-types = "0.24.0" op-alloy-flz = "0.13.1" ## alloy-evm -alloy-evm = "0.32.0" -alloy-op-evm = "0.30.0" +alloy-evm = "0.33.2" +alloy-op-evm = "0.31.0" # revm -revm = { version = "37.0.0", default-features = false } -revm-inspectors = { version = "0.38.1", features = ["serde"] } -op-revm = { version = "18.0.0", default-features = false } +revm = { version = "38.0.0", default-features = false } +revm-inspectors = { version = "0.39.0", features = ["serde"] } +op-revm = { version = "19.0.0", default-features = false } ## cli -anstream = "1.0" +anstream = "0.6" anstyle = "1.0" dialoguer = { version = "0.12", default-features = false, features = [ "password", @@ -487,6 +485,7 @@ soldeer-core = { version = "=0.10.1", features = ["serde"] } strum = "0.27" tempfile = "3.23" tokio = "1" +tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] } toml = "0.9" toml_edit = "0.24" tower = "0.5" @@ -510,21 +509,23 @@ flate2 = "1.1" ethereum_ssz = "0.10" # Tempo -mpp = { git = "https://github.com/tempoxyz/mpp-rs", rev = "310c9a1f3fe485fa9c7a877fd4da6b7a6ab34815", default-features = false, features = [ +mpp = { git = "https://github.com/tempoxyz/mpp-rs", rev = "554d20112eb014bd223d54de7f152ca59b2aa4fd", default-features = false, features = [ "tempo", "client", "reqwest-rustls-tls", + "ws", +] } +tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69", default-features = false } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69", default-features = false, features = [ + "serde", ] } -tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9", default-features = false } -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9", default-features = false, features = [ +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69", default-features = false } +tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69", default-features = false } +tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69", default-features = false, features = [ "serde", - "reth-codec", ] } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9", default-features = false } -tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9", default-features = false } -tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9", default-features = false, features = ["serde"] } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9" } -tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" } +tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" } ## Pinned dependencies. Enabled for the workspace in crates/test-utils. @@ -589,34 +590,32 @@ rexpect = { git = "https://github.com/rust-cli/rexpect", rev = "2ed0b1898d7edaf6 # alloy-evm = { git = "https://github.com/paradigmxyz/evm.git", rev = "04d8e4a" } ## reth-core -# reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "af2174c" } -# reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "af2174c" } -# reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "af2174c" } -# reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "af2174c" } - -## op-alloy / alloy-op-evm -op-alloy-consensus = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } -op-alloy-network = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } -op-alloy-rpc-types = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } -op-alloy = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } -alloy-op-evm = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } -alloy-op-hardforks = { git = "https://github.com/foundry-rs/optimism", branch = "develop" } - -## revm -# revm = { git = "https://github.com/bluealloy/revm.git", rev = "7e59936" } -# op-revm = { git = "https://github.com/bluealloy/revm.git", rev = "7e59936" } -# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors.git", rev = "22dcee2" } +reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } +reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } +reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } +reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "6b12498" } + +## op-revm / op-alloy / alloy-op-evm +op-revm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +op-alloy-network = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +op-alloy-rpc-types = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } +alloy-op-hardforks = { git = "https://github.com/ethereum-optimism/optimism", rev = "42f5117c2e7de0614cd3b96f274d0a3078f9701c" } ## foundry-fork-db -# foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-fork-db", rev = "b139c57c2b54bc06a9e4c9783941f5bbd4bd3a1f" } +# foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "2f90eb86d4549fa15a8cc2d99bfc1039bc083977" } ## tempo — unify crates.io versions (pulled by mpp) with git rev -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9" } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9" } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "bb08bb9" } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" } +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "1e5a82dff207e2cbd6cecdcfb8ff3cd2e39baa69" } # solar solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } solar-interface = { package = "solar-interface", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } solar-ast = { package = "solar-ast", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } solar-sema = { package = "solar-sema", git = "https://github.com/paradigmxyz/solar", rev = "530f129" } + +[workspace.metadata.cargo-shear] +ignored = ["idna_adapter", "cast", "chisel", "forge", "alloy-contract"] diff --git a/Makefile b/Makefile index fe768f19abf43..1bf20f4eeebd1 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ help: ## Display this help. .PHONY: build build: ## Build the project. - cargo build --features "$(FEATURES)" --profile "$(PROFILE)" + cargo build --locked --features "$(FEATURES)" --profile "$(PROFILE)" .PHONY: build-docker build-docker: ## Build the docker image. @@ -47,17 +47,17 @@ build-docker: ## Build the docker image. ## is unstable (https://github.com/taiki-e/cargo-llvm-cov/issues/2). .PHONY: test-coverage test-coverage: - cargo +nightly llvm-cov --no-report nextest -E 'kind(test) & !test(/\b(issue|ext_integration|flaky_)/)' && \ - cargo +nightly llvm-cov --no-report --doc && \ + cargo +nightly llvm-cov --no-report nextest --locked -E 'kind(test) & !test(/\b(issue|ext_integration|flaky_)/)' && \ + cargo +nightly llvm-cov --no-report --doc --locked && \ cargo +nightly llvm-cov report --doctests --open .PHONY: test-unit test-unit: ## Run unit tests. - cargo nextest run -E 'kind(test) & !test(/\b(issue|ext_integration|flaky_)/)' + cargo nextest run --workspace --locked -E 'kind(test) & !test(/\b(issue|ext_integration|flaky_)/)' .PHONY: test-doc test-doc: ## Run doc tests. - cargo test --doc --workspace + cargo test --doc --workspace --locked .PHONY: test test: ## Run all tests. @@ -77,6 +77,7 @@ lint-clippy: ## Run clippy on the codebase. --workspace \ --all-targets \ --all-features \ + --locked \ -- -D warnings .PHONY: lint-clippy-fix @@ -85,6 +86,7 @@ lint-clippy-fix: ## Run clippy on the codebase and fix warnings. --workspace \ --all-targets \ --all-features \ + --locked \ --fix \ --allow-dirty \ --allow-staged \ @@ -104,21 +106,46 @@ lint: ## Run all linters. $(MAKE) lint-clippy && \ $(MAKE) lint-typos +##@ Documentation + +.PHONY: doc +doc: ## Build the documentation. + RUSTDOCFLAGS="--cfg docsrs -D warnings -Zunstable-options --show-type-layout --generate-link-to-definition" \ + cargo +nightly doc \ + --workspace \ + --all-features \ + --document-private-items \ + --no-deps \ + --locked + ##@ Other +.PHONY: lock +lock: ## Update the Cargo.lock file with the current dependencies. + cargo fetch + .PHONY: clean clean: ## Clean the project. cargo clean .PHONY: deny deny: ## Perform a `cargo` deny check. - cargo deny --all-features check all + cargo deny --locked --all-features check all + +.PHONY: check +check: ## Run a feature check on all crates and binaries. + cargo hack check --locked --feature-powerset --depth 1 + +.PHONY: shear +shear: ## Run `cargo shear` to check for unused dependencies. + cargo shear --locked .PHONY: pr pr: ## Run all checks and tests. $(MAKE) deny && \ $(MAKE) lint && \ - $(MAKE) test + $(MAKE) test && \ + $(MAKE) doc # dprint formatting commands .PHONY: dprint-fmt diff --git a/SECURITY.md b/SECURITY.md index bea27ad1140c0..d84327cc18e91 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,4 +2,4 @@ ## Reporting a Vulnerability -Contact [security@ithaca.xyz](mailto:security@ithaca.xyz). +Contact [security@tempo.xyz](mailto:security@tempo.xyz). diff --git a/benches/LATEST.md b/benches/LATEST.md index 7ea1049a2ac41..238a691229389 100644 --- a/benches/LATEST.md +++ b/benches/LATEST.md @@ -1,6 +1,6 @@ # Foundry Benchmark Results -**Date**: 2025-10-02 12:14:23 +**Date**: 2026-04-24 23:10:24 ## Repositories Tested @@ -8,66 +8,67 @@ 2. [Vectorized/solady](https://github.com/Vectorized/solady) 3. [Uniswap/v4-core](https://github.com/Uniswap/v4-core) 4. [sparkdotfi/spark-psm](https://github.com/sparkdotfi/spark-psm) +5. [aave/aave-v4](https://github.com/aave/aave-v4) ## Foundry Versions -- **v1.3.6**: forge Version: 1.3.6-v1.3.6 (d241588 2025-09-16) -- **v1.4.0-rc1**: forge Version: 1.4.0-v1.4.0-rc1 (bd0e4a7 2025-10-01) +- **v1.5.1**: forge Version: 1.5.1-v1.5.1 (b0a9dd9 2025-12-19) +- **nightly**: forge Version: 1.6.0-nightly (a249f5c 2026-04-24) ## Forge Test -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 3.17 s | 2.94 s | -| solady | 2.28 s | 2.10 s | -| Uniswap-v4-core | 7.27 s | 6.13 s | -| sparkdotfi-spark-psm | 43.04 s | 44.08 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| vectorized-solady | 1.46 s | 1.38 s | +| aave-aave-v4 | 4m 14.2s | 3m 29.1s | ## Forge Fuzz Test -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------ | ---------- | -| ithacaxyz-account | 3.18 s | 3.02 s | -| solady | 2.39 s | 2.24 s | -| Uniswap-v4-core | 6.84 s | 6.20 s | -| sparkdotfi-spark-psm | 3.07 s | 2.72 s | +| Repository | v1.5.1 | nightly | +| -------------------- | --------- | -------- | +| ithacaxyz-account | 2.81 s | 1.59 s | +| vectorized-solady | 1.40 s | 1.34 s | +| Uniswap-v4-core | 3.01 s | 2.87 s | +| sparkdotfi-spark-psm | 2.04 s | 1.87 s | +| aave-aave-v4 | 3m 46.0s | 3m 17.3s | ## Forge Test (Isolated) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| solady | 2.26 s | 2.41 s | -| Uniswap-v4-core | 7.22 s | 7.71 s | -| sparkdotfi-spark-psm | 45.53 s | 50.49 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| Uniswap-v4-core | 3.50 s | 3.48 s. | +| aave-aave-v4 | 4m 14.0s | 3m 53.4s | ## Forge Build (No Cache) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 9.16 s | 9.08 s | -| solady | 14.62 s | 14.69 s | -| Uniswap-v4-core | 2m 3.8s | 2m 5.3s | -| sparkdotfi-spark-psm | 13.17 s | 13.14 s | +| Repository | v1.5.1 | nightly | +| -------------------- | -------- | -------- | +| ithacaxyz-account | 26.06 s | 26.61 s | +| vectorized-solady | 14.20 s | 14.26 s | +| Uniswap-v4-core | 2m 1.3s | 2m 5.0s | +| sparkdotfi-spark-psm | 15.16 s | 15.30 s | +| aave-aave-v4 | 3m 37.0s | 3m 35.1s | ## Forge Build (With Cache) -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | ------- | ---------- | -| ithacaxyz-account | 0.156 s | 0.113 s | -| solady | 0.089 s | 0.094 s | -| Uniswap-v4-core | 0.133 s | 0.127 s | -| sparkdotfi-spark-psm | 0.173 s | 0.131 s | +| Repository | v1.5.1 | nightly | +| -------------------- | ------- | ------- | +| ithacaxyz-account | 0.167 s | 0.201 s | +| vectorized-solady | 0.099 s | 0.098 s | +| Uniswap-v4-core | 0.139 s | 0.140 s | +| sparkdotfi-spark-psm | 0.168 s | 0.173 s | +| aave-aave-v4 | 0.370 s | 0.357 s | ## Forge Coverage -| Repository | v1.3.6 | v1.4.0-rc1 | -| -------------------- | -------- | ---------- | -| ithacaxyz-account | 14.91 s | 13.34 s | -| Uniswap-v4-core | 1m 34.8s | 1m 30.3s | -| sparkdotfi-spark-psm | 3m 49.3s | 3m 40.2s | +| Repository | v1.5.1 | nightly | +| -------------------- | --------- | ---------- | +| Uniswap-v4-core | 1m 13.9s | 1m 10.3s | +| sparkdotfi-spark-psm | 2m 54.7s | 2m 50.0s | +| aave-aave-v4 | 11m 20.8s | 10m 58.7s | ## System Information - **OS**: macos -- **CPU**: 8 -- **Rustc**: rustc 1.90.0-nightly (3014e79f9 2025-07-15) +- **CPU**: 12 +- **Rustc**: rustc 1.95.0 (59807616e 2026-04-14) \ No newline at end of file diff --git a/benches/src/lib.rs b/benches/src/lib.rs index ab3bec614e3cd..7ed8807cbf0f5 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -24,46 +24,53 @@ pub struct RepoConfig { pub org: String, pub repo: String, pub rev: String, + /// Optional extra arguments appended to every benchmark command for this + /// repo (e.g. `--nmc BrokenTest` to skip a broken test contract). + pub extra_args: Option, } impl FromStr for RepoConfig { type Err = eyre::Error; + /// Parse a repo spec of the form `org/repo[:rev][ ]`. + /// + /// Anything after the first whitespace is treated as extra arguments + /// appended to every benchmark command for this repo. fn from_str(spec: &str) -> Result { - // Split by ':' first to separate repo path from optional rev - let parts: Vec<&str> = spec.splitn(2, ':').collect(); - let repo_path = parts[0]; - let custom_rev = parts.get(1).copied(); - - // Now split the repo path by '/' - let path_parts: Vec<&str> = repo_path.split('/').collect(); - if path_parts.len() != 2 { - eyre::bail!("Invalid repo format '{}'. Expected 'org/repo' or 'org/repo:rev'", spec); - } - - let org = path_parts[0]; - let repo = path_parts[1]; + let spec = spec.trim(); + // Anything after the first whitespace is per-repo extra args. + let (head, extra_args) = match spec.split_once(char::is_whitespace) { + Some((head, rest)) => (head, Some(rest.trim().to_string())), + None => (spec, None), + }; - // Try to find this repo in BENCHMARK_REPOS to get the full config - let existing_config = BENCHMARK_REPOS.iter().find(|r| r.org == org && r.repo == repo); + let (repo_path, custom_rev) = match head.split_once(':') { + Some((path, rev)) => (path, Some(rev)), + None => (head, None), + }; - let config = if let Some(existing) = existing_config { - // Use existing config but allow custom rev to override - let mut config = existing.clone(); - if let Some(rev) = custom_rev { - config.rev = rev.to_string(); - } - config - } else { - // Create new config with custom rev or default - // Name should follow the format: org-repo (with hyphen) - Self { + let (org, repo) = repo_path.split_once('/').ok_or_else(|| { + eyre::eyre!("Invalid repo format '{spec}'. Expected 'org/repo' or 'org/repo:rev'") + })?; + + // Inherit defaults from BENCHMARK_REPOS when available, otherwise build + // a fresh config. Custom rev / extra args always override. + let mut config = BENCHMARK_REPOS + .iter() + .find(|r| r.org == org && r.repo == repo) + .cloned() + .unwrap_or_else(|| Self { name: format!("{org}-{repo}"), org: org.to_string(), repo: repo.to_string(), - rev: custom_rev.unwrap_or("main").to_string(), - } - }; + rev: "main".to_string(), + extra_args: None, + }); + + if let Some(rev) = custom_rev { + config.rev = rev.to_string(); + } + config.extra_args = extra_args; let _ = sh_println!("Parsed repo spec '{spec}' -> {config:?}"); Ok(config) @@ -78,12 +85,14 @@ pub fn default_benchmark_repos() -> Vec { org: "ithacaxyz".to_string(), repo: "account".to_string(), rev: "main".to_string(), + extra_args: None, }, RepoConfig { name: "solady".to_string(), org: "Vectorized".to_string(), repo: "solady".to_string(), rev: "main".to_string(), + extra_args: None, }, ] } @@ -113,6 +122,8 @@ pub struct BenchmarkProject { pub name: String, pub temp_project: TempProject, pub root_path: PathBuf, + /// Optional extra arguments appended to every benchmark command. + pub extra_args: Option, } impl BenchmarkProject { @@ -169,7 +180,20 @@ impl BenchmarkProject { Self::install_npm_dependencies(&root_path)?; sh_println!(" ✅ Project {} setup complete at {}", config.name, root); - Ok(Self { name: config.name.clone(), root_path, temp_project }) + Ok(Self { + name: config.name.clone(), + root_path, + temp_project, + extra_args: config.extra_args.clone(), + }) + } + + /// Append `self.extra_args` to a benchmark shell command, if any. + fn cmd(&self, base: &str) -> String { + match self.extra_args.as_deref().map(str::trim).filter(|s| !s.is_empty()) { + Some(extra) => format!("{base} {extra}"), + None => base.to_string(), + } } /// Install npm dependencies if package.json exists @@ -296,7 +320,7 @@ impl BenchmarkProject { self.hyperfine( "forge_test", version, - "forge test", + &self.cmd("forge test"), runs, Some("forge build"), None, @@ -315,7 +339,7 @@ impl BenchmarkProject { self.hyperfine( "forge_build_with_cache", version, - "FOUNDRY_LINT_LINT_ON_BUILD=false forge build", + &self.cmd("FOUNDRY_LINT_LINT_ON_BUILD=false forge build"), runs, None, Some("forge build"), @@ -335,7 +359,7 @@ impl BenchmarkProject { self.hyperfine( "forge_build_no_cache", version, - "FOUNDRY_LINT_LINT_ON_BUILD=false forge build", + &self.cmd("FOUNDRY_LINT_LINT_ON_BUILD=false forge build"), runs, Some("forge clean"), None, @@ -355,7 +379,7 @@ impl BenchmarkProject { self.hyperfine( "forge_fuzz_test", version, - r#"forge test --match-test "test[^(]*\([^)]+\)""#, + &self.cmd(r#"forge test --match-test "test[^(]*\([^)]+\)""#), runs, Some("forge build"), None, @@ -376,7 +400,7 @@ impl BenchmarkProject { self.hyperfine( "forge_coverage", version, - "forge coverage --ir-minimum", + &self.cmd("forge coverage --ir-minimum"), runs, None, None, @@ -396,7 +420,7 @@ impl BenchmarkProject { self.hyperfine( "forge_isolate_test", version, - "forge test --isolate", + &self.cmd("forge test --isolate"), runs, Some("forge build"), None, diff --git a/benches/src/main.rs b/benches/src/main.rs index f3361bf30b6e6..60e815cecb0ec 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -48,8 +48,16 @@ struct Cli { #[clap(long, value_delimiter = ',')] benchmarks: Option>, - /// Run only on specific repositories (comma-separated in org/repo[:rev] format: - /// ithacaxyz/account,Vectorized/solady:main,foundry-rs/foundry:v1.0.0) + /// Comma-separated list of repositories to benchmark. + /// + /// Each entry has the form `org/repo[:rev][ ]`. Anything + /// after the first whitespace is appended to every benchmark command for + /// that repo (handy to skip a broken test contract via e.g. + /// `--nmc BrokenTest`). + /// + /// Examples: + /// `ithacaxyz/account:v0.5.7` + /// `vectorized/solady:v0.1.26 --nmc 'LifebuoyTest|LibBitTest'` #[clap(long, value_delimiter = ',')] repos: Option>, } diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 1b5e231124a78..d2a10885bb74d 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -318,6 +318,14 @@ pub enum EthRequest { #[serde(rename = "debug_dbGet")] DebugDbGet(String), + /// geth's `debug_traceBlockByHash` endpoint + #[serde(rename = "debug_traceBlockByHash")] + DebugTraceBlockByHash(B256, #[serde(default)] GethDebugTracingOptions), + + /// geth's `debug_traceBlockByNumber` endpoint + #[serde(rename = "debug_traceBlockByNumber")] + DebugTraceBlockByNumber(BlockNumber, #[serde(default)] GethDebugTracingOptions), + /// Trace transaction endpoint for parity's `trace_transaction` #[serde(rename = "trace_transaction", with = "sequence")] TraceTransaction(B256), @@ -716,6 +724,26 @@ pub enum EthRequest { /// Rollback the chain #[serde(rename = "anvil_rollback", with = "sequence")] Rollback(Option), + + /// Sets the fee token for a user (Tempo-only) + #[serde(rename = "anvil_setFeeToken")] + SetFeeToken(Address, Address), + + /// Sets the fee token for a validator (Tempo-only) + #[serde(rename = "anvil_setValidatorFeeToken")] + SetValidatorFeeToken(Address, Address), + + /// Mints FeeAMM liquidity for a token pair (Tempo-only) + #[serde(rename = "anvil_setFeeAmmLiquidity")] + SetFeeAmmLiquidity( + /// user_token + Address, + /// validator_token + Address, + /// amount + #[serde(deserialize_with = "deserialize_number")] + U256, + ), } /// Represents ethereum JSON-RPC API diff --git a/crates/anvil/server/src/handler.rs b/crates/anvil/server/src/handler.rs index a04c2d647f105..250c486986240 100644 --- a/crates/anvil/server/src/handler.rs +++ b/crates/anvil/server/src/handler.rs @@ -7,6 +7,8 @@ use anvil_rpc::{ use axum::{ Json, extract::{State, rejection::JsonRejection}, + http::StatusCode, + response::{IntoResponse, Response as AxumResponse}, }; use futures::{FutureExt, future}; @@ -15,16 +17,18 @@ use futures::{FutureExt, future}; pub async fn handle( State((handler, _)): State<(Http, Ws)>, request: Result, JsonRejection>, -) -> Json { - Json(match request { +) -> AxumResponse { + match request { Ok(Json(req)) => handle_request(req, handler) .await - .unwrap_or_else(|| Response::error(RpcError::invalid_request())), + .map(Json) + .map(IntoResponse::into_response) + .unwrap_or_else(|| StatusCode::NO_CONTENT.into_response()), Err(err) => { warn!(target: "rpc", ?err, "invalid request"); - Response::error(RpcError::invalid_request()) + Json(Response::error(RpcError::invalid_request())).into_response() } - }) + } } /// Handle the JSON-RPC [Request] @@ -44,6 +48,9 @@ pub async fn handle_request( match req { Request::Single(call) => handle_call(call, handler).await.map(Response::Single), Request::Batch(calls) => { + if calls.is_empty() { + return Some(Response::error(RpcError::invalid_request())); + } future::join_all(calls.into_iter().map(move |call| handle_call(call, handler.clone()))) .map(responses_as_batch) .await @@ -68,3 +75,72 @@ async fn handle_call(call: RpcCall, handler: Handler) -> Op } } } + +#[cfg(test)] +mod tests { + use super::*; + use anvil_rpc::{ + request::{RequestParams, RpcNotification, Version}, + response::ResponseResult, + }; + use axum::body::to_bytes; + use std::{ + pin::pin, + task::{Context, Poll, Waker}, + }; + + #[derive(Clone)] + struct TestHandler; + + #[async_trait::async_trait] + impl RpcHandler for TestHandler { + type Request = serde_json::Value; + + async fn on_request(&self, request: Self::Request) -> ResponseResult { + ResponseResult::success(request) + } + } + + fn notification() -> RpcCall { + RpcCall::Notification(RpcNotification { + jsonrpc: Some(Version::V2), + method: "eth_subscribe".to_owned(), + params: RequestParams::None, + }) + } + + fn run_ready(future: F) -> F::Output { + let waker = Waker::noop(); + let mut cx = Context::from_waker(waker); + let mut future = pin!(future); + match future.as_mut().poll(&mut cx) { + Poll::Ready(output) => output, + Poll::Pending => panic!("future unexpectedly pending"), + } + } + + #[test] + fn empty_batch_returns_invalid_request() { + let response = run_ready(handle_request(Request::Batch(vec![]), TestHandler)); + + assert_eq!(response, Some(Response::error(RpcError::invalid_request()))); + } + + #[test] + fn notification_only_batch_returns_no_response() { + let response = run_ready(handle_request(Request::Batch(vec![notification()]), TestHandler)); + + assert_eq!(response, None); + } + + #[test] + fn http_notification_only_batch_returns_no_content() { + let response = run_ready(handle( + State((TestHandler, ())), + Ok(Json(Request::Batch(vec![notification()]))), + )); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + assert!(run_ready(to_bytes(response.into_body(), usize::MAX)).unwrap().is_empty()); + } +} diff --git a/crates/anvil/server/src/pubsub.rs b/crates/anvil/server/src/pubsub.rs index ef02ceefb2d61..55c774505c9b7 100644 --- a/crates/anvil/server/src/pubsub.rs +++ b/crates/anvil/server/src/pubsub.rs @@ -117,7 +117,7 @@ pub struct PubSubConnection { /// The established connection connection: Connection, /// currently in progress requests - processing: Vec + Send>>>, + processing: Vec> + Send>>>, /// pending messages to send pending: VecDeque, } @@ -142,12 +142,10 @@ impl PubSubConnection handle_request(req, handler) - .await - .unwrap_or_else(|| Response::error(RpcError::invalid_request())), + Ok(req) => handle_request(req, handler).await, Err(err) => { error!(target: "rpc", ?err, "invalid request"); - Response::error(RpcError::invalid_request()) + Some(Response::error(RpcError::invalid_request())) } } })); @@ -223,12 +221,13 @@ where let mut req = pin.processing.swap_remove(n); #[allow(clippy::collapsible_match)] match req.poll_unpin(cx) { - Poll::Ready(resp) => { + Poll::Ready(Some(resp)) => { if let Ok(text) = serde_json::to_string(&resp) { pin.pending.push_back(text); progress = true; } } + Poll::Ready(None) => {} Poll::Pending => pin.processing.push(req), } } @@ -262,3 +261,70 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use anvil_rpc::{ + request::{RequestParams, RpcCall, RpcNotification, Version}, + response::RpcResponse, + }; + use std::{pin::pin, task::Waker}; + + #[derive(Clone)] + struct TestHandler; + + #[async_trait::async_trait] + impl PubSubRpcHandler for TestHandler { + type Request = serde_json::Value; + type SubscriptionId = u64; + type Subscription = futures::stream::Empty; + + async fn on_request( + &self, + _request: Self::Request, + _cx: PubSubContext, + ) -> ResponseResult { + ResponseResult::success(serde_json::Value::Null) + } + } + + fn notification() -> RpcCall { + RpcCall::Notification(RpcNotification { + jsonrpc: Some(Version::V2), + method: "eth_subscribe".to_owned(), + params: RequestParams::None, + }) + } + + fn run_ready(future: F) -> F::Output { + let waker = Waker::noop(); + let mut cx = Context::from_waker(waker); + let mut future = pin!(future); + match future.as_mut().poll(&mut cx) { + Poll::Ready(output) => output, + Poll::Pending => panic!("future unexpectedly pending"), + } + } + + #[test] + fn process_request_keeps_empty_batch_invalid() { + let mut connection = PubSubConnection::new((), TestHandler); + connection.process_request(Ok(Request::Batch(vec![]))); + + let response = run_ready(connection.processing.pop().unwrap()); + assert_eq!( + response, + Some(Response::Single(RpcResponse::from(RpcError::invalid_request()))) + ); + } + + #[test] + fn process_request_skips_notification_only_batch_response() { + let mut connection = PubSubConnection::new((), TestHandler); + connection.process_request(Ok(Request::Batch(vec![notification()]))); + + let response = run_ready(connection.processing.pop().unwrap()); + assert_eq!(response, None); + } +} diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 0a0fd97d06556..78cce938318c1 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -58,7 +58,7 @@ use alloy_rpc_types::{ state::{AccountOverride, EvmOverrides, StateOverridesBuilder}, trace::{ filter::TraceFilter, - geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace}, + geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TraceResult}, parity::{LocalizedTransactionTrace, TraceResultsWithTransactionHash, TraceType}, }, txpool::{TxpoolContent, TxpoolInspect, TxpoolInspectSummary, TxpoolStatus}, @@ -1673,6 +1673,12 @@ impl EthApi { self.debug_code_by_hash(hash, block).await.to_rpc_result() } EthRequest::DebugDbGet(key) => self.debug_db_get(key).await.to_rpc_result(), + EthRequest::DebugTraceBlockByHash(block_hash, opts) => { + self.debug_trace_block_by_hash(block_hash, opts).await.to_rpc_result() + } + EthRequest::DebugTraceBlockByNumber(block_number, opts) => { + self.debug_trace_block_by_number(block_number, opts).await.to_rpc_result() + } EthRequest::TraceTransaction(tx) => self.trace_transaction(tx).await.to_rpc_result(), EthRequest::TraceBlock(block) => self.trace_block(block).await.to_rpc_result(), EthRequest::TraceFilter(filter) => self.trace_filter(filter).await.to_rpc_result(), @@ -1853,6 +1859,16 @@ impl EthApi { self.anvil_reorg(reorg_options).await.to_rpc_result() } EthRequest::Rollback(depth) => self.anvil_rollback(depth).await.to_rpc_result(), + EthRequest::SetFeeToken(user, token) => { + self.anvil_set_fee_token(user, token).await.to_rpc_result() + } + EthRequest::SetValidatorFeeToken(validator, token) => { + self.anvil_set_validator_fee_token(validator, token).await.to_rpc_result() + } + EthRequest::SetFeeAmmLiquidity(user_token, validator_token, amount) => self + .anvil_set_fee_amm_liquidity(user_token, validator_token, amount) + .await + .to_rpc_result(), }; if let ResponseResult::Error(err) = &response { @@ -2752,6 +2768,30 @@ impl EthApi { self.backend.debug_trace_transaction(tx_hash, opts).await } + /// Returns traces for all transactions in a block by hash. + /// + /// Handler for RPC call: `debug_traceBlockByHash` + pub async fn debug_trace_block_by_hash( + &self, + block_hash: B256, + opts: GethDebugTracingOptions, + ) -> Result> { + node_info!("debug_traceBlockByHash"); + self.backend.debug_trace_block_by_hash(block_hash, opts).await + } + + /// Returns traces for all transactions in a block by number. + /// + /// Handler for RPC call: `debug_traceBlockByNumber` + pub async fn debug_trace_block_by_number( + &self, + block_number: BlockNumber, + opts: GethDebugTracingOptions, + ) -> Result> { + node_info!("debug_traceBlockByNumber"); + self.backend.debug_trace_block_by_number(block_number, opts).await + } + /// Returns traces for the transaction for geth's tracing endpoint /// /// Handler for RPC call: `debug_traceCall` @@ -3189,7 +3229,7 @@ impl EthApi { entry.insert(key, convert(pending)); } for queued in self.pool.pending_transactions() { - let entry = inspect.pending.entry(*queued.pending_transaction.sender()).or_default(); + let entry = inspect.queued.entry(*queued.pending_transaction.sender()).or_default(); let key = queued.pending_transaction.nonce().to_string(); entry.insert(key, convert(queued)); } @@ -3232,7 +3272,7 @@ impl EthApi { entry.insert(key, convert(pending)?); } for queued in self.pool.pending_transactions() { - let entry = content.pending.entry(*queued.pending_transaction.sender()).or_default(); + let entry = content.queued.entry(*queued.pending_transaction.sender()).or_default(); let key = queued.pending_transaction.nonce().to_string(); entry.insert(key, convert(queued)?); } @@ -3526,10 +3566,64 @@ impl EthApi { FoundryTxEnvelope::Eip4844(_) => self.backend.ensure_eip4844_active(), FoundryTxEnvelope::Eip7702(_) => self.backend.ensure_eip7702_active(), FoundryTxEnvelope::Deposit(_) => self.backend.ensure_op_deposits_active(), + FoundryTxEnvelope::PostExec(_) => Err(BlockchainError::InvalidTransactionRequest( + "not implemented for post-exec tx".to_string(), + )), FoundryTxEnvelope::Legacy(_) => Ok(()), FoundryTxEnvelope::Tempo(_) => self.backend.ensure_tempo_active(), } } + + /// Sets the fee token for a user address. + /// + /// Handler for RPC call: `anvil_setFeeToken` + /// + /// Only supported when running in Tempo mode (`--tempo`). + pub async fn anvil_set_fee_token(&self, user: Address, token: Address) -> Result<()> { + node_info!("anvil_setFeeToken"); + if !self.backend.is_tempo() { + return Err(BlockchainError::RpcUnimplemented); + } + self.backend.set_fee_token(user, token).await?; + Ok(()) + } + + /// Sets the fee token for a validator address. + /// + /// Handler for RPC call: `anvil_setValidatorFeeToken` + /// + /// Only supported when running in Tempo mode (`--tempo`). + pub async fn anvil_set_validator_fee_token( + &self, + validator: Address, + token: Address, + ) -> Result<()> { + node_info!("anvil_setValidatorFeeToken"); + if !self.backend.is_tempo() { + return Err(BlockchainError::RpcUnimplemented); + } + self.backend.set_validator_fee_token(validator, token).await?; + Ok(()) + } + + /// Mints FeeAMM liquidity for a token pair. + /// + /// Handler for RPC call: `anvil_setFeeAmmLiquidity` + /// + /// Only supported when running in Tempo mode (`--tempo`). + pub async fn anvil_set_fee_amm_liquidity( + &self, + user_token: Address, + validator_token: Address, + amount: U256, + ) -> Result<()> { + node_info!("anvil_setFeeAmmLiquidity"); + if !self.backend.is_tempo() { + return Err(BlockchainError::RpcUnimplemented); + } + self.backend.set_fee_amm_liquidity(user_token, validator_token, amount).await?; + Ok(()) + } } fn required_marker(provided_nonce: u64, on_chain_nonce: u64, from: Address) -> Vec { diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 8da9bdfb3756a..76d2d0f9ba804 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -70,6 +70,7 @@ impl ReceiptBuilder for FoundryReceiptBuilder { FoundryTxType::Deposit => { unreachable!("deposit receipts are built in commit_transaction") } + FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt), FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt), } } diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index bc87fe2fc2052..4f260023602f4 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -20,7 +20,7 @@ use alloy_rpc_types::{ FeeHistory, Filter, Log, simulate::{SimulatePayload, SimulatedBlock}, trace::{ - geth::{GethDebugTracingOptions, GethTrace}, + geth::{GethDebugTracingOptions, GethTrace, TraceResult}, parity::{LocalizedTransactionTrace as Trace, TraceResultsWithTransactionHash, TraceType}, }, }; @@ -242,6 +242,36 @@ impl ClientFork { self.provider().debug_code_by_hash(code_hash, block_id).await } + pub async fn debug_trace_block_by_hash( + &self, + block_hash: B256, + opts: GethDebugTracingOptions, + ) -> Result, TransportError> { + if let Some(traces) = self.storage_read().geth_block_traces.get(&block_hash).cloned() { + return Ok(traces); + } + + let trace_results = self.provider().debug_trace_block_by_hash(block_hash, opts).await?; + + let mut storage = self.storage_write(); + storage.geth_block_traces.insert(block_hash, trace_results.clone()); + + Ok(trace_results) + } + + pub async fn debug_trace_block_by_number( + &self, + number: u64, + opts: GethDebugTracingOptions, + ) -> Result, TransportError> { + if let Ok(Some(block)) = self.provider().get_block_by_number(number.into()).await { + let block_hash = block.header().hash(); + return self.debug_trace_block_by_hash(block_hash, opts).await; + } + + self.provider().debug_trace_block_by_number(number.into(), opts).await + } + pub async fn trace_block(&self, number: u64) -> Result, TransportError> { if let Some(traces) = self.storage_read().block_traces.get(&number).cloned() { return Ok(traces); @@ -732,6 +762,7 @@ pub struct ForkedStorage { pub transaction_traces: FbHashMap<32, Vec>, pub logs: HashMap>, pub geth_transaction_traces: FbHashMap<32, GethTrace>, + pub geth_block_traces: FbHashMap<32, Vec>, pub block_traces: HashMap>, pub block_receipts: HashMap>, pub code_at: HashMap<(Address, u64), Bytes>, @@ -748,6 +779,7 @@ impl Default for ForkedStorage { transaction_traces: Default::default(), logs: Default::default(), geth_transaction_traces: Default::default(), + geth_block_traces: Default::default(), block_traces: Default::default(), block_receipts: Default::default(), code_at: Default::default(), diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index b538086fe6f10..f404031445611 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -19,6 +19,7 @@ use crate::{ storage::MinedTransactionReceipt, }, notifications::{NewBlockNotification, NewBlockNotifications}, + tempo::AnvilStorageProvider, time::{TimeManager, utc_from_secs}, validate::TransactionValidator, }, @@ -56,7 +57,7 @@ use alloy_network::{ AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, Network, NetworkTransactionBuilder, ReceiptResponse, UnknownTxEnvelope, UnknownTypedTransaction, }; -use alloy_op_evm::{OpEvmFactory, OpTx}; +use alloy_op_evm::{OpEvmContext, OpEvmFactory, OpTx}; use alloy_primitives::{ Address, B256, Bloom, Bytes, TxHash, TxKind, U64, U256, hex, keccak256, logs_bloom, map::{AddressMap, HashMap, HashSet}, @@ -75,6 +76,7 @@ use alloy_rpc_types::{ geth::{ FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, NoopFrame, + TraceResult, }, parity::{LocalizedTransactionTrace, TraceResultsWithTransactionHash, TraceType}, }, @@ -112,7 +114,7 @@ use foundry_primitives::{ }; use futures::channel::mpsc::{UnboundedSender, unbounded}; use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait}; -use op_revm::{OpContext, OpHaltReason, OpSpecId, OpTransaction}; +use op_revm::{OpHaltReason, OpSpecId, OpTransaction}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ DatabaseCommit, Inspector, @@ -139,6 +141,11 @@ use std::{ use storage::{Blockchain, DEFAULT_HISTORY_LIMIT, MinedTransaction}; use tempo_chainspec::hardfork::TempoHardfork; use tempo_evm::evm::TempoEvmFactory; +use tempo_precompiles::{ + storage::StorageCtx, + tip_fee_manager::{IFeeManager, TipFeeManager}, + tip20::{ISSUER_ROLE, ITIP20, TIP20Token}, +}; use tempo_primitives::TEMPO_TX_TYPE_ID; use tempo_revm::{ TempoBatchCallEnv, TempoBlockEnv, TempoHaltReason, TempoTxEnv, evm::TempoContext, @@ -483,12 +490,12 @@ impl Backend { } /// Returns true if op-stack deposits are active - pub const fn is_optimism(&self) -> bool { + pub fn is_optimism(&self) -> bool { self.networks.is_optimism() } /// Returns true if Tempo network mode is active - pub const fn is_tempo(&self) -> bool { + pub fn is_tempo(&self) -> bool { self.networks.is_tempo() } @@ -582,7 +589,7 @@ impl Backend { } /// Returns an error if op-stack deposits are not active - pub const fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { + pub fn ensure_op_deposits_active(&self) -> Result<(), BlockchainError> { if self.is_optimism() { return Ok(()); } @@ -590,7 +597,7 @@ impl Backend { } /// Returns an error if Tempo transactions are not active - pub const fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { + pub fn ensure_tempo_active(&self) -> Result<(), BlockchainError> { if self.is_tempo() { return Ok(()); } @@ -1137,7 +1144,7 @@ impl Backend { where DB: DatabaseRef + ?Sized, I: Inspector>> - + Inspector>> + + Inspector>> + Inspector>>, WrapDatabaseRef<&'db DB>: Database, { @@ -1197,7 +1204,7 @@ impl Backend { where DB: DatabaseRef + ?Sized, I: Inspector>> - + Inspector>> + + Inspector>> + Inspector>>, WrapDatabaseRef<&'db DB>: Database, { @@ -3181,7 +3188,7 @@ where ) -> Result where for<'a> I: Inspector>>>> - + Inspector>>>> + + Inspector>>>> + Inspector>>>> + 'a, for<'a> F: @@ -3472,6 +3479,72 @@ where Ok(GethTrace::Default(Default::default())) } + /// Returns geth-style traces for all transactions in a block by hash. + pub async fn debug_trace_block_by_hash( + &self, + block_hash: B256, + opts: GethDebugTracingOptions, + ) -> Result, BlockchainError> { + if let Some(block) = self.blockchain.get_block_by_hash(&block_hash) { + let mut traces = Vec::new(); + for tx in &block.body.transactions { + let tx_hash = tx.hash(); + match self.debug_trace_transaction(tx_hash, opts.clone()).await { + Ok(trace) => { + traces.push(TraceResult::Success { result: trace, tx_hash: Some(tx_hash) }); + } + Err(error) => { + traces.push(TraceResult::Error { + error: error.to_string(), + tx_hash: Some(tx_hash), + }); + } + } + } + return Ok(traces); + } + + if let Some(fork) = self.get_fork() { + return Ok(fork.debug_trace_block_by_hash(block_hash, opts).await?); + } + + Err(BlockchainError::BlockNotFound) + } + + /// Returns geth-style traces for all transactions in a block by number. + pub async fn debug_trace_block_by_number( + &self, + block_number: BlockNumber, + opts: GethDebugTracingOptions, + ) -> Result, BlockchainError> { + let number = self.convert_block_number(Some(block_number)); + + if let Some(block) = self.get_block(BlockId::Number(BlockNumber::Number(number))) { + let mut traces = Vec::new(); + for tx in &block.body.transactions { + let tx_hash = tx.hash(); + match self.debug_trace_transaction(tx_hash, opts.clone()).await { + Ok(trace) => { + traces.push(TraceResult::Success { result: trace, tx_hash: Some(tx_hash) }); + } + Err(error) => { + traces.push(TraceResult::Error { + error: error.to_string(), + tx_hash: Some(tx_hash), + }); + } + } + } + return Ok(traces); + } + + if let Some(fork) = self.get_fork() { + return Ok(fork.debug_trace_block_by_number(number, opts).await?); + } + + Err(BlockchainError::BlockNotFound) + } + fn geth_trace( &self, tx: &MinedTransaction, @@ -4062,6 +4135,100 @@ impl Backend { Ok(None) } + + /// Sets the fee token for a user address (Tempo-only). + pub async fn set_fee_token(&self, user: Address, token: Address) -> DatabaseResult<()> { + let hardfork = self.hardfork(); + let chain_id = self.evm_env.read().cfg_env.chain_id; + let timestamp = U256::from(self.evm_env.read().block_env.timestamp); + let block_number: u64 = self.evm_env.read().block_env.number.to(); + let mut db = self.db.write().await; + let mut storage = AnvilStorageProvider::new( + &mut **db, + chain_id, + timestamp, + block_number, + hardfork.into(), + ); + StorageCtx::enter(&mut storage, || { + let mut fee_manager = TipFeeManager::new(); + fee_manager + .set_user_token(user, IFeeManager::setUserTokenCall { token }) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}")))) + }) + } + + /// Sets the fee token for a validator address (Tempo-only). + pub async fn set_validator_fee_token( + &self, + validator: Address, + token: Address, + ) -> DatabaseResult<()> { + let hardfork = self.hardfork(); + let chain_id = self.evm_env.read().cfg_env.chain_id; + let timestamp = U256::from(self.evm_env.read().block_env.timestamp); + let block_number: u64 = self.evm_env.read().block_env.number.to(); + let mut db = self.db.write().await; + let mut storage = AnvilStorageProvider::new( + &mut **db, + chain_id, + timestamp, + block_number, + hardfork.into(), + ); + StorageCtx::enter(&mut storage, || { + let mut fee_manager = TipFeeManager::new(); + // Use Address::ZERO as beneficiary so the check `sender != beneficiary` passes + fee_manager + .set_validator_token( + validator, + IFeeManager::setValidatorTokenCall { token }, + Address::ZERO, + ) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}")))) + }) + } + + /// Mints FeeAMM liquidity for a token pair (Tempo-only). + pub async fn set_fee_amm_liquidity( + &self, + user_token: Address, + validator_token: Address, + amount: U256, + ) -> DatabaseResult<()> { + let hardfork = self.hardfork(); + let chain_id = self.evm_env.read().cfg_env.chain_id; + let timestamp = U256::from(self.evm_env.read().block_env.timestamp); + let block_number: u64 = self.evm_env.read().block_env.number.to(); + let admin = Address::ZERO; + let mut db = self.db.write().await; + let mut storage = AnvilStorageProvider::new( + &mut **db, + chain_id, + timestamp, + block_number, + hardfork.into(), + ); + StorageCtx::enter(&mut storage, || { + // Mint the required tokens to admin so it can provide liquidity. + // grant_role_internal bypasses the caller check, matching genesis seeding. + for &token_address in &[user_token, validator_token] { + let mut token = TIP20Token::from_address(token_address) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}"))))?; + token + .grant_role_internal(admin, *ISSUER_ROLE) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}"))))?; + token + .mint(admin, ITIP20::mintCall { to: admin, amount }) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}"))))?; + } + let mut fee_manager = TipFeeManager::new(); + fee_manager + .mint(admin, user_token, validator_token, amount, admin) + .map_err(|e| DatabaseError::AnyRequest(Arc::new(eyre::eyre!("{e}"))))?; + Ok(()) + }) + } } /// Get max nonce from transaction pool by address. diff --git a/crates/anvil/src/eth/backend/tempo.rs b/crates/anvil/src/eth/backend/tempo.rs index fdff1149ac443..88db5fbcd0dfe 100644 --- a/crates/anvil/src/eth/backend/tempo.rs +++ b/crates/anvil/src/eth/backend/tempo.rs @@ -169,6 +169,14 @@ impl PrecompileStorageProvider for AnvilStorageProvider<'_> { self.gas_used } + fn state_gas_used(&self) -> u64 { + 0 + } + + fn gas_limit(&self) -> u64 { + u64::MAX + } + fn gas_refunded(&self) -> i64 { self.gas_refunded } diff --git a/crates/anvil/src/eth/sign.rs b/crates/anvil/src/eth/sign.rs index 27bab96509443..3fdf6192c4537 100644 --- a/crates/anvil/src/eth/sign.rs +++ b/crates/anvil/src/eth/sign.rs @@ -133,6 +133,9 @@ impl Signer for DevSigner { FoundryTypedTx::Deposit(_) => { unreachable!("op deposit txs should not be signed") } + FoundryTypedTx::PostExec(_) => { + unreachable!("op post-exec txs should not be signed") + } FoundryTypedTx::Tempo(mut t) => { let sig = signer.sign_transaction_sync(&mut t)?; FoundryTxEnvelope::Tempo(t.into_signed(sig.into())) @@ -154,6 +157,9 @@ pub fn build_impersonated(typed_tx: FoundryTypedTx) -> FoundryTxEnvelope { FoundryTypedTx::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)), FoundryTypedTx::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)), FoundryTypedTx::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)), + FoundryTypedTx::PostExec(_) => { + unreachable!("op post-exec txs should not be impersonated") + } FoundryTypedTx::Tempo(tx) => { let tempo_sig: TempoSignature = signature.into(); FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig)) diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm.rs index b51177d73fd79..d1b40ba56ebbd 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm.rs @@ -15,14 +15,14 @@ mod tests { use crate::PrecompileFactory; use alloy_evm::{ - EthEvm, Evm, + EthEvm, Evm, EvmEnv, EvmFactory, eth::EthEvmContext, precompiles::{DynPrecompile, PrecompilesMap}, }; - use alloy_op_evm::{OpEvm, OpTx}; + use alloy_op_evm::{OpEvm, OpEvmFactory, OpTx}; use alloy_primitives::{Address, Bytes, TxKind, U256, address}; use itertools::Itertools; - use op_revm::{L1BlockInfo, OpContext, OpSpecId, OpTransaction, precompiles::OpPrecompiles}; + use op_revm::{OpSpecId, OpTransaction}; use revm::{ Journal, context::{BlockEnv, CfgEnv, Evm as RevmEvm, JournalTr, LocalContext, TxEnv}, @@ -60,6 +60,7 @@ mod tests { status: PrecompileStatus::Success, bytes: Bytes::copy_from_slice(input.data), gas_used: 0, + gas_refunded: 0, state_gas_used: 0, reservoir: input.reservoir, }) @@ -110,52 +111,30 @@ mod tests { /// Creates a new OP EVM instance. fn create_op_evm( - spec: SpecId, + _spec: SpecId, op_spec: OpSpecId, ) -> (OpTx, OpEvm, NoOpInspector, PrecompilesMap, OpTx>) { - let tx = OpTransaction:: { + let tx = OpTx(OpTransaction:: { base: TxEnv { kind: TxKind::Call(PRECOMPILE_ADDR), data: PAYLOAD.into(), ..Default::default() }, ..Default::default() - }; + }); - let mut chain = L1BlockInfo::default(); + let mut evm = OpEvmFactory::::default().create_evm_with_inspector( + EmptyDB::default(), + EvmEnv::new(CfgEnv::new_with_spec(op_spec), BlockEnv::default()), + NoOpInspector, + ); if op_spec == OpSpecId::ISTHMUS { - chain.operator_fee_constant = Some(U256::from(0)); - chain.operator_fee_scalar = Some(U256::from(0)); + evm.ctx_mut().chain.operator_fee_constant = Some(U256::ZERO); + evm.ctx_mut().chain.operator_fee_scalar = Some(U256::ZERO); } - let op_cfg: CfgEnv = CfgEnv::new_with_spec(op_spec); - let op_evm_context = OpContext { - journaled_state: { - let mut journal = Journal::new(EmptyDB::default()); - journal.set_spec_id(spec); - journal - }, - block: BlockEnv::default(), - cfg: op_cfg.clone(), - tx: tx.clone(), - chain, - local: LocalContext::default(), - error: Ok(()), - }; - - let op_precompiles = OpPrecompiles::new_with_spec(op_cfg.spec).precompiles(); - let op_evm = OpEvm::new( - op_revm::OpEvm(RevmEvm::new_with_inspector( - op_evm_context, - NoOpInspector, - EthInstructions::>::new_mainnet_with_spec(spec), - PrecompilesMap::from_static(op_precompiles), - )), - true, - ); - - (OpTx(tx), op_evm) + (tx, evm) } #[test] diff --git a/crates/anvil/tests/it/anvil_api.rs b/crates/anvil/tests/it/anvil_api.rs index 239d454587698..81dc089f64265 100644 --- a/crates/anvil/tests/it/anvil_api.rs +++ b/crates/anvil/tests/it/anvil_api.rs @@ -1003,7 +1003,6 @@ async fn test_mine_blk_with_prev_timestamp() { } // increase time by 0 seconds i.e next_block_timestamp = prev_block_timestamp -// api.evm_increase_time(0).unwrap(); #[tokio::test(flavor = "multi_thread")] async fn test_increase_time_by_zero() { let (api, handle) = spawn(NodeConfig::test()).await; @@ -1014,7 +1013,7 @@ async fn test_increase_time_by_zero() { let init_number = init_blk.header.number; let init_timestamp = init_blk.header.timestamp; - let _ = api.evm_increase_time(U256::ZERO).await; + api.evm_set_next_block_timestamp(init_timestamp).unwrap(); api.mine_one().await; diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index 37d3f6f4a1dc4..61b0326992942 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -1940,6 +1940,7 @@ async fn test_config_with_osaka_hardfork_with_precompile_factory() { Ok(revm::precompile::PrecompileOutput { bytes: Bytes::copy_from_slice(input.data), gas_used: 0, + gas_refunded: 0, status: PrecompileStatus::Success, state_gas_used: 0, reservoir: input.reservoir, diff --git a/crates/anvil/tests/it/tempo.rs b/crates/anvil/tests/it/tempo.rs index 52923a47ba44b..18b2de3eb9465 100644 --- a/crates/anvil/tests/it/tempo.rs +++ b/crates/anvil/tests/it/tempo.rs @@ -21,6 +21,7 @@ use alloy_sol_types::sol; use anvil::{NodeConfig, spawn}; use foundry_evm::core::tempo::{PATH_USD_ADDRESS, TEMPO_PRECOMPILE_ADDRESSES, TEMPO_TIP20_TOKENS}; use tempo_alloy::primitives::TempoTxEnvelope; +use tempo_precompiles::{DEFAULT_FEE_TOKEN, TIP_FEE_MANAGER_ADDRESS}; use tempo_primitives::{ AASigned, TempoSignature, TempoTransaction, transaction::{Call, PrimitiveSignature}, @@ -34,6 +35,23 @@ const THETA_USD: Address = address!("0x20C0000000000000000000000000000000000003" /// Gas limit for TIP20 transfer calls (precompile interactions need more gas). const TIP20_TRANSFER_GAS: u64 = 300_000; +sol! { + #[sol(rpc)] + interface IFeeManagerRpc { + function userTokens(address user) external view returns (address); + function validatorTokens(address validator) external view returns (address); + + struct Pool { + uint128 reserveUserToken; + uint128 reserveValidatorToken; + } + + function getPool(address userToken, address validatorToken) external view returns (Pool memory); + function getPoolId(address userToken, address validatorToken) external pure returns (bytes32); + function totalSupply(bytes32 poolId) external view returns (uint256); + } +} + sol! { #[sol(rpc)] interface IERC20 { @@ -2654,3 +2672,108 @@ async fn test_legacy_transaction() { "Recipient should receive transfer amount" ); } + +// ============================================================================ +// TipFeeManager RPC Methods +// ============================================================================ + +/// `anvil_setFeeToken` sets the fee token for a user address, readable via `userTokens`. +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_fee_token() { + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let user = Address::random(); + let fee_manager = IFeeManagerRpc::new(TIP_FEE_MANAGER_ADDRESS, &provider); + + // Before: user has no token set (returns zero address) + let token_before = fee_manager.userTokens(user).call().await.unwrap(); + assert_eq!(token_before, Address::ZERO, "User should have no fee token initially"); + + // Set user fee token to ALPHA_USD + api.anvil_set_fee_token(user, ALPHA_USD).await.unwrap(); + + // After: userTokens returns the configured token + let token_after = fee_manager.userTokens(user).call().await.unwrap(); + assert_eq!(token_after, ALPHA_USD, "User fee token should be set to ALPHA_USD"); +} + +/// `anvil_setFeeToken` returns an error when Tempo mode is not active. +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_fee_token_non_tempo_fails() { + let (api, _handle) = spawn(NodeConfig::test()).await; + + let result = api.anvil_set_fee_token(Address::random(), ALPHA_USD).await; + assert!(result.is_err(), "anvil_setFeeToken should fail outside of Tempo mode"); +} + +/// `anvil_setValidatorFeeToken` sets the fee token for a validator, readable via `validatorTokens`. +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_validator_fee_token() { + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let validator = Address::random(); + let fee_manager = IFeeManagerRpc::new(TIP_FEE_MANAGER_ADDRESS, &provider); + + // Before: validator has no token set (returns zero address) + let token_before = fee_manager.validatorTokens(validator).call().await.unwrap(); + assert_eq!(token_before, DEFAULT_FEE_TOKEN, "Validator should have no fee token initially"); + + // Set validator fee token to BETA_USD + api.anvil_set_validator_fee_token(validator, BETA_USD).await.unwrap(); + + // After: validatorTokens returns the configured token + let token_after = fee_manager.validatorTokens(validator).call().await.unwrap(); + assert_eq!(token_after, BETA_USD, "Validator fee token should be set to BETA_USD"); +} + +/// `anvil_setValidatorFeeToken` returns an error when Tempo mode is not active. +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_validator_fee_token_non_tempo_fails() { + let (api, _handle) = spawn(NodeConfig::test()).await; + + let result = api.anvil_set_validator_fee_token(Address::random(), BETA_USD).await; + assert!(result.is_err(), "anvil_setValidatorFeeToken should fail outside of Tempo mode"); +} + +/// `anvil_setFeeAmmLiquidity` mints AMM liquidity for a token pair, +/// verifiable via `getPool` (non-zero reserves) and `totalSupply` (non-zero LP supply). +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_fee_amm_liquidity() { + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let fee_manager = IFeeManagerRpc::new(TIP_FEE_MANAGER_ADDRESS, &provider); + + // Genesis pre-seeds all pairs between fee tokens; record current state before adding more + let pool_before = fee_manager.getPool(PATH_USD, ALPHA_USD).call().await.unwrap(); + let pool_id = fee_manager.getPoolId(PATH_USD, ALPHA_USD).call().await.unwrap(); + let lp_supply_before = fee_manager.totalSupply(pool_id).call().await.unwrap(); + + let amount = U256::from(1_000_000u64); + api.anvil_set_fee_amm_liquidity(PATH_USD, ALPHA_USD, amount).await.unwrap(); + + // After minting: reserves and LP supply should have increased + let pool_after = fee_manager.getPool(PATH_USD, ALPHA_USD).call().await.unwrap(); + let lp_supply_after = fee_manager.totalSupply(pool_id).call().await.unwrap(); + + assert!( + pool_after.reserveValidatorToken > pool_before.reserveValidatorToken, + "Validator token reserve should increase after minting liquidity" + ); + assert!( + lp_supply_after > lp_supply_before, + "LP token total supply should increase after minting liquidity" + ); +} + +/// `anvil_setFeeAmmLiquidity` returns an error when Tempo mode is not active. +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_set_fee_amm_liquidity_non_tempo_fails() { + let (api, _handle) = spawn(NodeConfig::test()).await; + + let result = + api.anvil_set_fee_amm_liquidity(PATH_USD, ALPHA_USD, U256::from(1_000_000u64)).await; + assert!(result.is_err(), "anvil_setFeeAmmLiquidity should fail outside of Tempo mode"); +} diff --git a/crates/anvil/tests/it/traces.rs b/crates/anvil/tests/it/traces.rs index caaf6ca6466c7..2509a523c7cd5 100644 --- a/crates/anvil/tests/it/traces.rs +++ b/crates/anvil/tests/it/traces.rs @@ -16,7 +16,7 @@ use alloy_provider::{ ext::{DebugApi, TraceApi}, }; use alloy_rpc_types::{ - TransactionRequest, + BlockNumberOrTag, TransactionRequest, state::StateOverride, trace::{ filter::{TraceFilter, TraceFilterMode}, @@ -1351,3 +1351,87 @@ async fn test_trace_replay_block_transactions_local() { ); } } + +#[tokio::test(flavor = "multi_thread")] +async fn test_debug_trace_block_by_number() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let accounts = handle.dev_wallets().collect::>(); + let from = accounts[0].address(); + let to = accounts[1].address(); + let amount = U256::from(1000); + + let tx = TransactionRequest::default().to(to).value(amount).from(from); + let tx = WithOtherFields::new(tx); + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + let block_number = receipt.block_number.unwrap(); + + let traces = api + .backend + .debug_trace_block_by_number( + BlockNumberOrTag::Number(block_number), + GethDebugTracingOptions::default() + .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer)), + ) + .await + .unwrap(); + + assert_eq!(traces.len(), 1); + + match &traces[0] { + alloy_rpc_types::trace::geth::TraceResult::Success { result, .. } => match result { + GethTrace::CallTracer(call_frame) => { + assert_eq!(call_frame.from, from); + assert_eq!(call_frame.to.unwrap(), to); + assert_eq!(call_frame.value, Some(amount)); + } + _ => unreachable!("expected CallTracer"), + }, + alloy_rpc_types::trace::geth::TraceResult::Error { error, .. } => { + panic!("trace failed: {error}"); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_debug_trace_block_by_hash() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let accounts = handle.dev_wallets().collect::>(); + let from = accounts[0].address(); + let to = accounts[1].address(); + let amount = U256::from(2000); + + let tx = TransactionRequest::default().to(to).value(amount).from(from); + let tx = WithOtherFields::new(tx); + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + let block_hash = receipt.block_hash.unwrap(); + + let traces = api + .backend + .debug_trace_block_by_hash( + block_hash, + GethDebugTracingOptions::default() + .with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer)), + ) + .await + .unwrap(); + + assert_eq!(traces.len(), 1); + + match &traces[0] { + alloy_rpc_types::trace::geth::TraceResult::Success { result, .. } => match result { + GethTrace::CallTracer(call_frame) => { + assert_eq!(call_frame.from, from); + assert_eq!(call_frame.to.unwrap(), to); + assert_eq!(call_frame.value, Some(amount)); + } + _ => unreachable!("expected CallTracer"), + }, + alloy_rpc_types::trace::geth::TraceResult::Error { error, .. } => { + panic!("trace failed: {error}"); + } + } +} diff --git a/crates/anvil/tests/it/txpool.rs b/crates/anvil/tests/it/txpool.rs index 6bd14ba48596c..7e553cca1d100 100644 --- a/crates/anvil/tests/it/txpool.rs +++ b/crates/anvil/tests/it/txpool.rs @@ -55,6 +55,74 @@ async fn geth_txpool() { } } +#[tokio::test(flavor = "multi_thread")] +async fn geth_txpool_separates_queued_transactions() { + let (api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + api.anvil_set_auto_mine(false).await.unwrap(); + + let accounts = handle.dev_wallets().collect::>(); + let account = accounts[0].address(); + let recipient = accounts[1].address(); + let gas_price = 221435145689u128; + + let pending_value = U256::from(42); + let pending_tx = TransactionRequest::default() + .with_to(recipient) + .with_from(account) + .with_value(pending_value) + .with_gas_price(gas_price) + .with_nonce(0); + let pending_tx = WithOtherFields::new(pending_tx); + + let queued_value = U256::from(84); + let queued_tx = TransactionRequest::default() + .with_to(recipient) + .with_from(account) + .with_value(queued_value) + .with_gas_price(gas_price) + .with_nonce(2); + let queued_tx = WithOtherFields::new(queued_tx); + + let _ = provider.send_transaction(pending_tx).await.unwrap(); + let _ = provider.send_transaction(queued_tx).await.unwrap(); + + let status = provider.txpool_status().await.unwrap(); + assert_eq!(status.pending, 1); + assert_eq!(status.queued, 1); + + let inspect = provider.txpool_inspect().await.unwrap(); + let pending = inspect.pending.get(&account).unwrap(); + assert_eq!(pending.len(), 1); + assert!(!pending.contains_key("2")); + let pending_summary = pending.get("0").unwrap(); + assert_eq!(pending_summary.gas_price, gas_price); + assert_eq!(pending_summary.value, pending_value); + assert_eq!(pending_summary.gas, 21000); + assert_eq!(pending_summary.to.unwrap(), recipient); + + let queued = inspect.queued.get(&account).unwrap(); + assert_eq!(queued.len(), 1); + assert!(!queued.contains_key("0")); + let queued_summary = queued.get("2").unwrap(); + assert_eq!(queued_summary.gas_price, gas_price); + assert_eq!(queued_summary.value, queued_value); + assert_eq!(queued_summary.gas, 21000); + assert_eq!(queued_summary.to.unwrap(), recipient); + + let content = provider.txpool_content().await.unwrap(); + let pending = content.pending.get(&account).unwrap(); + assert_eq!(pending.len(), 1); + assert!(pending.contains_key("0")); + assert!(!pending.contains_key("2")); + + let queued = content.queued.get(&account).unwrap(); + assert_eq!(queued.len(), 1); + assert!(queued.contains_key("2")); + assert!(!queued.contains_key("0")); +} + // Cf. https://github.com/foundry-rs/foundry/issues/11239 #[tokio::test(flavor = "multi_thread")] async fn accepts_spend_after_funding_when_pool_checks_disabled() { diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index dbd89ce88827d..dc5be77a244da 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -28,7 +28,8 @@ foundry-compilers.workspace = true foundry-config.workspace = true foundry-debugger.workspace = true foundry-evm.workspace = true -foundry-wallets.workspace = true +foundry-evm-networks.workspace = true +foundry-wallets = { workspace = true, features = ["browser", "tempo"] } forge-fmt.workspace = true alloy-chains.workspace = true diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index d36e74b53b6d1..6dc2ed6acf430 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -15,10 +15,7 @@ use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest}; use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::{Result, WrapErr}; -use foundry_cli::{ - opts::NetworkVariant, - utils::{self, LoadConfig}, -}; +use foundry_cli::utils::{self, LoadConfig}; use foundry_common::{ abi::{get_error, get_event}, fmt::{format_tokens, format_uint_exp, serialize_value_as_json}, @@ -31,6 +28,7 @@ use foundry_common::{ }, shell, stdin, }; +use foundry_evm_networks::NetworkVariant; use op_alloy_network::Optimism; use std::time::Instant; use tempo_alloy::TempoNetwork; diff --git a/crates/cast/src/call_spec.rs b/crates/cast/src/call_spec.rs index b39e024f38af5..a1be179fbecc7 100644 --- a/crates/cast/src/call_spec.rs +++ b/crates/cast/src/call_spec.rs @@ -112,6 +112,7 @@ impl CallSpec { chain: Chain, provider: &P, etherscan_api_key: Option<&str>, + etherscan_api_url: Option<&str>, ) -> Result { let input = if let Some(data) = &self.data { data.clone() @@ -123,6 +124,7 @@ impl CallSpec { chain, provider, etherscan_api_key, + etherscan_api_url, ) .await .map_err(|e| eyre!("Failed to encode call {}: {e}", i + 1))?; @@ -145,8 +147,8 @@ impl FromStr for CallSpec { /// Parse a value string that can be in ether notation (e.g., "0.1ether") or raw wei. fn parse_ether_or_wei(s: &str) -> Result { // Use alloy's DynSolType coercion which handles "1ether", "1gwei", "1000" etc. - if s.starts_with("0x") { - U256::from_str_radix(s, 16).map_err(|e| eyre!("Invalid hex value '{}': {}", s, e)) + if s.starts_with("0x") || s.starts_with("0X") { + U256::from_str(s).map_err(|e| eyre!("Invalid hex value '{}': {}", s, e)) } else { alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), s) .wrap_err_with(|| format!("Invalid value '{s}'"))? @@ -180,6 +182,12 @@ mod tests { assert!(spec.sig.is_none()); } + #[test] + fn test_parse_hex_value() { + assert_eq!(parse_ether_or_wei("0x10").unwrap(), U256::from(16)); + assert_eq!(parse_ether_or_wei("0X10").unwrap(), U256::from(16)); + } + #[test] fn test_parse_with_sig() { let spec = CallSpec::parse( diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index 759d8ef507f20..ae5c9668f522f 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -72,12 +72,22 @@ impl BatchMakeTxArgs { // Get chain for parsing function args let chain = utils::get_chain(config.chain, &provider).await?; - let etherscan_api_key = config.get_etherscan_api_key(Some(chain)); + let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten(); + let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone()); + let etherscan_api_url = etherscan_config.map(|c| c.api_url); let mut tempo_calls = Vec::with_capacity(call_specs.len()); for (i, spec) in call_specs.iter().enumerate() { - tempo_calls - .push(spec.resolve(i, chain, &provider, etherscan_api_key.as_deref()).await?); + tempo_calls.push( + spec.resolve( + i, + chain, + &provider, + etherscan_api_key.as_deref(), + etherscan_api_url.as_deref(), + ) + .await?, + ); } sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index 5fd18b0261897..ec7254b08e2f5 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -72,13 +72,23 @@ impl BatchSendArgs { // Get chain for parsing function args let chain = utils::get_chain(config.chain, &provider).await?; - let etherscan_api_key = config.get_etherscan_api_key(Some(chain)); + let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten(); + let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone()); + let etherscan_api_url = etherscan_config.map(|c| c.api_url); // Build Vec from specs let mut tempo_calls = Vec::with_capacity(call_specs.len()); for (i, spec) in call_specs.iter().enumerate() { - tempo_calls - .push(spec.resolve(i, chain, &provider, etherscan_api_key.as_deref()).await?); + tempo_calls.push( + spec.resolve( + i, + chain, + &provider, + etherscan_api_key.as_deref(), + etherscan_api_url.as_deref(), + ) + .await?, + ); } sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?; diff --git a/crates/cast/src/cmd/creation_code.rs b/crates/cast/src/cmd/creation_code.rs index 222857c374a1a..29e3a18279420 100644 --- a/crates/cast/src/cmd/creation_code.rs +++ b/crates/cast/src/cmd/creation_code.rs @@ -7,7 +7,6 @@ use alloy_provider::{Provider, RootProvider, ext::TraceApi}; use alloy_rpc_types::trace::parity::{Action, CreateAction, CreateOutput, TraceOutput}; use clap::Parser; use eyre::{OptionExt, Result, eyre}; -use foundry_block_explorers::Client; use foundry_cli::{ opts::{EtherscanOpts, RpcOpts}, utils::{self, LoadConfig, fetch_abi_from_etherscan}, @@ -139,8 +138,10 @@ pub async fn fetch_creation_code_from_etherscan( provider: RootProvider, ) -> Result { let chain = config.chain.unwrap_or_default(); - let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, api_key)?; + let client = config + .get_etherscan_config_with_chain(Some(chain))? + .ok_or_else(|| eyre!("No Etherscan API key configured for chain {chain}"))? + .into_client()?; let creation_data = client.contract_creation_data(contract).await?; let creation_tx_hash = creation_data.transaction_hash; let tx_data = provider.get_transaction_by_hash(creation_tx_hash).await?; diff --git a/crates/cast/src/cmd/da_estimate.rs b/crates/cast/src/cmd/da_estimate.rs index 2f49f950b7579..23197b6ee7a56 100644 --- a/crates/cast/src/cmd/da_estimate.rs +++ b/crates/cast/src/cmd/da_estimate.rs @@ -6,12 +6,10 @@ use alloy_provider::Provider; use alloy_rpc_types::BlockId; use clap::Parser; use eyre::Result; -use foundry_cli::{ - opts::{NetworkVariant, RpcOpts}, - utils::LoadConfig, -}; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; use foundry_common::provider::ProviderBuilder; use foundry_config::Config; +use foundry_evm_networks::NetworkVariant; use op_alloy_network::Optimism; /// CLI arguments for `cast da-estimate`. diff --git a/crates/cast/src/cmd/erc20.rs b/crates/cast/src/cmd/erc20.rs index e629b8eca0d5a..27e33ec03e7e7 100644 --- a/crates/cast/src/cmd/erc20.rs +++ b/crates/cast/src/cmd/erc20.rs @@ -3,13 +3,13 @@ use std::{str::FromStr, time::Duration}; use crate::{ cmd::send::{cast_send, cast_send_with_access_key}, format_uint_exp, - tx::{SendTxOpts, TxParams}, + tx::{CastTxSender, SendTxOpts, TxParams}, }; use alloy_consensus::{SignableTransaction, Signed}; use alloy_eips::BlockId; use alloy_ens::NameOrAddress; -use alloy_network::{Ethereum, EthereumWallet, Network}; -use alloy_primitives::U256; +use alloy_network::{Ethereum, EthereumWallet, Network, TransactionBuilder}; +use alloy_primitives::{Address, U256}; use alloy_provider::{Provider, fillers::RecommendedFillers}; use alloy_signer::Signature; use alloy_sol_types::sol; @@ -23,11 +23,13 @@ use foundry_common::{ fmt::{UIfmt, UIfmtReceiptExt}, provider::{ProviderBuilder, RetryProviderWithSigner}, shell, + tempo::TEMPO_BROWSER_GAS_BUFFER, }; #[doc(hidden)] pub use foundry_config::{Chain, utils::*}; use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; use tempo_alloy::TempoNetwork; +use tempo_contracts::precompiles::PATH_USD_ADDRESS; sol! { #[sol(rpc)] @@ -281,6 +283,34 @@ impl Erc20Subcommand { } } + const fn uses_browser_send(&self) -> bool { + match self { + Self::Transfer { send_tx, .. } + | Self::Approve { send_tx, .. } + | Self::Mint { send_tx, .. } + | Self::Burn { send_tx, .. } => send_tx.browser.browser, + _ => false, + } + } + + async fn should_use_tempo_network( + &self, + tempo_access_key: &Option, + ) -> eyre::Result { + if self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo()) + || tempo_access_key.is_some() + { + return Ok(true); + } + + if self.uses_browser_send() { + let config = self.rpc_opts().load_config()?; + return Ok(get_chain(config.chain, &get_provider(&config)?).await?.is_tempo()); + } + + Ok(false) + } + pub async fn run(self) -> eyre::Result<()> { // Resolve the signer once for state-changing variants. let (signer, tempo_access_key) = match &self { @@ -299,8 +329,7 @@ impl Erc20Subcommand { _ => (None, None), }; - let is_tempo = self.erc20_opts().is_some_and(|erc20| erc20.tempo.is_tempo()) - || tempo_access_key.is_some(); + let is_tempo = self.should_use_tempo_network(&tempo_access_key).await?; if is_tempo { self.run_generic::(signer, tempo_access_key).await @@ -356,6 +385,28 @@ impl Erc20Subcommand { timeout, ) .await? + } else if let Some(browser) = $send_tx.browser.run::().await? { + let $provider = ProviderBuilder::::from_config(&config)?.build()?; + if let Some(interval) = $send_tx.poll_interval { + $provider.client().set_poll_interval(Duration::from_secs(interval)); + } + let $erc20 = IERC20::new($token.resolve(&$provider).await?, &$provider); + let mut tx = { $build_tx }.into_transaction_request(); + let chain = get_chain(config.chain, &$provider).await?; + $tx_opts.apply::(&mut tx, chain.is_legacy()); + if chain.is_tempo() && tx.fee_token().is_none() { + tx.set_fee_token(PATH_USD_ADDRESS); + } + fill_tx(&$provider, &mut tx, browser.address(), chain).await?; + let tx_hash = browser.send_transaction_via_browser(tx).await?; + CastTxSender::new(&$provider) + .print_tx_result( + tx_hash, + $send_tx.cast_async, + $send_tx.confirmations, + timeout, + ) + .await? } else { let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?); let $provider = build_provider_with_signer::(&$send_tx, signer)?; @@ -503,3 +554,55 @@ impl Erc20Subcommand { Ok(()) } } + +/// Fills from, chain_id, nonce, fees, and gas limit on a transaction request for the browser +/// wallet path. Mirrors the filling logic in the shared tx builder but operates on a +/// pre-built transaction request from the sol! macro rather than through the builder pipeline. +/// Only fills fields that haven't already been set by the user. +async fn fill_tx>( + provider: &P, + tx: &mut N::TransactionRequest, + from: Address, + chain: Chain, +) -> eyre::Result<()> +where + N::TransactionRequest: FoundryTransactionBuilder, +{ + tx.set_from(from); + tx.set_chain_id(chain.id()); + + if tx.nonce().is_none() { + tx.set_nonce(provider.get_transaction_count(from).await?); + } + + let legacy = chain.is_legacy(); + + if legacy { + if tx.gas_price().is_none() { + tx.set_gas_price(provider.get_gas_price().await?); + } + } else if tx.max_fee_per_gas().is_none() || tx.max_priority_fee_per_gas().is_none() { + let estimate = provider.estimate_eip1559_fees().await?; + if tx.max_fee_per_gas().is_none() { + tx.set_max_fee_per_gas(estimate.max_fee_per_gas); + } + if tx.max_priority_fee_per_gas().is_none() { + tx.set_max_priority_fee_per_gas(estimate.max_priority_fee_per_gas); + } + } + + if tx.gas_limit().is_none() { + let mut estimated = provider.estimate_gas(tx.clone()).await?; + + // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which + // costs more gas for signature verification on Tempo chains. Add a + // conservative buffer since we can't determine the signature type beforehand. + if chain.is_tempo() { + estimated += TEMPO_BROWSER_GAS_BUFFER; + } + + tx.set_gas_limit(estimated); + } + + Ok(()) +} diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 08c535bef8a18..2d6e248cc7a73 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -13,6 +13,7 @@ use foundry_common::{ FoundryTransactionBuilder, fmt::{UIfmt, UIfmtReceiptExt}, provider::ProviderBuilder, + tempo::TEMPO_BROWSER_GAS_BUFFER, }; use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; use tempo_alloy::TempoNetwork; @@ -258,7 +259,18 @@ impl SendTxArgs { // Case 2: // Browser wallet signs and sends the transaction in one step. } else if let Some(browser) = browser { - let (tx_request, _) = builder.build(browser.address()).await?; + let chain = builder.chain(); + let (mut tx_request, _) = builder.build(browser.address()).await?; + + // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which + // costs more gas for signature verification on Tempo chains. Add a + // conservative buffer since we can't determine the signature type beforehand. + if chain.is_tempo() + && let Some(gas) = tx_request.gas_limit() + { + tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); + } + let tx_hash = browser.send_transaction_via_browser(tx_request).await?; let cast = CastTxSender::new(&provider); diff --git a/crates/cast/src/cmd/storage.rs b/crates/cast/src/cmd/storage.rs index 5c534777d87f8..eb3c6908082ec 100644 --- a/crates/cast/src/cmd/storage.rs +++ b/crates/cast/src/cmd/storage.rs @@ -7,7 +7,6 @@ use alloy_rpc_types::BlockId; use clap::Parser; use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN}; use eyre::Result; -use foundry_block_explorers::Client; use foundry_cli::{ opts::{BuildOpts, EtherscanOpts, RpcOpts}, utils, @@ -141,10 +140,16 @@ impl StorageArgs { } let chain = utils::get_chain(config.chain, &provider).await?; - let api_key = config.get_etherscan_api_key(Some(chain)).or_else(|| self.etherscan.key()).ok_or_else(|| { - eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.") - })?; - let client = Client::new(chain, api_key)?; + let etherscan_api_key = self.etherscan.key(); + let client = match config.get_etherscan_config_with_chain(Some(chain))? { + Some(etherscan_config) => etherscan_config.into_client()?, + None => { + let api_key = etherscan_api_key.ok_or_else(|| { + eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.") + })?; + foundry_block_explorers::Client::new(chain, api_key)? + } + }; let source = if let Some(proxy) = self.proxy { find_source(client, proxy.resolve(&provider).await?).await? } else { diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index c7d9dcfc897f4..effc081e8072a 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -13,8 +13,9 @@ use alloy_primitives::{Address, B256, Selector, U256}; use alloy_rpc_types::BlockId; use clap::{ArgAction, Parser, Subcommand, ValueHint}; use eyre::Result; -use foundry_cli::opts::{EtherscanOpts, GlobalArgs, NetworkVariant, RpcOpts}; +use foundry_cli::opts::{EtherscanOpts, GlobalArgs, RpcOpts}; use foundry_common::version::{LONG_VERSION, SHORT_VERSION}; +use foundry_evm_networks::NetworkVariant; use std::{path::PathBuf, str::FromStr}; /// A Swiss Army knife for interacting with Ethereum applications from the command line. #[derive(Parser)] diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 131c96e6f6c17..96f5fc5137575 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -394,10 +394,18 @@ pub struct CastTxBuilder { auth: Vec, chain: Chain, etherscan_api_key: Option, + etherscan_api_url: Option, access_list: Option>, state: S, } +impl CastTxBuilder { + /// Returns the resolved chain for this builder. + pub const fn chain(&self) -> Chain { + self.chain + } +} + impl> CastTxBuilder where N::TransactionRequest: FoundryTransactionBuilder, @@ -408,7 +416,9 @@ where let mut tx = N::TransactionRequest::default(); let chain = utils::get_chain(config.chain, &provider).await?; - let etherscan_api_key = config.get_etherscan_api_key(Some(chain)); + let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten(); + let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone()); + let etherscan_api_url = etherscan_config.map(|c| c.api_url); // mark it as legacy if requested or the chain is legacy and no 7702 is provided. let legacy = tx_opts.legacy || (chain.is_legacy() && tx_opts.auth.is_empty()); @@ -424,6 +434,7 @@ where fill: true, chain, etherscan_api_key, + etherscan_api_url, auth: tx_opts.auth, access_list: tx_opts.access_list, state: InitState, @@ -442,6 +453,7 @@ where fill: self.fill, chain: self.chain, etherscan_api_key: self.etherscan_api_key, + etherscan_api_url: self.etherscan_api_url, auth: self.auth, access_list: self.access_list, state: ToState { to }, @@ -470,6 +482,7 @@ where self.chain, &self.provider, self.etherscan_api_key.as_deref(), + self.etherscan_api_url.as_deref(), ) .await? } else { @@ -503,6 +516,7 @@ where fill: self.fill, chain: self.chain, etherscan_api_key: self.etherscan_api_key, + etherscan_api_url: self.etherscan_api_url, auth: self.auth, access_list: self.access_list, state: InputState { kind: self.state.to.into(), input, func }, diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 0df824f3588da..da33a34d849db 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4995,6 +4995,55 @@ casttest!(cast_decode_tx_invalid, |_prj, cmd| { cmd.args(["decode-tx", "0xinvalid"]).assert_failure(); }); +// Test that `--network tempo` and `-n tempo` (short form) produce identical output for decode-tx. +// Uses a known Tempo mainnet transaction. +casttest!(cast_decode_tx_network_flag_short_and_long_equivalent, |_prj, cmd| { + let tx = "0x76f8cf82a5bf1485059682f018830494e5f85ef85c9420c0000000000000000000007d9cc57068833ea780b84440c10f190000000000000000000000008a871f4189067637cfc4cc1500abd6244bf1df740000000000000000000000000000000000000000000000000000000005f5e100c08082057e80809420c000000000000000000000000000000000000080c0b841eb100c4cbd96903bf9e97968c0982670bb90fc191ee4544c7ff32d44e901dbea3f6fbdd58255051135c2fe1aa81583a270d96009cbe375f4605ef15971273a4f1b"; + + let via_long = cmd + .args(["decode-tx", "--network", "tempo", tx]) + .assert_success() + .get_output() + .stdout + .clone(); + + let via_short = cmd + .cast_fuse() + .args(["decode-tx", "-n", "tempo", tx]) + .assert_success() + .get_output() + .stdout + .clone(); + + assert_eq!(via_long, via_short, "--network tempo and -n tempo should produce same output"); +}); + +// Test that `--network optimism` and `-n optimism` produce identical output for decode-tx. +// Uses a known OP-stack deposit transaction (same tx as tx_raw_opstack_deposit test). +casttest!(cast_decode_tx_network_optimism_short_and_long_equivalent, |_prj, cmd| { + let tx = "0x7ef90207a0cbde10ec697aff886f95d2514bab434e455620627b9bb8ba33baaaa4d537d62794d45955f4de64f1840e5686e64278da901e263031944200000000000000000000000000000000000007872386f26fc10000872386f26fc1000083096c4980b901a4d764ad0b0001000000000000000000000000000000000000000000000000000000065132000000000000000000000000fd0bf71f60660e2f608ed56e1659c450eb1131200000000000000000000000004200000000000000000000000000000000000010000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000a41635f5fd000000000000000000000000ca11bde05977b3631167028862be2a173976ca110000000000000000000000005703b26fe5a7be820db1bf34c901a79da1a46ba4000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + let via_long = cmd + .args(["decode-tx", "--network", "optimism", tx]) + .assert_success() + .get_output() + .stdout + .clone(); + + let via_short = cmd + .cast_fuse() + .args(["decode-tx", "-n", "optimism", tx]) + .assert_success() + .get_output() + .stdout + .clone(); + + assert_eq!( + via_long, via_short, + "--network optimism and -n optimism should produce same output" + ); +}); + // Test that `cast run --evm-version` correctly updates gas parameters for historical blocks. // Mainnet tx 0xb856d9...d05d9647 is a Homestead-era tx (block 1,625,693). // EXP gas pricing differs between Homestead (10 gas/byte) and Spurious Dragon+ (50 gas/byte). diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 0d30ee845007d..613a5199c8338 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1246,6 +1246,10 @@ impl Cheatcode for startDebugTraceRecordingCall { return Err(Error::from("no tracer initiated, consider adding -vvv flag")); }; + if ccx.state.record_debug_steps_info.is_some() { + bail!("debug trace recording was already started"); + } + let mut info = RecordDebugStepInfo { // will be updated later start_node_idx: 0, diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index 5e762ec62a035..f2f11d91ad163 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -434,6 +434,7 @@ fn deploy_code( value.unwrap_or(U256::ZERO), bytecode.into(), ccx.gas_limit, + 0, ), ccx, )?; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 165154da88f63..37340f5f4cc3c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -13,13 +13,12 @@ repository.workspace = true workspace = true [dependencies] -foundry-block-explorers.workspace = true foundry-cli-markdown.workspace = true foundry-common.workspace = true foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-networks.workspace = true -foundry-wallets.workspace = true +foundry-wallets = { workspace = true, features = ["browser", "tempo"] } tempo-primitives.workspace = true diff --git a/crates/cli/src/opts/evm.rs b/crates/cli/src/opts/evm.rs index f818878b897e1..87f14e2039606 100644 --- a/crates/cli/src/opts/evm.rs +++ b/crates/cli/src/opts/evm.rs @@ -178,11 +178,8 @@ impl Provider for EvmArgs { // Only insert network flags when explicitly set via CLI to avoid overriding // values from foundry.toml (NetworkConfigs is flattened in Config). - if self.networks.is_tempo() { - dict.insert("tempo".to_string(), true.into()); - } - if self.networks.is_optimism() { - dict.insert("optimism".to_string(), true.into()); + if let Some(name) = self.networks.active_network_name() { + dict.insert("network".to_string(), name.into()); } if self.networks.is_celo() { dict.insert("celo".to_string(), true.into()); diff --git a/crates/cli/src/opts/mod.rs b/crates/cli/src/opts/mod.rs index fc619f482d2fb..0c074307445ab 100644 --- a/crates/cli/src/opts/mod.rs +++ b/crates/cli/src/opts/mod.rs @@ -3,7 +3,6 @@ mod chain; mod dependency; mod evm; mod global; -mod network; mod rpc; mod rpc_common; mod tempo; @@ -14,7 +13,6 @@ pub use chain::*; pub use dependency::*; pub use evm::*; pub use global::*; -pub use network::*; pub use rpc::*; pub use rpc_common::*; pub use tempo::*; diff --git a/crates/cli/src/opts/network.rs b/crates/cli/src/opts/network.rs deleted file mode 100644 index ffb63ad7048d4..0000000000000 --- a/crates/cli/src/opts/network.rs +++ /dev/null @@ -1,27 +0,0 @@ -use alloy_chains::Chain; -use alloy_primitives::ChainId; - -/// Network selection, defaulting to Ethereum -#[derive(Clone, Debug, Default, clap::ValueEnum)] -pub enum NetworkVariant { - /// Ethereum (default) - #[default] - Ethereum, - /// Optimism / OP-stack - Optimism, - /// Tempo - Tempo, -} - -impl From for NetworkVariant { - fn from(chain_id: ChainId) -> Self { - let chain = Chain::from_id(chain_id); - if chain.is_tempo() { - Self::Tempo - } else if chain.is_optimism() { - Self::Optimism - } else { - Default::default() - } - } -} diff --git a/crates/cli/src/utils/abi.rs b/crates/cli/src/utils/abi.rs index 6e91fcc9a537b..13127ec368b5b 100644 --- a/crates/cli/src/utils/abi.rs +++ b/crates/cli/src/utils/abi.rs @@ -34,6 +34,7 @@ pub async fn parse_function_args>( chain: Chain, provider: &P, etherscan_api_key: Option<&str>, + etherscan_api_url: Option<&str>, ) -> Result<(Vec, Option)> { if sig.trim().is_empty() { eyre::bail!("Function signature or calldata must be provided.") @@ -60,7 +61,7 @@ pub async fn parse_function_args>( "Function signature does not contain parentheses. If you wish to fetch function data from Etherscan, please provide an API key.", )?; let to = to.ok_or_eyre("A 'to' address must be provided to fetch function data.")?; - get_func_etherscan(sig, to, &args, chain, etherscan_api_key).await? + get_func_etherscan(sig, to, &args, chain, etherscan_api_key, etherscan_api_url).await? }; if to.is_none() { diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index f1c6c435217b7..4a4944a1cfadc 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -129,8 +129,8 @@ where /// If the string represents an untagged amount (e.g. "100") then /// it is interpreted as wei. pub fn parse_ether_value(value: &str) -> Result { - Ok(if value.starts_with("0x") { - U256::from_str_radix(value, 16)? + Ok(if value.starts_with("0x") || value.starts_with("0X") { + U256::from_str(value)? } else { alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)? .as_uint() @@ -227,8 +227,10 @@ pub async fn fetch_abi_from_etherscan( config: &foundry_config::Config, ) -> Result> { let chain = config.chain.unwrap_or_default(); - let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = foundry_block_explorers::Client::new(chain, api_key)?; + let client = config + .get_etherscan_config_with_chain(Some(chain))? + .ok_or_else(|| eyre::eyre!("No Etherscan API key configured for chain {chain}"))? + .into_client()?; let source = client.contract_source_code(address).await?; source.items.into_iter().map(|item| Ok((item.abi()?, item.contract_name))).collect() } @@ -844,6 +846,16 @@ mod tests { assert!(!p.is_sol_test()); } + #[test] + fn parse_ether_value_accepts_hex_prefixed_wei() { + assert_eq!(parse_ether_value("0x10").unwrap(), U256::from(16)); + assert_eq!(parse_ether_value("0X10").unwrap(), U256::from(16)); + assert_eq!(parse_ether_value("0x12").unwrap(), U256::from(0x12)); + assert_eq!(parse_ether_value("0xff").unwrap(), U256::from(0xff)); + assert_eq!(parse_ether_value("100").unwrap(), U256::from(100)); + assert_eq!(parse_ether_value("1ether").unwrap(), U256::from(1000000000000000000u128)); + } + // loads .env from cwd and project dir, See [`find_project_root()`] #[test] fn can_load_dotenv() { diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ac7a8c12eae71..51115fb4a0bae 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -38,6 +38,7 @@ alloy-rpc-types-engine = { workspace = true, features = ["jwt"] } alloy-sol-types.workspace = true alloy-transport-ipc.workspace = true alloy-transport-ws.workspace = true +rustls.workspace = true alloy-transport.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true @@ -82,6 +83,9 @@ flate2.workspace = true tempo-alloy.workspace = true tempo-primitives.workspace = true mpp.workspace = true +foundry-wallets = { workspace = true, features = ["browser", "tempo"] } +tokio-tungstenite.workspace = true +futures.workspace = true [build-dependencies] chrono.workspace = true diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 78759848fc4bd..2c8e16bccdcc6 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -30,6 +30,7 @@ serde_json.workspace = true chrono.workspace = true revm.workspace = true yansi.workspace = true +comfy-table.workspace = true # Tempo tempo-alloy.workspace = true diff --git a/crates/common/fmt/src/console.rs b/crates/common/fmt/src/console.rs index d751ddba245ba..8b66335445046 100644 --- a/crates/common/fmt/src/console.rs +++ b/crates/common/fmt/src/console.rs @@ -1,5 +1,6 @@ use super::UIfmt; use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256}; +use comfy_table::{Table, TableComponent, presets::UTF8_FULL}; use std::fmt::{self, Write}; /// A piece is a portion of the format string which represents the next part to emit. @@ -407,6 +408,36 @@ fn format_spec<'a>( } } +pub fn console_table_format( + keys: Option<&[&dyn ConsoleFmt]>, + values: &[&dyn ConsoleFmt], +) -> String { + let keys_strings: Vec = match keys { + Some(keys) => keys.iter().map(|k| k.fmt(FormatSpec::String)).collect(), + None => (0..values.len()).map(|i| i.to_string()).collect(), + }; + let values_strings: Vec = values.iter().map(|v| v.fmt(FormatSpec::String)).collect(); + + let mut table = Table::new(); + table.load_preset(UTF8_FULL); + table.set_style(TableComponent::VerticalLines, '│'); + table.set_style(TableComponent::HeaderLines, '─'); + table.set_style(TableComponent::MiddleHeaderIntersections, '┼'); + table.set_style(TableComponent::LeftHeaderIntersection, '├'); + table.set_style(TableComponent::RightHeaderIntersection, '┤'); + table.set_header(vec!["(index)", "Values"]); + table.remove_style(TableComponent::HorizontalLines); + table.remove_style(TableComponent::MiddleIntersections); + table.remove_style(TableComponent::LeftBorderIntersections); + table.remove_style(TableComponent::RightBorderIntersections); + for i in 0..keys_strings.len().max(values_strings.len()) { + let key = keys_strings.get(i).map(String::as_str).unwrap_or(""); + let value = values_strings.get(i).map(String::as_str).unwrap_or(""); + table.add_row(vec![key, value]); + } + table.to_string() +} + #[cfg(test)] mod tests { use super::*; @@ -610,4 +641,80 @@ mod tests { let call = Logs::Log1(log1); assert_eq!(call.fmt(Default::default()), "foo 42 bar"); } + + #[test] + fn test_console_table_format() { + // auto-indexed, uint256 values + let values: &[&dyn ConsoleFmt] = &[&U256::from(100), &U256::from(200), &U256::from(300)]; + assert_eq!( + console_table_format(None, values), + "┌─────────┬────────┐\n\ + │ (index) │ Values │\n\ + ├─────────┼────────┤\n\ + │ 0 │ 100 │\n\ + │ 1 │ 200 │\n\ + │ 2 │ 300 │\n\ + └─────────┴────────┘" + ); + + // string keys, uint256 values + // key col expands to fit "charlie123" and value col expands to fit "20000000000000000" + let keys: &[&dyn ConsoleFmt] = + &[&String::from("alice"), &String::from("bob"), &String::from("charlie123")]; + let values: &[&dyn ConsoleFmt] = &[ + &U256::from(1), + &U256::from_str("20000000000000000").unwrap(), + &U256::from_str("30000000000").unwrap(), + ]; + assert_eq!( + console_table_format(Some(keys), values), + "┌────────────┬───────────────────┐\n\ + │ (index) │ Values │\n\ + ├────────────┼───────────────────┤\n\ + │ alice │ 1 │\n\ + │ bob │ 20000000000000000 │\n\ + │ charlie123 │ 30000000000 │\n\ + └────────────┴───────────────────┘" + ); + + // empty table + assert_eq!( + console_table_format(None, &[]), + "┌─────────┬────────┐\n\ + │ (index) │ Values │\n\ + ├─────────┼────────┤\n\ + └─────────┴────────┘" + ); + + // more keys than values + let keys: &[&dyn ConsoleFmt] = + &[&String::from("alice"), &String::from("bob"), &String::from("charlie")]; + let values: &[&dyn ConsoleFmt] = &[&U256::from(1), &U256::from(2)]; + assert_eq!( + console_table_format(Some(keys), values), + "┌─────────┬────────┐\n\ + │ (index) │ Values │\n\ + ├─────────┼────────┤\n\ + │ alice │ 1 │\n\ + │ bob │ 2 │\n\ + │ charlie │ │\n\ + └─────────┴────────┘" + ); + + // more values than keys + let keys: &[&dyn ConsoleFmt] = &[&String::from("alice"), &String::from("bob")]; + let values: &[&dyn ConsoleFmt] = + &[&U256::from(1), &U256::from(2), &U256::from(3), &U256::from(4)]; + assert_eq!( + console_table_format(Some(keys), values), + "┌─────────┬────────┐\n\ + │ (index) │ Values │\n\ + ├─────────┼────────┤\n\ + │ alice │ 1 │\n\ + │ bob │ 2 │\n\ + │ │ 3 │\n\ + │ │ 4 │\n\ + └─────────┴────────┘" + ); + } } diff --git a/crates/common/fmt/src/lib.rs b/crates/common/fmt/src/lib.rs index 89297a8e2c6b5..45ed47263ce32 100644 --- a/crates/common/fmt/src/lib.rs +++ b/crates/common/fmt/src/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] mod console; -pub use console::{ConsoleFmt, FormatSpec, console_format}; +pub use console::{ConsoleFmt, FormatSpec, console_format, console_table_format}; mod dynamic; pub use dynamic::{ diff --git a/crates/common/fmt/src/ui.rs b/crates/common/fmt/src/ui.rs index 964b26b978fbe..e883810dcda34 100644 --- a/crates/common/fmt/src/ui.rs +++ b/crates/common/fmt/src/ui.rs @@ -18,7 +18,7 @@ use alloy_rpc_types::{ AccessListItem, Block, BlockTransactions, Header, Log, Transaction, TransactionReceipt, }; use alloy_serde::{OtherFields, WithOtherFields}; -use op_alloy_consensus::{OpTxEnvelope, TxDeposit}; +use op_alloy_consensus::{OpTxEnvelope, TxDeposit, TxPostExec}; use revm::context_interface::transaction::SignedAuthorization; use serde::Deserialize; use tempo_alloy::{ @@ -472,6 +472,20 @@ input {}", } } +impl UIfmt for TxPostExec { + fn pretty(&self) -> String { + format!( + " +blockNumber {} +gasRefundEntries {:?} +input {}", + self.payload.block_number.pretty(), + self.payload.gas_refund_entries, + self.input.pretty(), + ) + } +} + impl UIfmt for Call { fn pretty(&self) -> String { format!( @@ -600,6 +614,7 @@ impl UIfmt for OpTxEnvelope { Self::Eip1559(tx) => tx.pretty(), Self::Eip7702(tx) => tx.pretty(), Self::Deposit(tx) => tx.pretty(), + Self::PostExec(tx) => tx.pretty(), } } } diff --git a/crates/common/src/abi.rs b/crates/common/src/abi.rs index 425892a16b869..358f3edd01bb1 100644 --- a/crates/common/src/abi.rs +++ b/crates/common/src/abi.rs @@ -143,8 +143,13 @@ pub async fn get_func_etherscan( args: &[String], chain: Chain, etherscan_api_key: &str, + etherscan_api_url: Option<&str>, ) -> Result { - let client = Client::new(chain, etherscan_api_key)?; + let client = if let Some(api_url) = etherscan_api_url { + Client::builder().with_api_key(etherscan_api_key).with_api_url(api_url)?.build()? + } else { + Client::new(chain, etherscan_api_key)? + }; let source = find_source(client, contract).await?; let metadata = source.items.first().wrap_err("etherscan returned empty metadata")?; diff --git a/crates/common/src/provider/mpp/mod.rs b/crates/common/src/provider/mpp/mod.rs index fe457567559db..a1a3ae8b8c483 100644 --- a/crates/common/src/provider/mpp/mod.rs +++ b/crates/common/src/provider/mpp/mod.rs @@ -7,3 +7,4 @@ pub mod keys; pub mod persist; pub mod session; pub mod transport; +pub mod ws; diff --git a/crates/common/src/provider/mpp/persist.rs b/crates/common/src/provider/mpp/persist.rs index 6d6781ab4c77b..803c7270e041e 100644 --- a/crates/common/src/provider/mpp/persist.rs +++ b/crates/common/src/provider/mpp/persist.rs @@ -1,243 +1,241 @@ //! Persistent channel storage for MPP sessions. //! -//! Stores open payment channel state in a JSON file at -//! `$TEMPO_HOME/foundry/channels.json` (default: `~/.tempo/foundry/channels.json`). +//! Stores open payment channel state in a SQLite database at +//! `$TEMPO_HOME/channels.db` (default: `~/.tempo/channels.db`). //! This allows channel reuse across process invocations, avoiding the cost of //! opening a new on-chain channel for every `cast` / `forge` command. use alloy_primitives::{Address, B256}; +use foundry_wallets::{Channel, ChannelDb}; use mpp::client::channel_ops::ChannelEntry; -use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, - path::PathBuf, + sync::OnceLock, time::{SystemTime, UNIX_EPOCH}, }; use tracing::{debug, warn}; use crate::tempo::tempo_home; -/// Relative path from Tempo home to the Foundry channels file. -const CHANNELS_PATH: &str = "foundry/channels.json"; +/// Process-wide database handle. +fn global_db() -> Option<&'static ChannelDb> { + static DB: OnceLock> = OnceLock::new(); + DB.get_or_init(|| { + let path = tempo_home()?.join("channels.db"); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Some(old) = + tempo_home().map(|h| h.join("foundry/channels.json")).filter(|p| p.exists()) + { + warn!( + ?old, + "found old channels.json — this file is no longer used; channels will be re-opened" + ); + } -/// Current schema version. -const SCHEMA_VERSION: u64 = 2; + match ChannelDb::open(&path) { + Ok(db) => { + debug!(?path, "opened channel database"); + Some(db) + } + Err(e) => { + warn!(?path, %e, "failed to open channel database"); + None + } + } + }) + .as_ref() +} -/// On-disk representation of the channel store. -#[derive(Debug, Serialize, Deserialize)] -struct ChannelStore { - version: u64, - #[serde(default)] - channels: HashMap, +/// Reconstruct the composite HashMap key from a persisted `Channel`. +/// +/// Mirrors `SessionProvider::channel_key()` in session.rs. +fn channel_key_from_persisted(ch: &Channel) -> String { + let origin_hash = &alloy_primitives::keccak256(ch.origin.as_bytes()).to_string()[..18]; + format!( + "{}:{}:{}:{}:{}:{}:{}", + origin_hash, + ch.chain_id, + ch.payer, + ch.authorized_signer, + ch.payee, + ch.token, + ch.escrow_contract + ) + .to_lowercase() } -impl Default for ChannelStore { - fn default() -> Self { - Self { version: SCHEMA_VERSION, channels: HashMap::new() } +/// Whether a channel can still be used (active and not fully spent). +fn is_usable(ch: &Channel) -> bool { + if ch.state != "active" { + return false; } + let cumulative: u128 = ch.cumulative_amount.parse().unwrap_or(u128::MAX); + let deposit: u128 = ch.deposit.parse().unwrap_or(0); + cumulative < deposit } -/// A persisted channel entry. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersistedChannel { - pub channel_id: String, - pub salt: String, - pub escrow_contract: String, - pub chain_id: u64, - pub cumulative_amount: String, - pub deposit: String, - pub status: String, - pub origin: String, - pub created_at: u64, - pub last_used_at: u64, +/// Convert a persisted `Channel` to a `ChannelEntry`. +pub fn to_channel_entry(ch: &Channel) -> Option { + let channel_id: B256 = ch.channel_id.parse().ok()?; + let salt: B256 = ch.salt.parse().ok()?; + let escrow_contract: Address = ch.escrow_contract.parse().ok()?; + let cumulative_amount: u128 = ch.cumulative_amount.parse().ok()?; + + Some(ChannelEntry { + channel_id, + salt, + cumulative_amount, + escrow_contract, + chain_id: ch.chain_id as u64, + opened: ch.state == "active", + }) } -impl PersistedChannel { - /// Convert to an mpp `ChannelEntry` for use in the session provider. - pub fn to_channel_entry(&self) -> Option { - let channel_id: B256 = self.channel_id.parse().ok()?; - let salt: B256 = self.salt.parse().ok()?; - let escrow_contract: Address = self.escrow_contract.parse().ok()?; - let cumulative_amount: u128 = self.cumulative_amount.parse().ok()?; - - Some(ChannelEntry { - channel_id, - salt, - cumulative_amount, - escrow_contract, - chain_id: self.chain_id, - opened: self.status == "active", - }) - } - - /// Create from a `ChannelEntry` with metadata. - pub fn from_channel_entry(entry: &ChannelEntry, deposit: u128, origin: &str) -> Self { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); - - Self { - channel_id: entry.channel_id.to_string(), - salt: entry.salt.to_string(), - escrow_contract: entry.escrow_contract.to_string(), - chain_id: entry.chain_id, - cumulative_amount: entry.cumulative_amount.to_string(), - deposit: deposit.to_string(), - status: if entry.opened { "active" } else { "closed" }.to_string(), - origin: origin.to_string(), - created_at: now, - last_used_at: now, - } - } - - /// Whether this channel can still be used (active and not fully spent). - fn is_usable(&self) -> bool { - if self.status != "active" { - return false; - } - let cumulative: u128 = self.cumulative_amount.parse().unwrap_or(u128::MAX); - let deposit: u128 = self.deposit.parse().unwrap_or(0); - cumulative < deposit +/// Create a `Channel` from a `ChannelEntry` with metadata. +pub fn from_channel_entry( + entry: &ChannelEntry, + deposit: u128, + origin: &str, + payer: &Address, + payee: &Address, + token: &Address, + authorized_signer: &Address, +) -> Channel { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64; + + Channel { + channel_id: entry.channel_id.to_string(), + version: 1, + origin: origin.to_string(), + request_url: String::new(), + chain_id: entry.chain_id as i64, + escrow_contract: entry.escrow_contract.to_string(), + token: token.to_string(), + payee: payee.to_string(), + payer: payer.to_string(), + authorized_signer: authorized_signer.to_string(), + salt: entry.salt.to_string(), + deposit: deposit.to_string(), + cumulative_amount: entry.cumulative_amount.to_string(), + challenge_echo: String::new(), + state: if entry.opened { "active" } else { "closed" }.to_string(), + close_requested_at: 0, + grace_ready_at: 0, + created_at: now, + last_used_at: now, } } -/// Returns the path to the channels file. -fn channels_path() -> Option { - tempo_home().map(|home| home.join(CHANNELS_PATH)) -} - -/// Load channels from disk, evicting spent/inactive entries. -pub fn load_channels() -> HashMap { - let Some(path) = channels_path().filter(|p| p.exists()) else { +/// Load channels from database, evicting spent/inactive entries. +pub fn load_channels() -> HashMap { + let Some(db) = global_db() else { return HashMap::new(); }; - let Ok(contents) = std::fs::read_to_string(&path).inspect_err(|e| { - warn!(?path, %e, "failed to read channels file"); - }) else { - return HashMap::new(); - }; - - let Ok(store) = serde_json::from_str::(&contents).inspect_err(|e| { - warn!(?path, %e, "failed to parse channels file, starting fresh"); - }) else { - return HashMap::new(); + let channels = match db.load() { + Ok(channels) => channels, + Err(e) => { + warn!(%e, "failed to load channels from database"); + return HashMap::new(); + } }; - if store.version != SCHEMA_VERSION { - warn!( - version = store.version, - expected = SCHEMA_VERSION, - "channels file version mismatch, starting fresh" - ); - return HashMap::new(); - } - - // Evict spent/inactive entries - let usable: HashMap = - store.channels.into_iter().filter(|(_, ch)| ch.is_usable()).collect(); + let usable: HashMap = channels + .into_iter() + .filter(is_usable) + .map(|ch| { + let key = channel_key_from_persisted(&ch); + (key, ch) + }) + .collect(); debug!(count = usable.len(), "loaded persisted MPP channels"); usable } -/// Save channels to disk. -pub fn save_channels(channels: &HashMap) { - let Some(path) = channels_path() else { +/// Save channels to database. +pub fn save_channels(channels: &HashMap) { + let Some(db) = global_db() else { return; }; - if let Some(parent) = path.parent() - && let Err(e) = std::fs::create_dir_all(parent) - { - warn!(?path, %e, "failed to create channels directory"); - return; + for ch in channels.values() { + if let Err(e) = db.upsert(ch) { + warn!(%e, channel_id = %ch.channel_id, "failed to save channel"); + } } + debug!(count = channels.len(), "saved MPP channels"); +} - let store = ChannelStore { version: SCHEMA_VERSION, channels: channels.clone() }; - - match serde_json::to_string_pretty(&store) { - Ok(json) => { - if let Err(e) = std::fs::write(&path, json) { - warn!(?path, %e, "failed to write channels file"); - } else { - debug!(?path, count = channels.len(), "saved MPP channels"); - } - } - Err(e) => warn!(%e, "failed to serialize channels"), +/// Delete a channel from the database by its channel ID. +pub fn delete_channel_from_db(channel_id: &str) { + let Some(db) = global_db() else { + return; + }; + if let Err(e) = db.delete(channel_id) { + warn!(%e, channel_id, "failed to delete channel from database"); } } /// Look up a usable persisted channel by key. -pub fn find_channel( - channels: &HashMap, - key: &str, -) -> Option { - channels.get(key).filter(|ch| ch.is_usable()).and_then(|ch| ch.to_channel_entry()) +pub fn find_channel(channels: &HashMap, key: &str) -> Option { + channels.get(key).filter(|ch| is_usable(ch)).and_then(to_channel_entry) } -/// Insert or update a channel entry in memory only (no disk write). -/// -/// Use [`upsert_channel`] when you want to persist immediately, or call -/// [`save_channels`] separately after this. +/// Insert or update a channel entry in memory only (no DB write). pub fn upsert_channel_in_memory( - channels: &mut HashMap, + channels: &mut HashMap, key: &str, entry: &ChannelEntry, - deposit: u128, - origin: &str, ) { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64; if let Some(existing) = channels.get_mut(key) { existing.cumulative_amount = entry.cumulative_amount.to_string(); existing.last_used_at = now; - existing.status = if entry.opened { "active" } else { "closed" }.to_string(); + existing.state = if entry.opened { "active" } else { "closed" }.to_string(); } else { - channels - .insert(key.to_string(), PersistedChannel::from_channel_entry(entry, deposit, origin)); + warn!(key, "upsert_channel_in_memory called for unknown channel"); } } -/// Insert or update a channel entry and save to disk. -/// -/// When updating an existing entry, `deposit` is ignored (preserved from the -/// original open). When inserting a new entry, `deposit` is recorded. -pub fn upsert_channel( - channels: &mut HashMap, - key: &str, - entry: &ChannelEntry, - deposit: u128, - origin: &str, -) { - upsert_channel_in_memory(channels, key, entry, deposit, origin); - save_channels(channels); -} - #[cfg(test)] mod tests { use super::*; - fn test_channel(status: &str, cumulative: &str, deposit: &str) -> PersistedChannel { - PersistedChannel { + fn test_channel(state: &str, cumulative: &str, deposit: &str) -> Channel { + Channel { channel_id: format!("0x{}", "ab".repeat(32)), - salt: format!("0x{}", "cd".repeat(32)), - escrow_contract: "0xe1c4d3dce17bc111181ddf716f75bae49e61a336".to_string(), + version: 1, + origin: "https://rpc.mpp.moderato.tempo.xyz".to_string(), + request_url: String::new(), chain_id: 42431, - cumulative_amount: cumulative.to_string(), + escrow_contract: "0xe1c4d3dce17bc111181ddf716f75bae49e61a336".to_string(), + token: "0x20c0000000000000000000000000000000000000".to_string(), + payee: "0x3333333333333333333333333333333333333333".to_string(), + payer: "0x1111111111111111111111111111111111111111".to_string(), + authorized_signer: "0x1111111111111111111111111111111111111111".to_string(), + salt: format!("0x{}", "cd".repeat(32)), deposit: deposit.to_string(), - status: status.to_string(), - origin: "https://rpc.mpp.moderato.tempo.xyz".to_string(), + cumulative_amount: cumulative.to_string(), + challenge_echo: String::new(), + state: state.to_string(), + close_requested_at: 0, + grace_ready_at: 0, created_at: 1000, last_used_at: 1000, } } #[test] - fn is_usable() { - assert!(test_channel("active", "5000", "100000").is_usable()); - assert!(!test_channel("active", "100000", "100000").is_usable()); - assert!(!test_channel("active", "200000", "100000").is_usable()); - assert!(!test_channel("closed", "0", "100000").is_usable()); - assert!(!test_channel("closing", "0", "100000").is_usable()); + fn usable() { + assert!(is_usable(&test_channel("active", "5000", "100000"))); + assert!(!is_usable(&test_channel("active", "100000", "100000"))); + assert!(!is_usable(&test_channel("active", "200000", "100000"))); + assert!(!is_usable(&test_channel("closed", "0", "100000"))); + assert!(!is_usable(&test_channel("closing", "0", "100000"))); } #[test] @@ -251,8 +249,12 @@ mod tests { opened: true, }; - let persisted = PersistedChannel::from_channel_entry(&entry, 100_000, "https://rpc.test"); - let restored = persisted.to_channel_entry().expect("should parse back"); + let payer = Address::random(); + let payee = Address::random(); + let token = Address::random(); + let persisted = + from_channel_entry(&entry, 100_000, "https://rpc.test", &payer, &payee, &token, &payer); + let restored = to_channel_entry(&persisted).expect("should parse back"); assert_eq!(restored.channel_id, entry.channel_id); assert_eq!(restored.salt, entry.salt); @@ -262,46 +264,6 @@ mod tests { assert!(restored.opened); } - #[test] - fn load_evicts_and_handles_edge_cases() { - let dir = tempfile::tempdir().unwrap(); - let foundry_dir = dir.path().join("foundry"); - std::fs::create_dir_all(&foundry_dir).unwrap(); - - let store = ChannelStore { - version: SCHEMA_VERSION, - channels: HashMap::from([ - ("active".into(), test_channel("active", "1000", "100000")), - ("spent".into(), test_channel("active", "100000", "100000")), - ("closed".into(), test_channel("closed", "0", "100000")), - ]), - }; - let json = serde_json::to_string(&store).unwrap(); - std::fs::write(foundry_dir.join("channels.json"), &json).unwrap(); - - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - let loaded = load_channels(); - unsafe { std::env::remove_var("TEMPO_HOME") }; - - assert_eq!(loaded.len(), 1); - assert!(loaded.contains_key("active")); - } - - #[test] - fn load_missing_and_wrong_version() { - let dir = tempfile::tempdir().unwrap(); - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - assert!(load_channels().is_empty()); - - let foundry_dir = dir.path().join("foundry"); - std::fs::create_dir_all(&foundry_dir).unwrap(); - std::fs::write(foundry_dir.join("channels.json"), r#"{"version": 999, "channels": {}}"#) - .unwrap(); - assert!(load_channels().is_empty()); - - unsafe { std::env::remove_var("TEMPO_HOME") }; - } - #[test] fn find_channel_filters_unusable() { let mut channels = HashMap::new(); @@ -312,34 +274,4 @@ mod tests { assert!(find_channel(&channels, "spent").is_none()); assert!(find_channel(&channels, "missing").is_none()); } - - #[test] - fn upsert_inserts_and_updates() { - let dir = tempfile::tempdir().unwrap(); - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - - let mut channels = HashMap::new(); - let entry = ChannelEntry { - channel_id: B256::random(), - salt: B256::random(), - cumulative_amount: 1000, - escrow_contract: Address::random(), - chain_id: 42431, - opened: true, - }; - - upsert_channel(&mut channels, "key1", &entry, 100_000, "https://rpc.test"); - assert_eq!(channels["key1"].cumulative_amount, "1000"); - assert_eq!(channels["key1"].deposit, "100000"); - let created_at = channels["key1"].created_at; - - let mut updated = entry.clone(); - updated.cumulative_amount = 5000; - upsert_channel(&mut channels, "key1", &updated, 0, "https://rpc.test"); - assert_eq!(channels["key1"].cumulative_amount, "5000"); - assert_eq!(channels["key1"].deposit, "100000"); - assert_eq!(channels["key1"].created_at, created_at); - - unsafe { std::env::remove_var("TEMPO_HOME") }; - } } diff --git a/crates/common/src/provider/mpp/session.rs b/crates/common/src/provider/mpp/session.rs index 5996c933218d2..334166b844613 100644 --- a/crates/common/src/provider/mpp/session.rs +++ b/crates/common/src/provider/mpp/session.rs @@ -6,8 +6,9 @@ //! `eth_getTransactionCount`. This avoids the chicken-and-egg problem when //! the RPC endpoint is itself 402-gated. -use super::persist::{self, PersistedChannel}; +use super::persist; use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; +use foundry_wallets::Channel; use mpp::{ client::{ PaymentProvider, @@ -42,7 +43,7 @@ static GLOBAL_CHANNELS: OnceLock>> = O /// /// Using a single map ensures saves from different origins don't clobber /// each other's state. -static GLOBAL_PERSISTED: OnceLock>>> = OnceLock::new(); +static GLOBAL_PERSISTED: OnceLock>>> = OnceLock::new(); /// Tracks uncommitted channel state from the most recent payment. /// @@ -86,7 +87,7 @@ pub struct SessionProvider { default_deposit: Option, channels: Arc>>, key_provisioned: Arc>, - persisted: Arc>>, + persisted: Arc>>, /// Tracks uncommitted open/top-up state for deferred persistence. pending: Arc>>, /// Chain ID from the key entry in `keys.toml` that was used to initialize @@ -128,10 +129,10 @@ impl SessionProvider { map.entry(origin.clone()) .or_insert_with(|| { // Hydrate only channels belonging to this origin. - let mut channels = HashMap::new(); + let mut channels: HashMap = HashMap::new(); for (key, ch) in persisted.lock().unwrap().iter() { if ch.origin == origin - && let Some(entry) = ch.to_channel_entry() + && let Some(entry) = persist::to_channel_entry(ch) { channels.insert(key.clone(), entry); } @@ -209,16 +210,16 @@ impl SessionProvider { // Lock order: channels → persisted (consistent with pay_session) let mut channels = self.channels.lock().unwrap(); let mut persisted = self.persisted.lock().unwrap(); - let keys_to_remove: Vec = persisted + let keys_to_remove: Vec<(String, String)> = persisted .iter() .filter(|(_, ch)| ch.origin == *origin) - .map(|(k, _)| k.clone()) + .map(|(k, ch): (&String, &Channel)| (k.clone(), ch.channel_id.clone())) .collect(); - for key in &keys_to_remove { + for (key, channel_id) in &keys_to_remove { channels.remove(key); persisted.remove(key); + persist::delete_channel_from_db(channel_id); } - persist::save_channels(&persisted); } /// Mark whether the access key has been provisioned on-chain. @@ -685,13 +686,7 @@ impl SessionProvider { // confirms acceptance. let updated_entry = ChannelEntry { cumulative_amount: new_cumulative, ..entry }; let mut persisted = self.persisted.lock().unwrap(); - persist::upsert_channel_in_memory( - &mut persisted, - &key, - &updated_entry, - 0, - &self.origin, - ); + persist::upsert_channel_in_memory(&mut persisted, &key, &updated_entry); drop(persisted); // Track the voucher so we can roll back cumulative_amount @@ -727,9 +722,18 @@ impl SessionProvider { // Update in-memory state but defer disk persistence until server confirms. self.channels.lock().unwrap().insert(key.clone(), entry.clone()); + let authorized_signer = self.authorized_signer.unwrap_or(payer); self.persisted.lock().unwrap().insert( key.clone(), - persist::PersistedChannel::from_channel_entry(&entry, deposit, &self.origin), + persist::from_channel_entry( + &entry, + deposit, + &self.origin, + &payer, + &payee, + ¤cy, + &authorized_signer, + ), ); *self.pending.lock().unwrap() = Some(PendingAction::Open { key }); Ok(build_credential(challenge, payload, chain_id, payer)) diff --git a/crates/common/src/provider/mpp/transport.rs b/crates/common/src/provider/mpp/transport.rs index aea72174c9756..12f0717ab8732 100644 --- a/crates/common/src/provider/mpp/transport.rs +++ b/crates/common/src/provider/mpp/transport.rs @@ -64,7 +64,7 @@ pub struct LazySessionProvider { } impl LazySessionProvider { - fn new(origin: String) -> Self { + pub(super) fn new(origin: String) -> Self { let pay_lock = { let global = GLOBAL_PAY_LOCKS.get_or_init(|| Mutex::new(HashMap::new())); global @@ -89,13 +89,13 @@ impl LazySessionProvider { } } - fn flush_pending(&self) { + pub(super) fn flush_pending(&self) { if let Some(p) = self.inner.lock().unwrap().as_ref() { p.flush_pending(); } } - fn rollback_pending(&self) { + pub(super) fn rollback_pending(&self) { if let Some(p) = self.inner.lock().unwrap().as_ref() { p.rollback_pending(); } @@ -107,7 +107,7 @@ impl LazySessionProvider { } } - fn get_or_init(&self, opts: DiscoverOptions) -> TransportResult { + pub(super) fn get_or_init(&self, opts: DiscoverOptions) -> TransportResult { let mut guard = self.inner.lock().unwrap(); if let Some(ref provider) = *guard { return Ok(provider.clone()); @@ -527,7 +527,7 @@ where } /// Extract `(chainId, currency)` from a parsed MPP challenge. -fn extract_challenge_chain_and_currency( +pub(super) fn extract_challenge_chain_and_currency( c: &mpp::protocol::core::PaymentChallenge, ) -> (Option, Option) { if c.method.as_str() == "tempo" { diff --git a/crates/common/src/provider/mpp/ws.rs b/crates/common/src/provider/mpp/ws.rs new file mode 100644 index 0000000000000..f631d0b08a2fc --- /dev/null +++ b/crates/common/src/provider/mpp/ws.rs @@ -0,0 +1,492 @@ +//! MPP WebSocket transport. +//! +//! Implements [`PubSubConnect`] with automatic MPP 402 challenge/credential +//! handshake at WebSocket connect time. Non-MPP servers are handled via a +//! timeout-based fallback: if no challenge frame arrives within a short window +//! after connecting, we assume the server is a plain JSON-RPC WebSocket. + +use alloy_json_rpc::PubSubItem; +use alloy_pubsub::{ConnectionHandle, PubSubConnect}; +use alloy_transport::{Authorization, TransportErrorKind, TransportResult, utils::guess_local_url}; +use alloy_transport_ws::WsBackend; +use futures::{SinkExt, StreamExt}; +use mpp::{ + client::{ + PaymentProvider, + ws::{WsClientMessage, WsServerMessage}, + }, + protocol::core::{PaymentChallenge, format_authorization}, +}; +use rustls::crypto::{CryptoProvider, aws_lc_rs}; +use std::{io, time::Duration}; +use tokio::time::timeout; +use tokio_tungstenite::{ + MaybeTlsStream, WebSocketStream, connect_async, + tungstenite::{ + Message, + client::IntoClientRequest, + http::{HeaderValue, header::AUTHORIZATION}, + }, +}; +use tracing::debug; + +use super::{ + keys::DiscoverOptions, + transport::{LazySessionProvider, extract_challenge_chain_and_currency}, +}; + +type TungsteniteStream = WebSocketStream>; + +/// Timeout for waiting on an MPP challenge frame from the server. +/// +/// If no challenge arrives within this window after the WebSocket upgrade +/// completes, we assume it's a plain (non-MPP) JSON-RPC WebSocket. +const MPP_CHALLENGE_TIMEOUT: Duration = Duration::from_millis(500); + +/// Keepalive ping interval (matches alloy-transport-ws default). +const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10); + +/// WebSocket connector with automatic MPP payment at connect time. +/// +/// Implements [`PubSubConnect`] so it can be used as a drop-in replacement for +/// alloy's `WsConnect`. On connect, it: +/// +/// 1. Opens a WebSocket connection. +/// 2. Waits briefly for an MPP challenge frame from the server. +/// 3. If a challenge arrives, performs the payment handshake using [`LazySessionProvider`] (same +/// payment logic as the HTTP transport). +/// 4. Spawns a backend loop that bridges the authenticated WebSocket to the alloy +/// [`PubSubFrontend`](alloy_pubsub::PubSubFrontend). +/// +/// Non-MPP servers work transparently — the timeout expires and the backend +/// proceeds with normal JSON-RPC message forwarding. +#[derive(Clone, Debug)] +pub struct MppWsConnect { + url: String, + auth: Option, + provider: LazySessionProvider, +} + +impl MppWsConnect { + /// Create a new MPP WebSocket connector for the given URL. + pub fn new(url: String) -> Self { + let origin = url.clone(); + let auth = + url::Url::parse(&url).ok().and_then(|parsed| Authorization::extract_from_url(&parsed)); + Self { url, auth, provider: LazySessionProvider::new(origin) } + } + + /// Set the authorization header (e.g. JWT bearer token). + pub fn with_auth(mut self, auth: Authorization) -> Self { + self.auth = Some(auth); + self + } + + /// Attempt the MPP handshake on an already-connected WebSocket. + /// + /// Waits up to [`MPP_CHALLENGE_TIMEOUT`] for a challenge frame. If one + /// arrives, pays it and sends the credential back. Returns any buffered + /// non-challenge messages that arrived during the handshake window. + async fn try_mpp_handshake( + socket: &mut TungsteniteStream, + provider: &LazySessionProvider, + ) -> TransportResult> { + let mut buffered_messages: Vec = Vec::new(); + + // Wait briefly for a challenge frame. + let first_msg = timeout(MPP_CHALLENGE_TIMEOUT, socket.next()).await; + + let challenge_frame = match first_msg { + Err(_) => { + // Timeout — not an MPP server. + debug!("no MPP challenge within timeout, treating as plain WS"); + return Ok(buffered_messages); + } + Ok(None) => { + return Err(TransportErrorKind::custom(io::Error::other( + "WebSocket closed before any message", + ))); + } + Ok(Some(Err(e))) => { + return Err(TransportErrorKind::custom(e)); + } + Ok(Some(Ok(msg))) => msg, + }; + + let text = match &challenge_frame { + Message::Text(t) => t.as_ref(), + Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => { + return Ok(buffered_messages); + } + Message::Binary(_) | Message::Close(_) => { + return Err(TransportErrorKind::custom(io::Error::other( + "unexpected binary/close frame on WS connect", + ))); + } + }; + + // Try to parse as an MPP server message. + let server_msg: WsServerMessage = match serde_json::from_str(text) { + Ok(m) => m, + Err(_) => { + // Not an MPP message — buffer it for the backend. + buffered_messages.push(text.to_owned()); + return Ok(buffered_messages); + } + }; + + let challenge: PaymentChallenge = match server_msg { + WsServerMessage::Challenge { ref challenge, .. } => { + serde_json::from_value(challenge.clone()).map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "failed to parse MPP WS challenge: {e}" + ))) + })? + } + _ => { + // Non-challenge MPP message — buffer it. + buffered_messages.push(text.to_owned()); + return Ok(buffered_messages); + } + }; + + debug!(id = %challenge.id, method = %challenge.method, intent = %challenge.intent, "received MPP WS challenge, paying"); + + // Resolve the payment provider (lazily discovers keys on first use). + let (chain_id, currency) = extract_challenge_chain_and_currency(&challenge); + let currency = currency.and_then(|s| s.parse().ok()); + let session = + provider.get_or_init(DiscoverOptions { chain_id, currency }).map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "MPP key discovery failed: {e}" + ))) + })?; + + let credential = session.pay(&challenge).await.map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!("MPP WS payment failed: {e}"))) + })?; + + // Everything after pay() must rollback on failure — wrap so we can't + // miss an error path. + let result = async { + let auth_header = format_authorization(&credential).map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "failed to format MPP credential: {e}" + ))) + })?; + + // Send credential as a WS message. + let cred_msg = WsClientMessage::Credential { credential: auth_header }; + let cred_text = serde_json::to_string(&cred_msg).map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "failed to serialize credential message: {e}" + ))) + })?; + + socket.send(Message::Text(cred_text.into())).await.map_err(|e| { + TransportErrorKind::custom(io::Error::other(format!( + "failed to send MPP credential: {e}" + ))) + })?; + + // Wait for server acknowledgement (receipt or data). + let ack = timeout(Duration::from_secs(30), socket.next()).await.map_err(|_| { + TransportErrorKind::custom(io::Error::other( + "timeout waiting for MPP server acknowledgement", + )) + })?; + + match ack { + None => { + return Err(TransportErrorKind::custom(io::Error::other( + "WebSocket closed after sending credential", + ))); + } + Some(Err(e)) => return Err(TransportErrorKind::custom(e)), + Some(Ok(Message::Text(t))) => { + if let Ok(msg) = serde_json::from_str::(t.as_ref()) { + match msg { + WsServerMessage::Receipt { .. } => { + debug!("MPP WS handshake complete (receipt received)"); + } + WsServerMessage::Error { error } => { + return Err(TransportErrorKind::custom(io::Error::other(format!( + "MPP WS server error: {error}" + )))); + } + _ => { + buffered_messages.push(t.to_string()); + } + } + } else { + buffered_messages.push(t.to_string()); + } + } + Some(Ok(Message::Close(_))) => { + return Err(TransportErrorKind::custom(io::Error::other( + "WebSocket closed after sending credential", + ))); + } + Some(Ok(_)) => {} + } + + Ok(buffered_messages) + } + .await; + + match &result { + Ok(_) => provider.flush_pending(), + Err(_) => provider.rollback_pending(), + } + + result + } +} + +impl PubSubConnect for MppWsConnect { + fn is_local(&self) -> bool { + guess_local_url(&self.url) + } + + async fn connect(&self) -> TransportResult { + let mut request = + self.url.as_str().into_client_request().map_err(TransportErrorKind::custom)?; + + if let Some(ref auth) = self.auth { + let mut auth_value = + HeaderValue::from_str(&auth.to_string()).map_err(TransportErrorKind::custom)?; + auth_value.set_sensitive(true); + request.headers_mut().insert(AUTHORIZATION, auth_value); + } + + // Install the default rustls crypto provider (required by rustls 0.23+). + let _ = CryptoProvider::install_default(aws_lc_rs::default_provider()); + + let (mut socket, _) = connect_async(request).await.map_err(TransportErrorKind::custom)?; + + // Attempt MPP handshake (timeout-based fallback for non-MPP servers). + let buffered = Self::try_mpp_handshake(&mut socket, &self.provider).await?; + + let (handle, interface) = ConnectionHandle::new(); + + // Replay any messages that arrived during the handshake window. + for msg in &buffered { + if let Ok(item) = serde_json::from_str::(msg) { + let _ = interface.send_to_frontend(item); + } + } + + // Reuse alloy's WsBackend for the post-handshake JSON-RPC loop. + WsBackend::from_socket(socket, interface, KEEPALIVE_INTERVAL).spawn(); + + Ok(handle) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_json_rpc::{Id, Request, RequestMeta, RequestPacket, ResponsePacket}; + use alloy_primitives::hex; + use mpp::{ + PrivateKeySigner, + protocol::core::{Base64UrlJson, IntentName, MethodName, parse_authorization}, + }; + use tokio::task::JoinHandle; + use tokio_tungstenite::accept_async; + use tower::Service; + + fn test_challenge() -> PaymentChallenge { + let request = Base64UrlJson::from_value(&serde_json::json!({ + "amount": "1000", + "currency": "0x0000000000000000000000000000000000000000", + "recipient": "0x0000000000000000000000000000000000000001", + "methodDetails": { + "chainId": 42431 + } + })) + .unwrap(); + + PaymentChallenge { + id: "ws-test-id".to_string(), + realm: "test-realm".to_string(), + method: MethodName::new("tempo"), + intent: IntentName::new("session"), + request, + expires: None, + description: None, + digest: None, + opaque: None, + } + } + + /// Spawn a WS server on localhost, returns (ws_url, join_handle). + /// `handler` receives the server-side socket for full control. + async fn spawn_ws_server(handler: F) -> (String, JoinHandle<()>) + where + F: FnOnce(WebSocketStream) -> Fut + Send + 'static, + Fut: std::future::Future + Send, + { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let handle = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let ws = accept_async(stream).await.unwrap(); + handler(ws).await; + }); + (format!("ws://{addr}"), handle) + } + + /// Plain WS server (no MPP) — connect and send a JSON-RPC request. + #[tokio::test] + async fn test_ws_no_mpp_plain_jsonrpc() { + let (url, server) = spawn_ws_server(|mut ws| async move { + // Wait for a JSON-RPC request, echo a response. + while let Some(Ok(msg)) = ws.next().await { + if let Message::Text(text) = msg { + let req: serde_json::Value = serde_json::from_str(&text).unwrap(); + let id = req.get("id").unwrap().clone(); + let resp = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": "0xabc" + }); + ws.send(Message::Text(resp.to_string().into())).await.unwrap(); + } + } + }) + .await; + + let connector = MppWsConnect::new(url); + let mut frontend = connector.into_service().await.unwrap(); + + let req = Request { + meta: RequestMeta::new("eth_blockNumber".into(), Id::Number(1)), + params: serde_json::Value::Array(vec![]), + }; + let packet = RequestPacket::Single(req.serialize().unwrap()); + let resp = frontend.call(packet).await.unwrap(); + + match resp { + ResponsePacket::Single(r) => assert!(r.is_success()), + _ => panic!("expected single response"), + } + + server.abort(); + } + + /// MPP server sends challenge → client pays → server sends receipt. + #[tokio::test] + async fn test_ws_mpp_challenge_credential_receipt() { + let challenge = test_challenge(); + let challenge_json = serde_json::to_value(&challenge).unwrap(); + + let (url, server) = spawn_ws_server(move |mut ws| async move { + // Send challenge. + let challenge_msg = serde_json::json!({ + "type": "challenge", + "challenge": challenge_json + }); + ws.send(Message::Text(challenge_msg.to_string().into())).await.unwrap(); + + // Receive credential. + let msg = ws.next().await.unwrap().unwrap(); + let text = match msg { + Message::Text(t) => t, + other => panic!("expected text, got {other:?}"), + }; + let parsed: serde_json::Value = serde_json::from_str(&text).unwrap(); + assert_eq!(parsed["type"], "credential"); + // Verify it's a valid MPP credential. + let cred_str = parsed["credential"].as_str().unwrap(); + let cred = parse_authorization(cred_str).unwrap(); + assert_eq!(cred.challenge.id, "ws-test-id"); + + // Send receipt. + let receipt_msg = serde_json::json!({ + "type": "receipt", + "receipt": { "id": "ws-test-id" } + }); + ws.send(Message::Text(receipt_msg.to_string().into())).await.unwrap(); + + // Now serve JSON-RPC. + while let Some(Ok(msg)) = ws.next().await { + if let Message::Text(text) = msg { + let req: serde_json::Value = serde_json::from_str(&text).unwrap(); + let id = req.get("id").unwrap().clone(); + let resp = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": "0xpaid" + }); + ws.send(Message::Text(resp.to_string().into())).await.unwrap(); + } + } + }) + .await; + + // Set a random private key so LazySessionProvider can initialize. + let signer = PrivateKeySigner::random(); + let key_hex = hex::encode(signer.to_bytes()); + unsafe { std::env::set_var("TEMPO_PRIVATE_KEY", &key_hex) }; + + let connector = MppWsConnect::new(url); + let mut frontend = connector.into_service().await.unwrap(); + + let req = Request { + meta: RequestMeta::new("eth_blockNumber".into(), Id::Number(1)), + params: serde_json::Value::Array(vec![]), + }; + let packet = RequestPacket::Single(req.serialize().unwrap()); + let resp = frontend.call(packet).await.unwrap(); + + match resp { + ResponsePacket::Single(r) => assert!(r.is_success()), + _ => panic!("expected single response"), + } + + unsafe { std::env::remove_var("TEMPO_PRIVATE_KEY") }; + server.abort(); + } + + /// MPP server sends challenge, client pays, server closes → rollback. + #[tokio::test] + async fn test_ws_mpp_rollback_on_post_pay_close() { + let challenge = test_challenge(); + let challenge_json = serde_json::to_value(&challenge).unwrap(); + + let (url, server) = spawn_ws_server(move |mut ws| async move { + // Send challenge. + let challenge_msg = serde_json::json!({ + "type": "challenge", + "challenge": challenge_json + }); + ws.send(Message::Text(challenge_msg.to_string().into())).await.unwrap(); + + // Receive credential (client paid). + let _ = ws.next().await; + + // Close without sending receipt — simulates post-pay failure. + ws.close(None).await.ok(); + }) + .await; + + let signer = PrivateKeySigner::random(); + let key_hex = hex::encode(signer.to_bytes()); + unsafe { std::env::set_var("TEMPO_PRIVATE_KEY", &key_hex) }; + + let connector = MppWsConnect::new(url); + let result = connector.connect().await; + + // Connect must fail — server closed after credential was sent. + assert!(result.is_err(), "expected error when server closes after payment, got Ok"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("closed") || err.contains("WebSocket"), + "expected close-related error, got: {err}" + ); + + unsafe { std::env::remove_var("TEMPO_PRIVATE_KEY") }; + server.abort(); + } +} diff --git a/crates/common/src/provider/runtime_transport.rs b/crates/common/src/provider/runtime_transport.rs index 9767d39ec8096..7db1ebd1b3f91 100644 --- a/crates/common/src/provider/runtime_transport.rs +++ b/crates/common/src/provider/runtime_transport.rs @@ -2,7 +2,10 @@ //! WebSocket, or IPC transport. Retries are handled by a client layer (e.g., //! `RetryBackoffLayer`) when used. -use crate::{DEFAULT_USER_AGENT, REQUEST_TIMEOUT, provider::mpp::transport::LazyMppHttpTransport}; +use crate::{ + DEFAULT_USER_AGENT, REQUEST_TIMEOUT, + provider::mpp::{keys::discover_mpp_key, transport::LazyMppHttpTransport, ws::MppWsConnect}, +}; use alloy_json_rpc::{RequestPacket, ResponsePacket}; use alloy_pubsub::{PubSubConnect, PubSubFrontend}; use alloy_rpc_types_engine::{Claims, JwtSecret}; @@ -18,6 +21,17 @@ use tokio::sync::RwLock; use tower::Service; use url::Url; +/// Known MPP-enabled RPC host suffixes. +/// +/// Endpoints matching these patterns are always connected via [`MppWsConnect`], +/// regardless of whether local MPP keys have been discovered. +const KNOWN_MPP_HOSTS: &[&str] = &[".mpp.tempo.xyz"]; + +/// Returns `true` if `url` points to a known MPP-enabled RPC service. +fn is_known_mpp_endpoint(url: &Url) -> bool { + url.host_str().is_some_and(|host| KNOWN_MPP_HOSTS.iter().any(|suffix| host.ends_with(suffix))) +} + /// An enum representing the different transports that can be used to connect to a runtime. /// Only meant to be used internally by [RuntimeTransport]. #[derive(Clone, Debug)] @@ -245,16 +259,32 @@ impl RuntimeTransport { } /// Connects to a WS transport. + /// + /// Uses [`MppWsConnect`] (which performs the MPP challenge/credential + /// handshake at connect time) when the endpoint is a known MPP service or + /// when MPP keys are discoverable. Otherwise falls back to alloy's plain + /// [`WsConnect`] with zero overhead. async fn connect_ws(&self) -> Result { let auth = self.jwt.as_ref().and_then(|jwt| build_auth(jwt.clone()).ok()); - let mut ws = WsConnect::new(self.url.to_string()); - if let Some(auth) = auth { - ws = ws.with_auth(auth); + + let service = if is_known_mpp_endpoint(&self.url) && discover_mpp_key().is_some() { + let mut ws = MppWsConnect::new(self.url.to_string()); + if let Some(auth) = auth { + ws = ws.with_auth(auth); + } + ws.into_service() + .await + .map_err(|e| RuntimeTransportError::TransportError(e, self.url.to_string()))? + } else { + let mut ws = WsConnect::new(self.url.to_string()); + if let Some(auth) = auth { + ws = ws.with_auth(auth); + } + ws.into_service() + .await + .map_err(|e| RuntimeTransportError::TransportError(e, self.url.to_string()))? }; - let service = ws - .into_service() - .await - .map_err(|e| RuntimeTransportError::TransportError(e, self.url.to_string()))?; + Ok(InnerTransport::Ws(service)) } diff --git a/crates/common/src/tempo/mod.rs b/crates/common/src/tempo/mod.rs index d6c6340cc4d12..ef7be1b8e189e 100644 --- a/crates/common/src/tempo/mod.rs +++ b/crates/common/src/tempo/mod.rs @@ -2,3 +2,14 @@ mod keystore; pub use keystore::*; + +/// Conservative gas buffer for browser wallet transactions on Tempo chains. +/// +/// Browser wallets may sign with P256 or WebAuthn instead of secp256k1, which costs more gas +/// for signature verification. Since we can't determine the signature type before signing, +/// we add the worst-case (WebAuthn) overhead: +/// - P256: +5,000 gas (P256 precompile cost minus ecrecover savings) +/// - WebAuthn: ~6,500 gas (P256 cost + calldata for webauthn_data) +/// +/// See +pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index f148432c42bed..4934503ea66c4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -316,6 +316,8 @@ pub struct Config { pub etherscan: EtherscanConfigs, /// List of solidity error codes to always silence in the compiler output. pub ignored_error_codes: Vec, + /// List of (path prefix, solidity error codes) to silence in the compiler output. + pub ignored_error_codes_from: Vec<(PathBuf, Vec)>, /// List of file paths to ignore. #[serde(rename = "ignored_warnings_from")] pub ignored_file_paths: Vec, @@ -1235,6 +1237,10 @@ impl Config { pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result { let settings = self.compiler_settings()?; let paths = self.project_paths(); + + // Strip "./" prefix for consistent path matching + let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf(); + let mut builder = Project::builder() .artifacts(self.configured_artifacts_handler()) .additional_settings(self.additional_settings(&settings)) @@ -1242,15 +1248,10 @@ impl Config { .settings(settings) .paths(paths) .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into)) - .ignore_paths( - self.ignored_file_paths - .iter() - .map(|path| { - // Strip "./" prefix for consistent path matching - path.strip_prefix("./").unwrap_or(path).to_path_buf() - }) - .collect::>(), - ) + .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| { + (parse_path(path), codes.iter().copied().map(Into::into).collect()) + })) + .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect()) .set_compiler_severity_filter(if self.deny.warnings() { Severity::Warning } else { @@ -2716,6 +2717,7 @@ impl Default for Config { SolidityErrorCode::TransferDeprecated, SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated, ], + ignored_error_codes_from: vec![], ignored_file_paths: vec![], deny: DenyLevel::Never, deny_warnings: false, diff --git a/crates/evm/abi/src/Console.json b/crates/evm/abi/src/Console.json index 54e6d46dff349..f275f471087ee 100644 --- a/crates/evm/abi/src/Console.json +++ b/crates/evm/abi/src/Console.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes10","name":"","type":"bytes10"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes11","name":"","type":"bytes11"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes25","name":"","type":"bytes25"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"int256","name":"","type":"int256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes3","name":"","type":"bytes3"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes17","name":"","type":"bytes17"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes27","name":"","type":"bytes27"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"int256","name":"","type":"int256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes29","name":"","type":"bytes29"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes7","name":"","type":"bytes7"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes8","name":"","type":"bytes8"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes20","name":"","type":"bytes20"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes19","name":"","type":"bytes19"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes16","name":"","type":"bytes16"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes1","name":"","type":"bytes1"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes12","name":"","type":"bytes12"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes9","name":"","type":"bytes9"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes14","name":"","type":"bytes14"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes13","name":"","type":"bytes13"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes5","name":"","type":"bytes5"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes23","name":"","type":"bytes23"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes6","name":"","type":"bytes6"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes31","name":"","type":"bytes31"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes18","name":"","type":"bytes18"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes28","name":"","type":"bytes28"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes22","name":"","type":"bytes22"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes15","name":"","type":"bytes15"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes2","name":"","type":"bytes2"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes21","name":"","type":"bytes21"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes30","name":"","type":"bytes30"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes24","name":"","type":"bytes24"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes26","name":"","type":"bytes26"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes10","name":"","type":"bytes10"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes11","name":"","type":"bytes11"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes25","name":"","type":"bytes25"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes","name":"","type":"bytes"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"int256","name":"","type":"int256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes3","name":"","type":"bytes3"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes17","name":"","type":"bytes17"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes27","name":"","type":"bytes27"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"int256","name":"","type":"int256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes29","name":"","type":"bytes29"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes7","name":"","type":"bytes7"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes8","name":"","type":"bytes8"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes20","name":"","type":"bytes20"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes19","name":"","type":"bytes19"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes16","name":"","type":"bytes16"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes1","name":"","type":"bytes1"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes12","name":"","type":"bytes12"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes9","name":"","type":"bytes9"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes14","name":"","type":"bytes14"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes13","name":"","type":"bytes13"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes5","name":"","type":"bytes5"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes23","name":"","type":"bytes23"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes6","name":"","type":"bytes6"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes31","name":"","type":"bytes31"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes18","name":"","type":"bytes18"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes28","name":"","type":"bytes28"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes22","name":"","type":"bytes22"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes15","name":"","type":"bytes15"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes2","name":"","type":"bytes2"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes21","name":"","type":"bytes21"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes30","name":"","type":"bytes30"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes24","name":"","type":"bytes24"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes26","name":"","type":"bytes26"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"string","name":"","type":"string"},{"internalType":"address","name":"","type":"address"}],"name":"log","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"bool[]","name":"","type":"bool[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"string[]","name":"","type":"string[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address[]","name":"","type":"address[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bool[]","name":"","type":"bool[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"int256[]","name":"","type":"int256[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"int256[]","name":"","type":"int256[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"string[]","name":"","type":"string[]"},{"internalType":"address[]","name":"","type":"address[]"}],"name":"table","outputs":[],"stateMutability":"pure","type":"function"}] \ No newline at end of file diff --git a/crates/evm/abi/src/console.py b/crates/evm/abi/src/console.py index 2e28fece21e42..d0b32ff410941 100755 --- a/crates/evm/abi/src/console.py +++ b/crates/evm/abi/src/console.py @@ -16,12 +16,15 @@ def main(): # Parse signatures from `console.sol`'s string literals console_sol = open(console_file).read() sig_strings = re.findall( - r'"(log.*?)"', + r'"((?:log|table).*?)"', console_sol, ) raw_sigs = [s.strip().strip('"') for s in sig_strings] sigs = [ - s.replace("string", "string memory").replace("bytes)", "bytes memory)") + re.sub(r"(\w+\[\])", r"\1 memory", s) + .replace("string,", "string memory,") + .replace("string)", "string memory)") + .replace("bytes)", "bytes memory)") for s in raw_sigs ] sigs = list(set(sigs)) @@ -38,6 +41,7 @@ def main(): ) combined = json.loads(r.stdout.strip()) abi = combined["contracts"][":HardhatConsole"]["abi"] + open(abi_file, "w").write(json.dumps(abi, separators=(",", ":"), indent=None)) diff --git a/crates/evm/core/src/bytecode.rs b/crates/evm/core/src/bytecode.rs index 4e571e209d2fd..5e8b350eeb266 100644 --- a/crates/evm/core/src/bytecode.rs +++ b/crates/evm/core/src/bytecode.rs @@ -62,6 +62,7 @@ impl<'a> Iterator for InstIter<'a> { #[inline] fn next(&mut self) -> Option { self.iter.next().map(|&opcode| { + // SAFETY: OpCode wraps a u8, unknown opcodes are valid to construct. let opcode = unsafe { OpCode::new_unchecked(opcode) }; let len = imm_len(opcode.get()) as usize; let (immediate, rest) = self.iter.as_slice().split_at_checked(len).unwrap_or_default(); diff --git a/crates/evm/core/src/evm/op.rs b/crates/evm/core/src/evm/op.rs index 1b9db85cf2781..cb8bf272d9a05 100644 --- a/crates/evm/core/src/evm/op.rs +++ b/crates/evm/core/src/evm/op.rs @@ -1,24 +1,14 @@ -use std::ops::{Deref, DerefMut}; - -use alloy_evm::{Evm, EvmEnv, precompiles::PrecompilesMap}; -use alloy_op_evm::{OpEvmFactory, OpTx}; -use alloy_primitives::{Address, Bytes}; +use alloy_evm::{Evm, EvmEnv, EvmFactory, precompiles::PrecompilesMap}; +use alloy_op_evm::{OpEvm, OpEvmContext, OpEvmFactory, OpTx}; use foundry_fork_db::DatabaseError; -use op_revm::{ - L1BlockInfo, OpEvm, OpHaltReason, OpSpecId, OpTransaction, OpTransactionError, - handler::OpHandler, precompiles::OpPrecompiles, -}; +use op_revm::{OpEvm as RevmEvm, OpHaltReason, OpSpecId, OpTransactionError, handler::OpHandler}; use revm::{ - Context, Journal, MainContext, context::{ - BlockEnv, CfgEnv, ContextTr, LocalContextTr, - result::{ - EVMError, ExecResultAndState, ExecutionResult, HaltReason, InvalidTransaction, - ResultAndState, - }, + BlockEnv, ContextTr, LocalContextTr, + result::{EVMError, HaltReason, ResultAndState}, }, handler::{EthFrame, EvmTr, FrameResult, Handler, instructions::EthInstructions}, - inspector::{InspectorEvmTr, InspectorHandler}, + inspector::InspectorHandler, interpreter::{ FrameInput, SharedMemory, interpreter::EthInterpreter, interpreter_action::FrameInit, }, @@ -30,32 +20,21 @@ use crate::{ evm::{FoundryEvmFactory, NestedEvm}, }; -// Modified revm's OpContext with `OpTx` -pub type OpContext = Context, DB, Journal, L1BlockInfo>; - type OpEvmHandler<'db, I> = OpHandler, EVMError, EthFrame>; -pub type OpRevmEvm<'db, I> = op_revm::OpEvm< - OpContext<&'db mut dyn DatabaseExt>, +pub type OpRevmEvm<'db, I> = RevmEvm< + OpEvmContext<&'db mut dyn DatabaseExt>, I, - EthInstructions>>, + EthInstructions>>, PrecompilesMap, >; -/// Wraps [`op_revm::OpEvm`] and routes execution through [`OpHandler`]. -/// It uses foundry's custom [`OpContext`] as op-revm's one is not compatible with [`OpTx`]. -pub struct OpFoundryEvm< - 'db, - I: FoundryInspectorExt>>, -> { - pub inner: OpRevmEvm<'db, I>, -} - impl FoundryEvmFactory for OpEvmFactory { - type FoundryContext<'db> = OpContext<&'db mut dyn DatabaseExt>; + type FoundryContext<'db> = OpEvmContext<&'db mut dyn DatabaseExt>; - type FoundryEvm<'db, I: FoundryInspectorExt>> = OpFoundryEvm<'db, I>; + type FoundryEvm<'db, I: FoundryInspectorExt>> = + OpEvm<&'db mut dyn DatabaseExt, I, Self::Precompiles>; fn create_foundry_evm_with_inspector<'db, I: FoundryInspectorExt>>( &self, @@ -63,23 +42,10 @@ impl FoundryEvmFactory for OpEvmFactory { evm_env: EvmEnv, inspector: I, ) -> Self::FoundryEvm<'db, I> { - let spec_id = *evm_env.spec_id(); - let inner = Context::mainnet() - .with_tx(OpTx(OpTransaction::builder().build_fill())) - .with_cfg(CfgEnv::new_with_spec(OpSpecId::BEDROCK)) - .with_chain(L1BlockInfo::default()) - .with_db(db) - .with_block(evm_env.block_env) - .with_cfg(evm_env.cfg_env); - let mut inner = OpEvm::new(inner, inspector).with_precompiles(PrecompilesMap::from_static( - OpPrecompiles::new_with_spec(spec_id).precompiles(), - )); - inner.ctx_mut().cfg.tx_chain_id_check = true; - - let mut evm: OpFoundryEvm<'_, I> = OpFoundryEvm { inner }; - let networks = Evm::inspector(&evm).get_networks(); - networks.inject_precompiles(evm.precompiles_mut()); - evm + let mut op_evm = Self::default().create_evm_with_inspector(db, evm_env, inspector); + op_evm.cfg.tx_chain_id_check = true; + op_evm.inspector().get_networks().inject_precompiles(op_evm.precompiles_mut()); + op_evm } fn create_foundry_nested_evm<'db>( @@ -88,107 +54,7 @@ impl FoundryEvmFactory for OpEvmFactory { evm_env: EvmEnv, inspector: &'db mut dyn FoundryInspectorExt>, ) -> Box + 'db> { - Box::new(self.create_foundry_evm_with_inspector(db, evm_env, inspector).inner) - } -} - -impl<'db, I: FoundryInspectorExt>>> Evm - for OpFoundryEvm<'db, I> -{ - type Precompiles = PrecompilesMap; - type Inspector = I; - type DB = &'db mut dyn DatabaseExt; - type Error = EVMError; - type HaltReason = OpHaltReason; - type Spec = OpSpecId; - type Tx = OpTx; - type BlockEnv = BlockEnv; - - fn block(&self) -> &BlockEnv { - &self.inner.ctx_ref().block - } - - fn chain_id(&self) -> u64 { - self.inner.ctx_ref().cfg.chain_id - } - - fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) { - let (ctx, _, precompiles, _, inspector) = self.inner.all_inspector(); - (&ctx.journaled_state.database, inspector, precompiles) - } - - fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) { - let (ctx, _, precompiles, _, inspector) = self.inner.all_mut_inspector(); - (&mut ctx.journaled_state.database, inspector, precompiles) - } - - fn set_inspector_enabled(&mut self, _enabled: bool) { - unimplemented!("OpFoundryEvm is always inspecting") - } - - fn transact_raw( - &mut self, - tx: Self::Tx, - ) -> Result, Self::Error> { - self.inner.ctx().set_tx(tx); - - let mut handler = OpEvmHandler::::new(); - // Convert OpTransactionError to InvalidTransaction due to missing InvalidTxError impl - let result = handler.inspect_run(&mut self.inner).map_err(|e| match e { - EVMError::Transaction(tx_error) => EVMError::Transaction(match tx_error { - OpTransactionError::Base(invalid_transaction) => invalid_transaction, - OpTransactionError::DepositSystemTxPostRegolith => { - InvalidTransaction::Str("DepositSystemTxPostRegolith".into()) - } - OpTransactionError::HaltedDepositPostRegolith => { - InvalidTransaction::Str("HaltedDepositPostRegolith".into()) - } - OpTransactionError::MissingEnvelopedTx => { - InvalidTransaction::Str("MissingEnvelopedTx".into()) - } - }), - EVMError::Header(invalid_header) => EVMError::Header(invalid_header), - EVMError::Database(db_error) => EVMError::Database(db_error), - EVMError::Custom(custom_error) => EVMError::Custom(custom_error), - EVMError::CustomAny(custom_any_error) => EVMError::CustomAny(custom_any_error), - })?; - - Ok(ResultAndState::new(result, self.inner.ctx_ref().journaled_state.inner.state.clone())) - } - - fn transact_system_call( - &mut self, - _caller: Address, - _contract: Address, - _data: Bytes, - ) -> Result>, Self::Error> { - unimplemented!() - } - - fn finish(self) -> (Self::DB, EvmEnv) - where - Self: Sized, - { - let Context { block: block_env, cfg: cfg_env, journaled_state, .. } = self.inner.0.ctx; - (journaled_state.database, EvmEnv { block_env, cfg_env }) - } -} - -impl<'db, I: FoundryInspectorExt>>> Deref - for OpFoundryEvm<'db, I> -{ - type Target = OpContext<&'db mut dyn DatabaseExt>; - - fn deref(&self) -> &Self::Target { - &self.inner.0.ctx - } -} - -impl<'db, I: FoundryInspectorExt>>> DerefMut - for OpFoundryEvm<'db, I> -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner.0.ctx + Box::new(self.create_foundry_evm_with_inspector(db, evm_env, inspector).into_inner()) } } @@ -203,7 +69,7 @@ fn map_op_error(e: EVMError) -> EVMError>>> NestedEvm +impl<'db, I: FoundryInspectorExt>>> NestedEvm for OpRevmEvm<'db, I> { type Spec = OpSpecId; diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 3903ca6ff35bb..27d8e6a0ed588 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -11,7 +11,7 @@ use eyre::{ContextCompat, Result, eyre}; use foundry_common::{ TestFunctionExt, contracts::{ContractsByAddress, ContractsByArtifact}, - sh_println, + sh_eprintln, sh_println, }; use foundry_config::InvariantConfig; use foundry_evm_core::{ @@ -39,7 +39,7 @@ use revm::{context::Block, state::Account}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::{ - collections::{HashMap as Map, btree_map::Entry}, + collections::{HashMap as Map, HashSet, btree_map::Entry}, sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; @@ -153,6 +153,32 @@ fn rate_per_sec(total: f64, elapsed: Duration) -> f64 { if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 } } +/// Tracks invariant failure counts during a campaign. +#[derive(Debug, Default)] +struct InvariantFailureMetrics { + failures: u64, + unique_failures: HashSet, +} + +impl InvariantFailureMetrics { + /// Records a failure and emits a structured JSON `"failure"` event. + fn record_failure(&mut self, invariant_name: &str, target: &str, reason: &str) { + self.failures += 1; + self.unique_failures.insert(invariant_name.to_string()); + + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let event = json!({ + "timestamp": timestamp, + "event": "failure", + "invariant": invariant_name, + "target": target, + "reason": reason, + }); + let _ = sh_eprintln!("{}", serde_json::to_string(&event).unwrap_or_default()); + } +} + /// Builds the machine-readable invariant progress payload emitted during a /// campaign. /// @@ -165,12 +191,20 @@ fn build_invariant_progress_json( corpus_metrics: &M, optimization_best: Option, throughput: InvariantThroughputMetrics, + failure_metrics: &InvariantFailureMetrics, elapsed: Duration, ) -> serde_json::Value { + let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default(); + if let Some(obj) = metrics.as_object_mut() { + obj.insert("failures".to_string(), json!(failure_metrics.failures)); + obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len())); + } + let mut payload = json!({ "timestamp": timestamp_secs, + "event": "pulse", "invariant": invariant_name, - "metrics": corpus_metrics, + "metrics": metrics, "total_txs": throughput.total_txs, "total_gas": throughput.total_gas, "tx_per_sec": throughput.tx_per_sec(elapsed), @@ -434,6 +468,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { let mut last_metrics_report = Instant::now(); let campaign_start = Instant::now(); let mut throughput = InvariantThroughputMetrics::default(); + let mut failure_metrics = InvariantFailureMetrics::default(); let continue_campaign = |runs: u32| { if early_exit.should_stop() { return false; @@ -620,6 +655,18 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { } // If test cannot continue then stop current run and exit test suite. if !result.can_continue { + let reason = invariant_test + .test_data + .failures + .error + .as_ref() + .and_then(|e| e.revert_reason()) + .unwrap_or_default(); + failure_metrics.record_failure( + &invariant_contract.invariant_function.name, + invariant_contract.name, + &reason, + ); break 'stop; } @@ -650,13 +697,27 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { // Call `afterInvariant` only if it is declared and test didn't fail already. if invariant_contract.call_after_invariant && !invariant_test.has_errors() { - assert_after_invariant( + let success = assert_after_invariant( &invariant_contract, &mut invariant_test, ¤t_run, &self.config, ) .map_err(|_| eyre!("Failed to call afterInvariant"))?; + if !success { + let reason = invariant_test + .test_data + .failures + .error + .as_ref() + .and_then(|e| e.revert_reason()) + .unwrap_or_default(); + failure_metrics.record_failure( + &invariant_contract.invariant_function.name, + invariant_contract.name, + &reason, + ); + } } // End current invariant test run. @@ -689,6 +750,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { &corpus_manager.metrics, invariant_test.test_data.optimization_best_value, throughput, + &failure_metrics, campaign_start.elapsed(), ); let _ = sh_println!("{}", serde_json::to_string(&metrics)?); @@ -1284,6 +1346,7 @@ mod tests { &json!({ "corpus_count": 7 }), Some(I256::try_from(42).unwrap()), throughput, + &InvariantFailureMetrics::default(), Duration::from_secs(10), ); @@ -1308,6 +1371,7 @@ mod tests { &json!({ "corpus_count": 1 }), None, throughput, + &InvariantFailureMetrics::default(), Duration::ZERO, ); @@ -1315,4 +1379,45 @@ mod tests { assert_eq!(payload["gas_per_sec"], json!(0.0)); assert!(payload.get("optimization_best").is_none()); } + + #[test] + fn invariant_progress_json_includes_failure_counts() { + let mut failure_metrics = InvariantFailureMetrics::default(); + failure_metrics.record_failure("invariant_a", "TestContract", "revert"); + failure_metrics.record_failure("invariant_a", "TestContract", "revert"); + failure_metrics.record_failure("invariant_b", "TestContract", "assertion failed"); + + let payload = build_invariant_progress_json( + 789, + "invariant_a", + &json!({ "corpus_count": 5 }), + None, + InvariantThroughputMetrics::default(), + &failure_metrics, + Duration::from_secs(1), + ); + + assert_eq!(payload["metrics"]["failures"], json!(3)); + assert_eq!(payload["metrics"]["unique_failures"], json!(2)); + } + + #[test] + fn failure_metrics_tracks_total_and_unique_failures() { + let mut metrics = InvariantFailureMetrics::default(); + metrics.record_failure("invariant_a", "TestContract", "revert"); + metrics.record_failure("invariant_a", "TestContract", "revert"); + metrics.record_failure("invariant_b", "TestContract", "assertion failed"); + + assert_eq!(metrics.failures, 3); + assert_eq!(metrics.unique_failures.len(), 2); + assert!(metrics.unique_failures.contains("invariant_a")); + assert!(metrics.unique_failures.contains("invariant_b")); + } + + #[test] + fn failure_metrics_default_is_zero() { + let metrics = InvariantFailureMetrics::default(); + assert_eq!(metrics.failures, 0); + assert!(metrics.unique_failures.is_empty()); + } } diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 2d712dcf5a48d..3ffa92b459905 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -207,9 +207,9 @@ impl Executor { self.evm_env.cfg_env.spec } - /// Sets the EVM spec. - pub const fn set_spec_id(&mut self, spec_id: SpecFor) { - self.evm_env.cfg_env.spec = spec_id; + /// Sets the EVM spec and updates spec-dependent gas parameters. + pub fn set_spec_id(&mut self, spec_id: SpecFor) { + self.evm_env.cfg_env.set_spec_and_mainnet_gas_params(spec_id); } /// Returns the gas limit for calls and deployments. @@ -1316,6 +1316,7 @@ impl EarlyExit { mod tests { use super::*; use foundry_evm_core::constants::MAGIC_SKIP; + use revm::{context::Cfg, primitives::hardfork::SpecId}; #[test] fn cheatcode_skip_payload_is_classified_as_skip() { @@ -1352,4 +1353,30 @@ mod tests { let err = raw.into_evm_error(None); assert!(matches!(err, EvmError::Execution(_))); } + + #[test] + fn set_spec_id_updates_spec_dependent_cfg_state() { + let backend = Backend::::spawn(None).unwrap(); + let mut executor = ExecutorBuilder::default().build( + EvmEnvFor::::default(), + TxEnvFor::::default(), + backend, + ); + + executor.evm_env_mut().cfg_env.set_spec_and_mainnet_gas_params(SpecId::HOMESTEAD); + assert_eq!( + executor.evm_env().cfg_env.gas_params(), + &revm::context_interface::cfg::GasParams::new_spec(SpecId::HOMESTEAD), + ); + assert!(!executor.evm_env().cfg_env.is_amsterdam_eip8037_enabled()); + + executor.set_spec_id(SpecId::AMSTERDAM); + + assert_eq!(executor.spec_id(), SpecId::AMSTERDAM); + assert_eq!( + executor.evm_env().cfg_env.gas_params(), + &revm::context_interface::cfg::GasParams::new_spec(SpecId::AMSTERDAM), + ); + assert!(executor.evm_env().cfg_env.is_amsterdam_eip8037_enabled()); + } } diff --git a/crates/evm/evm/src/inspectors/logs.rs b/crates/evm/evm/src/inspectors/logs.rs index 26acbe56a97cb..c591b9426bf9e 100644 --- a/crates/evm/evm/src/inspectors/logs.rs +++ b/crates/evm/evm/src/inspectors/logs.rs @@ -50,7 +50,9 @@ impl LogCollector { fn hardhat_log(&mut self, data: &[u8]) -> alloy_sol_types::Result<()> { let decoded = console::hh::ConsoleCalls::abi_decode(data)?; - self.push_msg(&decoded.fmt(Default::default())); + for line in decoded.fmt(Default::default()).lines() { + self.push_msg(line); + } Ok(()) } diff --git a/crates/evm/fuzz/src/invariant/mod.rs b/crates/evm/fuzz/src/invariant/mod.rs index f811cf6728b14..d23066061b6e0 100644 --- a/crates/evm/fuzz/src/invariant/mod.rs +++ b/crates/evm/fuzz/src/invariant/mod.rs @@ -264,6 +264,8 @@ impl TargetedContract { pub struct InvariantContract<'a> { /// Address of the test contract. pub address: Address, + /// Name of the test contract. + pub name: &'a str, /// Invariant function present in the test contract. pub invariant_function: &'a Function, /// If true, `afterInvariant` function is called after each invariant run. @@ -276,11 +278,12 @@ impl<'a> InvariantContract<'a> { /// Creates a new invariant contract. pub const fn new( address: Address, + name: &'a str, invariant_function: &'a Function, call_after_invariant: bool, abi: &'a JsonAbi, ) -> Self { - Self { address, invariant_function, call_after_invariant, abi } + Self { address, name, invariant_function, call_after_invariant, abi } } /// Returns true if this is an optimization mode invariant (returns int256). diff --git a/crates/evm/networks/Cargo.toml b/crates/evm/networks/Cargo.toml index d3e9982fda1c0..a63ed34ba61cf 100644 --- a/crates/evm/networks/Cargo.toml +++ b/crates/evm/networks/Cargo.toml @@ -40,4 +40,7 @@ revm = { workspace = true, features = [ ] } clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } -serde.workspace = true \ No newline at end of file +serde.workspace = true + +[dev-dependencies] +serde_json.workspace = true \ No newline at end of file diff --git a/crates/evm/networks/src/lib.rs b/crates/evm/networks/src/lib.rs index 7d5743b73eb68..4672deff353d0 100644 --- a/crates/evm/networks/src/lib.rs +++ b/crates/evm/networks/src/lib.rs @@ -12,7 +12,7 @@ use alloy_chains::{ use alloy_eips::eip1559::BaseFeeParams; use alloy_evm::precompiles::PrecompilesMap; use alloy_op_hardforks::{OpChainHardforks, OpHardforks}; -use alloy_primitives::{Address, map::AddressHashMap}; +use alloy_primitives::{Address, ChainId, map::AddressHashMap}; use clap::Parser; use foundry_evm_hardforks::FoundryHardfork; use serde::{Deserialize, Serialize}; @@ -20,19 +20,61 @@ use std::collections::BTreeMap; pub mod celo; -#[derive(Clone, Debug, Default, Parser, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] +#[serde(rename_all = "lowercase")] +#[clap(rename_all = "lowercase")] +pub enum NetworkVariant { + #[default] + Ethereum, + Optimism, + Tempo, +} + +impl NetworkVariant { + pub const fn name(&self) -> &'static str { + match self { + Self::Ethereum => "ethereum", + Self::Optimism => "optimism", + Self::Tempo => "tempo", + } + } +} + +impl std::fmt::Display for NetworkVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl From for NetworkVariant { + fn from(chain_id: ChainId) -> Self { + let chain = Chain::from_id(chain_id); + if chain.is_tempo() { + Self::Tempo + } else if chain.is_optimism() { + Self::Optimism + } else { + Self::Ethereum + } + } +} + +#[derive(Clone, Debug, Default, Parser, Serialize, Deserialize, Copy, PartialEq, Eq)] pub struct NetworkConfigs { - /// Enable Optimism network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["celo", "tempo"])] + /// Enable a specific network family. + #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "optimism", "tempo"])] + #[serde(skip_serializing_if = "Option::is_none")] + network: Option, + /// Enable Celo network features. + #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "optimism", "tempo"])] + celo: bool, + /// Enable Optimism network features (deprecated: use --network optimism). + #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])] // Skipped from configs (forge) as there is no feature to be added yet. #[serde(skip)] optimism: bool, - /// Enable Celo network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["optimism", "tempo"])] - #[serde(default)] - celo: bool, - /// Enable Tempo network features. - #[arg(help_heading = "Networks", long, conflicts_with_all = ["optimism", "celo"])] + /// Enable Tempo network features (deprecated: use --network tempo). + #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "optimism"])] #[serde(default)] tempo: bool, /// Whether to bypass prevrandao. @@ -43,7 +85,7 @@ pub struct NetworkConfigs { impl NetworkConfigs { pub fn with_optimism() -> Self { - Self { optimism: true, ..Default::default() } + Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() } } pub fn with_celo() -> Self { @@ -51,15 +93,38 @@ impl NetworkConfigs { } pub fn with_tempo() -> Self { - Self { tempo: true, ..Default::default() } + Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() } + } + + pub fn is_optimism(&self) -> bool { + matches!(self.resolved_network(), Some(NetworkVariant::Optimism)) } - pub const fn is_optimism(&self) -> bool { - self.optimism + pub fn is_tempo(&self) -> bool { + matches!(self.resolved_network(), Some(NetworkVariant::Tempo)) } - pub const fn is_tempo(&self) -> bool { - self.tempo + pub const fn is_celo(&self) -> bool { + self.celo + } + + /// Returns the resolved network variant, folding legacy flags. + fn resolved_network(&self) -> Option { + self.network.or(if self.optimism { + Some(NetworkVariant::Optimism) + } else if self.tempo { + Some(NetworkVariant::Tempo) + } else { + None + }) + } + + /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum. + pub fn active_network_name(&self) -> Option<&'static str> { + self.resolved_network().and_then(|n| match n { + NetworkVariant::Ethereum => None, + _ => Some(n.name()), + }) } /// Returns the base fee parameters for the configured network. @@ -89,36 +154,23 @@ impl NetworkConfigs { self.bypass_prevrandao } - pub const fn is_celo(&self) -> bool { - self.celo - } - - /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum. - pub const fn active_network_name(&self) -> Option<&'static str> { - if self.tempo { - Some("tempo") - } else if self.optimism { - Some("optimism") - } else if self.celo { - Some("celo") - } else { - None - } - } - - pub fn with_chain_id(mut self, chain_id: u64) -> Self { - // Only infer network if no explicit network is already set - if !self.celo && !self.tempo && !self.optimism { - let chain = Chain::from_id(chain_id); - if matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) { - self.celo = true; - } else if chain.is_tempo() { - self.tempo = true; + pub fn with_chain_id(self, chain_id: u64) -> Self { + let chain = Chain::from_id(chain_id); + if self.resolved_network().is_none() { + if chain.is_tempo() { + Self::with_tempo() } else if chain.is_optimism() { - self.optimism = true; + Self::with_optimism() + } else { + self } + } else if !self.celo + && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) + { + Self::with_celo() + } else { + self } - self } /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an @@ -172,3 +224,100 @@ impl NetworkConfigs { precompiles } } + +#[cfg(test)] +mod tests { + use super::*; + + // --- Equivalence: new flag == legacy flag --- + + #[test] + fn new_tempo_flag_equivalent_to_legacy() { + let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() }; + let via_old = NetworkConfigs { tempo: true, ..Default::default() }; + assert_eq!(via_new.is_tempo(), via_old.is_tempo()); + assert_eq!(via_new.is_optimism(), via_old.is_optimism()); + assert_eq!(via_new.active_network_name(), via_old.active_network_name()); + } + + #[test] + fn new_optimism_flag_equivalent_to_legacy() { + let via_new = + NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() }; + let via_old = NetworkConfigs { optimism: true, ..Default::default() }; + assert_eq!(via_new.is_optimism(), via_old.is_optimism()); + assert_eq!(via_new.is_tempo(), via_old.is_tempo()); + assert_eq!(via_new.active_network_name(), via_old.active_network_name()); + } + + // --- resolved() / active_network_name --- + + #[test] + fn active_network_name_tempo() { + let cfg = NetworkConfigs::with_tempo(); + assert_eq!(cfg.active_network_name(), Some("tempo")); + } + + #[test] + fn active_network_name_optimism() { + let cfg = NetworkConfigs::with_optimism(); + assert_eq!(cfg.active_network_name(), Some("optimism")); + } + + #[test] + fn active_network_name_default_is_none() { + assert_eq!(NetworkConfigs::default().active_network_name(), None); + } + + // --- new flag takes precedence over legacy flag --- + + #[test] + fn new_flag_wins_over_legacy_when_both_set() { + // --network optimism --tempo: network field wins + let cfg = NetworkConfigs { + network: Some(NetworkVariant::Optimism), + tempo: true, + ..Default::default() + }; + assert!(cfg.is_optimism()); + assert!(!cfg.is_tempo()); + } + + // --- Serde round-trip --- + + #[test] + fn serde_roundtrip_tempo() { + let original = NetworkConfigs::with_tempo(); + let json = serde_json::to_string(&original).unwrap(); + let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); + assert!(restored.is_tempo()); + assert!(!restored.is_optimism()); + } + + #[test] + fn serde_roundtrip_optimism() { + let original = NetworkConfigs::with_optimism(); + let json = serde_json::to_string(&original).unwrap(); + let restored: NetworkConfigs = serde_json::from_str(&json).unwrap(); + assert!(restored.is_optimism()); + assert!(!restored.is_tempo()); + } + + #[test] + fn serde_legacy_tempo_bool_deserialized() { + // Old foundry.toml format: `tempo = true` + let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#; + let cfg: NetworkConfigs = serde_json::from_str(json).unwrap(); + assert!(cfg.is_tempo()); + } + + #[test] + fn serde_new_network_field_deserialized() { + let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#; + let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap(); + assert!(cfg_tempo.is_tempo()); + let json_optimism = r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#; + let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap(); + assert!(cfg_optimism.is_optimism()); + } +} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 9a7a237855b97..ecdc13ded67cf 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -51,7 +51,7 @@ forge-script.workspace = true forge-sol-macro-gen.workspace = true foundry-cli.workspace = true foundry-debugger.workspace = true -foundry-wallets.workspace = true +foundry-wallets = { workspace = true, features = ["browser", "tempo"] } alloy-chains.workspace = true alloy-consensus.workspace = true diff --git a/crates/forge/assets/tempo/workflowTemplate.yml b/crates/forge/assets/tempo/workflowTemplate.yml index f2294304b402a..1eb2d2bcf9af1 100644 --- a/crates/forge/assets/tempo/workflowTemplate.yml +++ b/crates/forge/assets/tempo/workflowTemplate.yml @@ -23,7 +23,6 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - network: tempo - name: Show Forge version run: forge --version diff --git a/crates/forge/src/cmd/clone.rs b/crates/forge/src/cmd/clone.rs index 404702dd659eb..9992bce209902 100644 --- a/crates/forge/src/cmd/clone.rs +++ b/crates/forge/src/cmd/clone.rs @@ -147,9 +147,12 @@ impl CloneArgs { // step 1. get the metadata from client based on source type let (meta, explorer_name, sourcify_client) = match source { SourceExplorer::Etherscan => { - let etherscan_api_key = - config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, etherscan_api_key.clone())?; + let client = config + .get_etherscan_config_with_chain(Some(chain))? + .ok_or_else(|| { + eyre::eyre!("No Etherscan API key configured for chain {chain}") + })? + .into_client()?; sh_println!("Downloading the source code of {address} from Etherscan...")?; let meta = Self::collect_metadata_from_client(address, &client).await?; (meta, "Etherscan", None) @@ -177,13 +180,15 @@ impl CloneArgs { match source { SourceExplorer::Etherscan => { - let etherscan_api_key = - config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, etherscan_api_key.clone())?; - if etherscan_api_key.is_empty() { + let etherscan_config = + config.get_etherscan_config_with_chain(Some(chain))?.ok_or_else(|| { + eyre::eyre!("No Etherscan API key configured for chain {chain}") + })?; + if etherscan_config.key.is_empty() { sh_warn!("Waiting for 5 seconds to avoid rate limit...")?; tokio::time::sleep(Duration::from_secs(5)).await; } + let client = etherscan_config.into_client()?; Self::collect_compilation_metadata(&meta, chain, address, &root, &client).await?; } SourceExplorer::Sourcify => { diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 122020571ad24..765bb64f95fdd 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -9,7 +9,7 @@ use alloy_provider::{PendingTransactionError, Provider, ProviderBuilder as Alloy use alloy_signer::{Signature, Signer}; use alloy_transport::TransportError; use clap::{Parser, ValueHint}; -use eyre::{Context, Result}; +use eyre::{Context, ContextCompat, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, @@ -21,6 +21,7 @@ use foundry_common::{ fmt::parse_tokens, provider::ProviderBuilder, shell, + tempo::TEMPO_BROWSER_GAS_BUFFER, }; use foundry_compilers::{ ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, @@ -33,10 +34,12 @@ use foundry_config::{ }, merge_impl_figment_convert, }; -use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; +use foundry_wallets::{ + BrowserWalletOpts, TempoAccessKeyConfig, WalletSigner, wallet_browser::signer::BrowserSigner, +}; use serde_json::json; use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; -use tempo_alloy::TempoNetwork; +use tempo_alloy::{TempoNetwork, contracts::precompiles::DEFAULT_FEE_TOKEN}; merge_impl_figment_convert!(CreateArgs, build, eth); @@ -101,14 +104,29 @@ pub struct CreateArgs { #[command(flatten)] retry: RetryArgs, + + /// Browser wallet options + #[command(flatten)] + browser: BrowserWalletOpts, } impl CreateArgs { /// Executes the command to create a contract - pub async fn run(self) -> Result<()> { + pub async fn run(mut self) -> Result<()> { let (signer, tempo_access_key) = self.eth.wallet.maybe_signer().await?; - if tempo_access_key.is_some() || self.tx.tempo.is_tempo() { + // Resolve chain early so we can dispatch to the correct network type. + if self.chain_id().is_none() { + let config = self.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let chain_id = provider.get_chain_id().await?; + self.eth.etherscan.chain = Some(chain_id.into()); + } + + if tempo_access_key.is_some() + || self.tx.tempo.is_tempo() + || self.chain_id().is_some_and(|c| c.is_tempo()) + { self.run_generic::(signer, tempo_access_key).await } else { self.run_generic::(signer, None).await @@ -185,17 +203,29 @@ impl CreateArgs { self.tx.tempo.key_id = Some(ak.key_address); } - // respect chain, if set explicitly via cmd args - let chain_id = if let Some(chain_id) = self.chain_id() { - chain_id - } else { - provider.get_chain_id().await? - }; - // Whether to broadcast the transaction or not let dry_run = !self.broadcast; - if self.unlocked { + // Launch browser signer if `--browser` flag is set + let browser = self.browser.run::().await?; + + if let Some(browser) = browser { + // Deploy with browser wallet + let deployer_address = browser.address(); + self.deploy( + abi, + bin, + params, + provider, + deployer_address, + config.transaction_timeout, + id, + dry_run, + None, + Some(browser), + ) + .await + } else if self.unlocked { // Deploy with unlocked account let sender = self.eth.wallet.from.expect("required"); self.deploy( @@ -203,12 +233,12 @@ impl CreateArgs { bin, params, provider, - chain_id, sender, config.transaction_timeout, id, dry_run, None, + None, ) .await } else if let Some(ak) = access_key { @@ -223,12 +253,12 @@ impl CreateArgs { bin, params, provider, - chain_id, deployer_address, config.transaction_timeout, id, dry_run, Some((signer, ak)), + None, ) .await } else { @@ -246,20 +276,20 @@ impl CreateArgs { bin, params, provider, - chain_id, deployer, config.transaction_timeout, id, dry_run, None, + None, ) .await } } - /// Returns the provided chain id, if any. - fn chain_id(&self) -> Option { - self.eth.etherscan.chain.map(|chain| chain.id()) + /// Returns the resolved chain, if any. + const fn chain_id(&self) -> Option { + self.eth.etherscan.chain } /// Ensures the verify command can be executed. @@ -271,7 +301,6 @@ impl CreateArgs { async fn verify_preflight_check( &self, constructor_args: Option, - chain: u64, id: &ArtifactId, ) -> Result<()> { // NOTE: this does not represent the same `VerifyArgs` that would be sent after deployment, @@ -287,7 +316,7 @@ impl CreateArgs { num_of_optimizations: None, etherscan: EtherscanOpts { key: self.eth.etherscan.key.clone(), - chain: Some(chain.into()), + chain: self.chain_id(), }, rpc: Default::default(), flatten: false, @@ -311,7 +340,7 @@ impl CreateArgs { // ETHERSCAN_API_KEY value set. let config = verify.load_config()?; verify.etherscan.key = - config.get_etherscan_config_with_chain(Some(chain.into()))?.map(|c| c.key); + config.get_etherscan_config_with_chain(self.chain_id())?.map(|c| c.key); let context = verify.resolve_context().await?; @@ -327,17 +356,19 @@ impl CreateArgs { bin: BytecodeObject, args: Vec, provider: P, - chain: u64, deployer_address: Address, timeout: u64, id: ArtifactId, dry_run: bool, tempo_keychain: Option<(WalletSigner, TempoAccessKeyConfig)>, + browser_signer: Option>, ) -> Result<()> where N::TransactionRequest: FoundryTransactionBuilder + serde::Serialize, N::ReceiptResponse: serde::Serialize, { + let chain = self.chain_id().context("chain ID not resolved")?; + let bin = bin.into_bytes().unwrap_or_default(); if bin.is_empty() { eyre::bail!("no bytecode found in bin object for {}", self.contract.name) @@ -349,22 +380,31 @@ impl CreateArgs { let is_args_empty = args.is_empty(); let mut deployer = - factory.deploy_tokens(args.clone(), self.tx.tempo.fee_token).context("failed to deploy contract").map_err(|e| { + factory.deploy_tokens(args.clone()).context("failed to deploy contract").map_err(|e| { if is_args_empty { e.wrap_err("no arguments provided for contract constructor; consider --constructor-args or --constructor-args-path") } else { e } })?; - let is_legacy = self.tx.legacy || Chain::from(chain).is_legacy(); + let is_legacy = self.tx.legacy || chain.is_legacy(); deployer.tx.set_from(deployer_address); - deployer.tx.set_chain_id(chain); + deployer.tx.set_chain_id(chain.id()); // `to` field must be set explicitly, cannot be None. if deployer.tx.to().is_none() { deployer.tx.set_create(); } + // If Tempo chain fee token must be set + if chain.is_tempo() { + if let Some(fee_token) = self.tx.tempo.fee_token { + deployer.tx.set_fee_token(fee_token); + } else { + deployer.tx.set_fee_token(DEFAULT_FEE_TOKEN); + } + } + // Apply user-provided gas, fee, nonce, and Tempo options. self.tx.apply::(&mut deployer.tx, is_legacy); @@ -394,7 +434,16 @@ impl CreateArgs { } if self.tx.gas_limit.is_none() { - deployer.tx.set_gas_limit(provider.estimate_gas(deployer.tx.clone()).await?); + let mut estimated = provider.estimate_gas(deployer.tx.clone()).await?; + + // Browser wallets may sign with P256/WebAuthn instead of secp256k1, which + // costs more gas for signature verification on Tempo chains. Add a + // conservative buffer since we can't determine the signature type beforehand. + if browser_signer.is_some() && chain.is_tempo() { + estimated += TEMPO_BROWSER_GAS_BUFFER; + } + + deployer.tx.set_gas_limit(estimated); } if is_legacy { @@ -422,7 +471,7 @@ impl CreateArgs { constructor_args = Some(hex::encode(encoded_args)); } - self.verify_preflight_check(constructor_args.clone(), chain, &id).await?; + self.verify_preflight_check(constructor_args.clone(), &id).await?; } if dry_run { @@ -452,7 +501,31 @@ impl CreateArgs { } // Deploy the actual contract - let (deployed_contract, receipt) = if let Some((signer, ak)) = tempo_keychain { + let (deployed_contract, receipt) = if let Some(browser) = browser_signer { + // Browser wallet signs and sends the transaction + let tx_hash = browser.send_transaction_via_browser(deployer.tx).await?; + + // Wait for the transaction to be confirmed, then fetch the receipt. + provider + .watch_pending_transaction(alloy_provider::PendingTransactionConfig::new(tx_hash)) + .await? + .await?; + + let receipt = provider + .get_transaction_receipt(tx_hash) + .await? + .ok_or_else(|| eyre::eyre!("could not get transaction receipt for {tx_hash}"))?; + + if !receipt.status() { + eyre::bail!("deployment transaction failed (receipt status 0): {tx_hash}"); + } + + let address = receipt + .contract_address() + .ok_or_else(|| eyre::eyre!("contract was not deployed"))?; + + (address, receipt) + } else if let Some((signer, ak)) = tempo_keychain { // Tempo keychain mode: sign with access key provisioning and send raw let raw_tx = deployer .tx @@ -518,7 +591,7 @@ impl CreateArgs { no_auto_detect: false, use_solc: None, num_of_optimizations, - etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain.into()) }, + etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain) }, rpc: Default::default(), flatten: false, force: false, @@ -681,7 +754,6 @@ impl + Clone> DeploymentTxFactory { pub fn deploy_tokens( self, params: Vec, - fee_token: Option
, ) -> Result, ContractDeploymentError> where N::TransactionRequest: FoundryTransactionBuilder, @@ -703,9 +775,6 @@ impl + Clone> DeploymentTxFactory { // create the tx object. Since we're deploying a contract, `to` is `None` let mut tx = N::TransactionRequest::default(); tx.set_input(data); - if let Some(fee_token) = fee_token { - tx.set_fee_token(fee_token); - } Ok(Deployer { client: self.client.clone(), tx, confs: 1, timeout: self.timeout }) } } @@ -763,7 +832,7 @@ mod tests { "--chain-id", "9999", ]); - assert_eq!(args.chain_id(), Some(9999)); + assert_eq!(args.chain_id().map(|c| c.id()), Some(9999)); } #[test] diff --git a/crates/forge/src/cmd/init.rs b/crates/forge/src/cmd/init.rs index a63b916d24788..ce94f619081f2 100644 --- a/crates/forge/src/cmd/init.rs +++ b/crates/forge/src/cmd/init.rs @@ -1,11 +1,11 @@ use super::install::DependencyInstallOpts; use clap::{Parser, ValueHint}; use eyre::Result; -use foundry_cli::{opts::NetworkVariant, utils::Git}; +use foundry_cli::utils::Git; use foundry_common::fs; use foundry_compilers::artifacts::remappings::Remapping; use foundry_config::Config; -use foundry_evm_networks::NetworkConfigs; +use foundry_evm_networks::{NetworkConfigs, NetworkVariant}; use std::path::{Path, PathBuf}; use yansi::Paint; diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index d098f578c94ad..d924c416759a2 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -759,8 +759,13 @@ impl<'a, FEN: FoundryEvmNetwork> FunctionRunner<'a, FEN> { identified_contracts, &self.cr.mcr.known_contracts, ); - let invariant_contract = - InvariantContract::new(self.address, func, call_after_invariant, &self.cr.contract.abi); + let invariant_contract = InvariantContract::new( + self.address, + self.cr.name, + func, + call_after_invariant, + &self.cr.contract.abi, + ); let show_solidity = invariant_config.show_solidity; // Compute current invariant settings for failure validation. diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index cd7c2af4eb02f..d3f1503faf569 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -962,6 +962,25 @@ forgetest!(can_execute_test_and_script_with_default_tempo_config, |prj, cmd| { .assert_success(); }); +// checks that `forge test --tempo` and `forge test -n tempo` are equivalent +forgetest!(network_flag_tempo_equivalent_to_legacy_tempo, |prj, cmd| { + prj.wipe(); + cmd.args(["init", "--network", "tempo"]).arg(prj.root()).assert_success(); + + // --network tempo (new flag) + cmd.forge_fuse() + .args(["test", "--network", "tempo"]) + .arg("--root") + .arg(prj.root()) + .assert_success(); + + // -n tempo (short form) + cmd.forge_fuse().args(["test", "-n", "tempo"]).arg("--root").arg(prj.root()).assert_success(); + + // --tempo (legacy flag) + cmd.forge_fuse().args(["test", "--tempo"]).arg("--root").arg(prj.root()).assert_success(); +}); + // checks that clone works with raw src containing `node_modules` // forgetest!(flaky_can_clone_with_node_modules, |prj, cmd| { @@ -1523,6 +1542,51 @@ Warning: SPDX license identifier not provided in source file. Before publishing, [SOLC_VERSION] [ELAPSED] Compiler run successful! +"#]]); +}); + +// test that `forge build` ignores error codes only from matching path prefixes +forgetest!(can_compile_without_warnings_ignored_error_codes_from, |prj, cmd| { + let contract = r" +pragma solidity *; +contract A {} + "; + prj.add_raw_source("A", contract); + prj.add_raw_test("A", contract); + + // suppressed for both src and test + prj.update_config(|config| { + config.ignored_error_codes = vec![]; + config.ignored_error_codes_from = vec![ + (std::path::PathBuf::from("src"), vec![SolidityErrorCode::SpdxLicenseNotProvided]), + (std::path::PathBuf::from("test"), vec![SolidityErrorCode::SpdxLicenseNotProvided]), + ]; + }); + + cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); + + // suppressed only for test, src warning still shows + prj.update_config(|config| { + config.ignored_error_codes_from = vec![( + std::path::PathBuf::from("test"), + vec![SolidityErrorCode::SpdxLicenseNotProvided], + )]; + }); + + cmd.forge_fuse().args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +[FILE] + + "#]]); }); @@ -2929,7 +2993,7 @@ Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] +=====================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 132471 | 396 | | | | | +| 132459 | 396 | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |----------------------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -2952,7 +3016,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/FallbackWithCalldataTest.sol:CounterWithFallback", "deployment": { - "gas": 132471, + "gas": 132459, "size": 396 }, "functions": { diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 0100358985693..dca88ad1c2f63 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -69,6 +69,7 @@ ignored_error_codes = [ "transfer-deprecated", "natspec-memory-safe-assembly-deprecated", ] +ignored_error_codes_from = [] ignored_warnings_from = [] deny = "never" test_failures_file = "cache/test-failures" @@ -344,6 +345,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6".to_string(), ], ignored_error_codes: vec![], + ignored_error_codes_from: vec![], ignored_file_paths: vec![], deny: foundry_config::DenyLevel::Never, deny_warnings: false, @@ -1253,6 +1255,7 @@ forgetest_init!(test_default_config, |prj, cmd| { "transfer-deprecated", "natspec-memory-safe-assembly-deprecated" ], + "ignored_error_codes_from": [], "ignored_warnings_from": [], "deny": "never", "match_test": null, diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index a9e01aa248783..242a0ebb4267f 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -136,6 +136,21 @@ contract FailingScript is Script { } "#; +static OUT_OF_GAS_SCRIPT: &str = r#" +import "forge-std/Script.sol"; + +contract OutOfGasScript is Script { + function run() external { + uint256 i; + while (true) { + unchecked { + ++i; + } + } + } +} +"#; + // Tests that execution throws upon encountering a revert in the script. forgetest_async!(assert_exit_code_error_on_failure_script, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); @@ -167,6 +182,32 @@ Error: script failed: failed "#]]); }); +// Tests that script failures surface halt reasons for empty revert data. +forgetest_async!(assert_exit_code_error_on_out_of_gas_script, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + let script = prj.add_source("OutOfGasScript", OUT_OF_GAS_SCRIPT); + + cmd.arg("script").arg(script); + + cmd.assert_failure().stderr_eq(str![[r#" +Error: script failed: EvmError: OutOfGas + +"#]]); +}); + +// Tests that --json script failures also surface halt reasons for empty revert data. +forgetest_async!(assert_exit_code_error_on_out_of_gas_script_with_json, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + let script = prj.add_source("OutOfGasScript", OUT_OF_GAS_SCRIPT); + + cmd.arg("script").arg(script).arg("--json"); + + cmd.assert_failure().stderr_eq(str![[r#" +Error: script failed: EvmError: OutOfGas + +"#]]); +}); + // Tests that the manually specified gas limit is used when using the --unlocked option forgetest_async!(can_execute_script_command_with_manual_gas_limit_unlocked, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); diff --git a/crates/lint/README.md b/crates/lint/README.md index 3f58ed6a0ec0f..9e3f394eba832 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -13,7 +13,11 @@ It helps enforce best practices and improve code quality within Foundry projects - `erc20-unchecked-transfer`: ERC20 `transfer` and `transferFrom` calls should check the return value. - **Medium Severity:** - `divide-before-multiply`: Warns against performing division before multiplication in the same expression, which can cause precision loss. + - `incorrect-erc20-interface`: Flags ERC20 interfaces and implementations with non-compliant function signatures. + - `incorrect-erc721-interface`: Flags ERC721 interfaces and implementations with non-compliant function signatures. - `unsafe-typecast`: Typecasts that can truncate values should be checked. +- **Low Severity:** + - `block-timestamp`: Warns when `block.timestamp` is used in a comparison, as it may be manipulated by validators. - **Informational / Style Guide:** - `pascal-case-struct`: Flags for struct names not adhering to `PascalCase`. - `mixed-case-function`: Flags for function names not adhering to `mixedCase`. @@ -75,4 +79,3 @@ Guidelines for contributing to `forge lint`: ### Developing a New Lint Rule Check the [dev docs](../../docs/dev/lintrules.md) for a full implementation guide. - diff --git a/crates/lint/src/linter/late.rs b/crates/lint/src/linter/late.rs index 73620a4bc331b..f7e97d00e7612 100644 --- a/crates/lint/src/linter/late.rs +++ b/crates/lint/src/linter/late.rs @@ -17,28 +17,28 @@ pub trait LateLintPass<'hir>: Send + Sync { &mut self, _ctx: &LintContext, _hir: &'hir hir::Hir<'hir>, - _id: &'hir hir::ItemId, + _id: hir::ItemId, ) { } fn check_nested_contract( &mut self, _ctx: &LintContext, _hir: &'hir hir::Hir<'hir>, - _id: &'hir hir::ContractId, + _id: hir::ContractId, ) { } fn check_nested_function( &mut self, _ctx: &LintContext, _hir: &'hir hir::Hir<'hir>, - _id: &'hir hir::FunctionId, + _id: hir::FunctionId, ) { } fn check_nested_var( &mut self, _ctx: &LintContext, _hir: &'hir hir::Hir<'hir>, - _id: &'hir hir::VariableId, + _id: hir::VariableId, ) { } fn check_item( @@ -143,6 +143,34 @@ where self.walk_nested_source(id) } + fn visit_nested_item(&mut self, id: hir::ItemId) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_nested_item(self.ctx, self.hir, id); + } + self.walk_nested_item(id) + } + + fn visit_nested_contract(&mut self, id: hir::ContractId) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_nested_contract(self.ctx, self.hir, id); + } + self.walk_nested_contract(id) + } + + fn visit_nested_function(&mut self, id: hir::FunctionId) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_nested_function(self.ctx, self.hir, id); + } + self.walk_nested_function(id) + } + + fn visit_nested_var(&mut self, id: hir::VariableId) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_nested_var(self.ctx, self.hir, id); + } + self.walk_nested_var(id) + } + fn visit_contract( &mut self, contract: &'hir hir::Contract<'hir>, @@ -160,6 +188,16 @@ where self.walk_function(func) } + fn visit_modifier( + &mut self, + modifier: &'hir hir::Modifier<'hir>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_modifier(self.ctx, self.hir, modifier); + } + self.walk_modifier(modifier) + } + fn visit_item(&mut self, item: hir::Item<'hir, 'hir>) -> ControlFlow { for pass in self.passes.iter_mut() { pass.check_item(self.ctx, self.hir, item); @@ -181,6 +219,16 @@ where self.walk_expr(expr) } + fn visit_call_args( + &mut self, + args: &'hir hir::CallArgs<'hir>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_call_args(self.ctx, self.hir, args); + } + self.walk_call_args(args) + } + fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow { for pass in self.passes.iter_mut() { pass.check_stmt(self.ctx, self.hir, stmt); @@ -195,3 +243,163 @@ where self.walk_ty(ty) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::linter::LinterConfig; + use foundry_common::comments::inline_config::InlineConfig; + use foundry_config::lint::LintSpecificConfig; + use solar::{ + interface::{Session, source_map::FileName}, + sema::Compiler, + }; + use std::sync::{Arc, Mutex}; + + #[derive(Debug, Default)] + struct HookCounts { + nested_item: usize, + nested_contract: usize, + nested_function: usize, + nested_var: usize, + modifier: usize, + call_args: usize, + } + + struct RecordingPass { + counts: Arc>, + } + + impl RecordingPass { + fn record(&self, update: impl FnOnce(&mut HookCounts)) { + update(&mut self.counts.lock().unwrap()); + } + } + + impl<'hir> LateLintPass<'hir> for RecordingPass { + fn check_nested_item( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _id: hir::ItemId, + ) { + self.record(|counts| counts.nested_item += 1); + } + + fn check_nested_contract( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _id: hir::ContractId, + ) { + self.record(|counts| counts.nested_contract += 1); + } + + fn check_nested_function( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _id: hir::FunctionId, + ) { + self.record(|counts| counts.nested_function += 1); + } + + fn check_nested_var( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _id: hir::VariableId, + ) { + self.record(|counts| counts.nested_var += 1); + } + + fn check_modifier( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _modifier: &'hir hir::Modifier<'hir>, + ) { + self.record(|counts| counts.modifier += 1); + } + + fn check_call_args( + &mut self, + _ctx: &LintContext, + _hir: &'hir hir::Hir<'hir>, + _args: &'hir hir::CallArgs<'hir>, + ) { + self.record(|counts| counts.call_args += 1); + } + } + + #[test] + fn calls_hooks_for_nested_items_modifiers_and_call_args() { + let counts = Arc::new(Mutex::new(HookCounts::default())); + let inline = InlineConfig::default(); + let lint_specific = LintSpecificConfig::default(); + let source = r#" + pragma solidity ^0.8.20; + + contract Base { + function hook(uint256 value) internal pure returns (uint256) { + return value; + } + } + + contract Test is Base { + uint256 stored; + + modifier gated(uint256 amount) { + _; + } + + function run(uint256 amount) public gated(amount) returns (uint256) { + return hook(amount + stored); + } + } + "#; + + let mut compiler = + Compiler::new(Session::builder().with_buffer_emitter(Default::default()).build()); + compiler + .enter_mut(|compiler| -> solar::interface::Result<()> { + let mut pcx = compiler.parse(); + pcx.set_resolve_imports(false); + let file = compiler + .sess() + .source_map() + .new_source_file(FileName::Stdin, source) + .expect("failed to create source file"); + pcx.add_file(file); + pcx.parse(); + + let ControlFlow::Continue(()) = compiler.lower_asts()? else { + panic!("expected HIR lowering to continue"); + }; + + let gcx = compiler.gcx(); + let source_id = gcx.hir.source_ids().next().expect("expected one lowered source"); + let ctx = LintContext::new( + gcx.sess, + false, + false, + LinterConfig { inline: &inline, lint_specific: &lint_specific }, + Vec::new(), + ); + let mut passes: Vec>> = + vec![Box::new(RecordingPass { counts: counts.clone() })]; + let mut visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir); + let _ = hir::Visit::visit_nested_source(&mut visitor, source_id); + Ok(()) + }) + .expect("failed to lower test source"); + + let counts = counts.lock().unwrap(); + assert!(counts.nested_item > 0, "expected nested item hook to run"); + assert!(counts.nested_contract > 0, "expected nested contract hook to run"); + assert!(counts.nested_function > 0, "expected nested function hook to run"); + assert!(counts.nested_var > 0, "expected nested var hook to run"); + assert!(counts.modifier > 0, "expected modifier hook to run"); + assert!(counts.call_args > 0, "expected call args hook to run"); + } +} diff --git a/crates/lint/src/sol/info/block_timestamp.rs b/crates/lint/src/sol/info/block_timestamp.rs new file mode 100644 index 0000000000000..b4ba3c7712217 --- /dev/null +++ b/crates/lint/src/sol/info/block_timestamp.rs @@ -0,0 +1,70 @@ +use super::BlockTimestamp; +use crate::{ + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::ast::{BinOp, BinOpKind, Expr, ExprKind}; + +declare_forge_lint!( + BLOCK_TIMESTAMP, + Severity::Low, + "block-timestamp", + "usage of `block.timestamp` in a comparison may be manipulated by validators" +); + +impl<'ast> EarlyLintPass<'ast> for BlockTimestamp { + fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) { + if let ExprKind::Binary(lhs, BinOp { kind, .. }, rhs) = &expr.kind + && is_cmp(*kind) + && (contains_block_timestamp(lhs) || contains_block_timestamp(rhs)) + { + ctx.emit(&BLOCK_TIMESTAMP, expr.span); + } + } +} + +const fn is_cmp(kind: BinOpKind) -> bool { + matches!( + kind, + BinOpKind::Lt + | BinOpKind::Le + | BinOpKind::Gt + | BinOpKind::Ge + | BinOpKind::Eq + | BinOpKind::Ne + ) +} + +/// Returns `true` if `expr` is `block.timestamp`. +fn is_block_timestamp(expr: &Expr<'_>) -> bool { + matches!( + &expr.kind, + ExprKind::Member(base, member) + if member.as_str() == "timestamp" + && matches!(&base.kind, ExprKind::Ident(ident) if ident.as_str() == "block") + ) +} + +/// Recursively checks if an expression tree contains `block.timestamp`. +fn contains_block_timestamp(expr: &Expr<'_>) -> bool { + if is_block_timestamp(expr) { + return true; + } + match &expr.kind { + ExprKind::Unary(_, inner) => contains_block_timestamp(inner), + ExprKind::Binary(lhs, _, rhs) => { + contains_block_timestamp(lhs) || contains_block_timestamp(rhs) + } + ExprKind::Tuple(elems) => elems.iter().any(|e| { + if let solar::interface::SpannedOption::Some(inner) = e.as_ref() { + contains_block_timestamp(inner) + } else { + false + } + }), + ExprKind::Call(callee, args) => { + contains_block_timestamp(callee) || args.exprs().any(|e| contains_block_timestamp(e)) + } + _ => false, + } +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index 97628a85e1132..28b7648b7dad5 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -21,6 +21,9 @@ use unsafe_cheatcodes::UNSAFE_CHEATCODE_USAGE; mod multi_contract_file; use multi_contract_file::MULTI_CONTRACT_FILE; +mod block_timestamp; +use block_timestamp::BLOCK_TIMESTAMP; + mod interface_naming; use interface_naming::{INTERFACE_FILE_NAMING, INTERFACE_NAMING}; @@ -33,5 +36,6 @@ register_lints!( (NamedStructFields, late, (NAMED_STRUCT_FIELDS)), (UnsafeCheatcodes, early, (UNSAFE_CHEATCODE_USAGE)), (MultiContractFile, early, (MULTI_CONTRACT_FILE)), - (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)) + (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)), + (BlockTimestamp, early, (BLOCK_TIMESTAMP)), ); diff --git a/crates/lint/src/sol/med/incorrect_erc20_interface.rs b/crates/lint/src/sol/med/incorrect_erc20_interface.rs new file mode 100644 index 0000000000000..b2f81e4503200 --- /dev/null +++ b/crates/lint/src/sol/med/incorrect_erc20_interface.rs @@ -0,0 +1,112 @@ +use super::IncorrectERC20Interface; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::sema::hir; + +declare_forge_lint!( + INCORRECT_ERC20_INTERFACE, + Severity::Med, + "incorrect-erc20-interface", + "incorrect ERC20 function interface" +); + +impl<'hir> LateLintPass<'hir> for IncorrectERC20Interface { + fn check_contract( + &mut self, + ctx: &LintContext, + hir: &'hir hir::Hir<'hir>, + contract: &'hir hir::Contract<'hir>, + ) { + // Check if the contract is a possible ERC20 by name or inheritance. + let is_erc20 = contract.linearized_bases.iter().any(|base_id| { + let name = hir.contract(*base_id).name.as_str(); + name == "ERC20" || name == "IERC20" + }); + + if !is_erc20 { + return; + } + + // If this contract implements a function from ERC721, we can assume it is an ERC721 token. + // These tokens offer functions which are similar to ERC20, but are not compatible. + let is_erc721 = contract.linearized_bases.iter().any(|base_id| { + let name = hir.contract(*base_id).name.as_str(); + name == "ERC721" || name == "IERC721" + }); + + if is_erc721 { + return; + } + + // Check each function in the contract for incorrect ERC20 signatures. + for item_id in contract.items { + let Some(fid) = item_id.as_function() else { continue }; + let func = hir.function(fid); + + if !func.kind.is_function() { + continue; + } + + let Some(name) = func.name else { continue }; + + if has_incorrect_erc20_signature(hir, name.as_str(), func.parameters, func.returns) { + ctx.emit(&INCORRECT_ERC20_INTERFACE, func.span); + } + } + } +} + +/// Checks if a function signature does not match the expected ERC20 specification. +/// +/// Returns `true` if the function name and parameter types match an ERC20 function but the return +/// types are incorrect. +fn has_incorrect_erc20_signature( + hir: &hir::Hir<'_>, + name: &str, + parameters: &[hir::VariableId], + returns: &[hir::VariableId], +) -> bool { + let is_type = |var_id: hir::VariableId, type_str: &str| { + matches!( + &hir.variable(var_id).ty.kind, + hir::TypeKind::Elementary(ty) if ty.to_abi_str() == type_str + ) + }; + + let params_match = |params: &[hir::VariableId], expected: &[&str]| -> bool { + params.len() == expected.len() + && params.iter().zip(expected).all(|(&id, &ty)| is_type(id, ty)) + }; + + let returns_match = |rets: &[hir::VariableId], expected: &[&str]| -> bool { + rets.len() == expected.len() && rets.iter().zip(expected).all(|(&id, &ty)| is_type(id, ty)) + }; + + match name { + // function transfer(address,uint256) external returns (bool) + "transfer" if params_match(parameters, &["address", "uint256"]) => { + !returns_match(returns, &["bool"]) + } + // function transferFrom(address,address,uint256) external returns (bool) + "transferFrom" if params_match(parameters, &["address", "address", "uint256"]) => { + !returns_match(returns, &["bool"]) + } + // function approve(address,uint256) external returns (bool) + "approve" if params_match(parameters, &["address", "uint256"]) => { + !returns_match(returns, &["bool"]) + } + // function allowance(address,address) external view returns (uint256) + "allowance" if params_match(parameters, &["address", "address"]) => { + !returns_match(returns, &["uint256"]) + } + // function balanceOf(address) external view returns (uint256) + "balanceOf" if params_match(parameters, &["address"]) => { + !returns_match(returns, &["uint256"]) + } + // function totalSupply() external view returns (uint256) + "totalSupply" if params_match(parameters, &[]) => !returns_match(returns, &["uint256"]), + _ => false, + } +} diff --git a/crates/lint/src/sol/med/incorrect_erc721_interface.rs b/crates/lint/src/sol/med/incorrect_erc721_interface.rs new file mode 100644 index 0000000000000..c19f4dc9f653f --- /dev/null +++ b/crates/lint/src/sol/med/incorrect_erc721_interface.rs @@ -0,0 +1,121 @@ +use super::IncorrectERC721Interface; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::sema::hir; + +declare_forge_lint!( + INCORRECT_ERC721_INTERFACE, + Severity::Med, + "incorrect-erc721-interface", + "incorrect ERC721 function interface" +); + +impl<'hir> LateLintPass<'hir> for IncorrectERC721Interface { + fn check_contract( + &mut self, + ctx: &LintContext, + hir: &'hir hir::Hir<'hir>, + contract: &'hir hir::Contract<'hir>, + ) { + // Check if the contract is a possible ERC721 by name or inheritance. + let is_erc721 = contract.linearized_bases.iter().any(|base_id| { + let name = hir.contract(*base_id).name.as_str(); + name == "ERC721" || name == "IERC721" + }); + + if !is_erc721 { + return; + } + + // Check each function in the contract for incorrect ERC721 signatures. + for item_id in contract.items { + let Some(fid) = item_id.as_function() else { continue }; + let func = hir.function(fid); + + if !func.kind.is_function() { + continue; + } + + let Some(name) = func.name else { continue }; + + if has_incorrect_erc721_signature(hir, name.as_str(), func.parameters, func.returns) { + ctx.emit(&INCORRECT_ERC721_INTERFACE, func.span); + } + } + } +} + +/// Checks if a function signature does not match the expected ERC721 (or ERC165) specification. +/// +/// Returns `true` if the function name and parameter types match an ERC721 function but the return +/// types are incorrect. +fn has_incorrect_erc721_signature( + hir: &hir::Hir<'_>, + name: &str, + parameters: &[hir::VariableId], + returns: &[hir::VariableId], +) -> bool { + let is_type = |var_id: hir::VariableId, type_str: &str| { + matches!( + &hir.variable(var_id).ty.kind, + hir::TypeKind::Elementary(ty) if ty.to_abi_str() == type_str + ) + }; + + let params_match = |params: &[hir::VariableId], expected: &[&str]| -> bool { + params.len() == expected.len() + && params.iter().zip(expected).all(|(&id, &ty)| is_type(id, ty)) + }; + + let returns_match = |rets: &[hir::VariableId], expected: &[&str]| -> bool { + rets.len() == expected.len() && rets.iter().zip(expected).all(|(&id, &ty)| is_type(id, ty)) + }; + + match name { + // function balanceOf(address) external view returns (uint256) + "balanceOf" if params_match(parameters, &["address"]) => { + !returns_match(returns, &["uint256"]) + } + // function ownerOf(uint256) external view returns (address) + "ownerOf" if params_match(parameters, &["uint256"]) => { + !returns_match(returns, &["address"]) + } + // function safeTransferFrom(address,address,uint256,bytes) external + "safeTransferFrom" + if params_match(parameters, &["address", "address", "uint256", "bytes"]) => + { + !returns_match(returns, &[]) + } + // function safeTransferFrom(address,address,uint256) external + "safeTransferFrom" if params_match(parameters, &["address", "address", "uint256"]) => { + !returns_match(returns, &[]) + } + // function transferFrom(address,address,uint256) external + "transferFrom" if params_match(parameters, &["address", "address", "uint256"]) => { + !returns_match(returns, &[]) + } + // function approve(address,uint256) external + "approve" if params_match(parameters, &["address", "uint256"]) => { + !returns_match(returns, &[]) + } + // function setApprovalForAll(address,bool) external + "setApprovalForAll" if params_match(parameters, &["address", "bool"]) => { + !returns_match(returns, &[]) + } + // function getApproved(uint256) external view returns (address) + "getApproved" if params_match(parameters, &["uint256"]) => { + !returns_match(returns, &["address"]) + } + // function isApprovedForAll(address,address) external view returns (bool) + "isApprovedForAll" if params_match(parameters, &["address", "address"]) => { + !returns_match(returns, &["bool"]) + } + // ERC165: function supportsInterface(bytes4) external view returns (bool) + "supportsInterface" if params_match(parameters, &["bytes4"]) => { + !returns_match(returns, &["bool"]) + } + _ => false, + } +} diff --git a/crates/lint/src/sol/med/mod.rs b/crates/lint/src/sol/med/mod.rs index 5034e849ce601..ba7a09b0e9bac 100644 --- a/crates/lint/src/sol/med/mod.rs +++ b/crates/lint/src/sol/med/mod.rs @@ -3,10 +3,18 @@ use crate::sol::{EarlyLintPass, LateLintPass, SolLint}; mod div_mul; use div_mul::DIVIDE_BEFORE_MULTIPLY; +mod incorrect_erc20_interface; +use incorrect_erc20_interface::INCORRECT_ERC20_INTERFACE; + +mod incorrect_erc721_interface; +use incorrect_erc721_interface::INCORRECT_ERC721_INTERFACE; + mod unsafe_typecast; use unsafe_typecast::UNSAFE_TYPECAST; register_lints!( (DivideBeforeMultiply, early, (DIVIDE_BEFORE_MULTIPLY)), + (IncorrectERC20Interface, late, (INCORRECT_ERC20_INTERFACE)), + (IncorrectERC721Interface, late, (INCORRECT_ERC721_INTERFACE)), (UnsafeTypecast, late, (UNSAFE_TYPECAST)) ); diff --git a/crates/lint/testdata/BlockTimestamp.sol b/crates/lint/testdata/BlockTimestamp.sol new file mode 100644 index 0000000000000..f5382f7a2988b --- /dev/null +++ b/crates/lint/testdata/BlockTimestamp.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract BlockTimestamp { + uint256 public deadline; + + // SHOULD FAIL: + + function directComparison() public view returns (bool) { + return block.timestamp > deadline; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function comparisonEq() public view returns (bool) { + return block.timestamp == 0; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function comparisonNe() public view returns (bool) { + return block.timestamp != 0; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function comparisonLe() public view returns (bool) { + return block.timestamp <= deadline; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function comparisonGe() public view returns (bool) { + return block.timestamp >= deadline; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function comparisonLt() public view returns (bool) { + return block.timestamp < deadline; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function timestampOnRight() public view returns (bool) { + return deadline > block.timestamp; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function timestampInArithmetic() public view returns (bool) { + return block.timestamp + 1 > deadline; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function timestampInComplexExpr() public view returns (bool) { + return (block.timestamp / 3600) == 0; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function inRequire() public view { + require(block.timestamp > deadline); //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + function inIfCondition() public view returns (uint256) { + if (block.timestamp > deadline) { //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + return 1; + } + return 0; + } + + function timestampInCallArg() public view returns (bool) { + return foo(block.timestamp) > 0; //~WARN: usage of `block.timestamp` in a comparison may be manipulated by validators + } + + // SHOULD PASS: + + function assignOnly() public view returns (uint256) { + uint256 t = block.timestamp; + return t; + } + + function emitOnly() public { + emit Timestamp(block.timestamp); + } + + function blockNumber() public view returns (bool) { + return block.number > 100; + } + + function arithmetic() public view returns (uint256) { + return block.timestamp + 100; + } + + function foo(uint256 x) internal pure returns (uint256) { + return x; + } + + event Timestamp(uint256 ts); +} diff --git a/crates/lint/testdata/BlockTimestamp.stderr b/crates/lint/testdata/BlockTimestamp.stderr new file mode 100644 index 0000000000000..016f8fa2bdb2d --- /dev/null +++ b/crates/lint/testdata/BlockTimestamp.stderr @@ -0,0 +1,96 @@ +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp > deadline; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp == 0; + │ ━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp != 0; + │ ━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp <= deadline; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp >= deadline; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp < deadline; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return deadline > block.timestamp; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return block.timestamp + 1 > deadline; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return (block.timestamp / 3600) == 0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ require(block.timestamp > deadline); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ if (block.timestamp > deadline) { + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + +warning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators + ╭▸ ROOT/testdata/BlockTimestamp.sol:LL:CC + │ +LL │ return foo(block.timestamp) > 0; + │ ━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#block-timestamp + diff --git a/crates/lint/testdata/IncorrectERC20Interface.sol b/crates/lint/testdata/IncorrectERC20Interface.sol new file mode 100644 index 0000000000000..bfc571ec0a474 --- /dev/null +++ b/crates/lint/testdata/IncorrectERC20Interface.sol @@ -0,0 +1,44 @@ +//@compile-flags: --severity high med low info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IERC20 {} + +// SHOULD FAIL: Interface named ERC20 with incorrect function signatures +interface ERC20 { + function transfer(address to, uint256 value) external returns (uint256); //~WARN: incorrect ERC20 function interface + function approve(address spender, uint256 value) external returns (uint256); //~WARN: incorrect ERC20 function interface +} + +// SHOULD FAIL: Interface inheriting from IERC20 with incorrect function signatures +interface IERC20Incorrect is IERC20 { + function transfer(address to, uint256 value) external returns (uint256); //~WARN: incorrect ERC20 function interface + function transferFrom(address from, address to, uint256 value) external returns (uint256); //~WARN: incorrect ERC20 function interface + function approve(address spender, uint256 value) external returns (uint256); //~WARN: incorrect ERC20 function interface + function allowance(address owner, address spender) external view returns (bool); //~WARN: incorrect ERC20 function interface + function balanceOf(address account) external view returns (bool); //~WARN: incorrect ERC20 function interface + function totalSupply() external view returns (bool); //~WARN: incorrect ERC20 function interface +} + +// SHOULD PASS: Correct ERC20 interface inheriting from IERC20 +interface IERC20Correct is IERC20 { + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + function approve(address spender, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function totalSupply() external view returns (uint256); +} + +// SHOULD PASS: Interface named IERC20 with correct function signatures +interface IERC20NamedCorrect { + function transfer(address to, uint256 value) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +// SHOULD PASS: Contract that is NOT named ERC20 and does not inherit from one +interface INotERC20 { + function transfer(address to, uint256 value) external returns (uint256); + function balanceOf(address account) external view returns (bool); +} diff --git a/crates/lint/testdata/IncorrectERC20Interface.stderr b/crates/lint/testdata/IncorrectERC20Interface.stderr new file mode 100644 index 0000000000000..3bb60ecce8320 --- /dev/null +++ b/crates/lint/testdata/IncorrectERC20Interface.stderr @@ -0,0 +1,120 @@ +note[interface-naming]: interface names should be prefixed with 'I' + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface ERC20 { + │ ━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface IERC20 {} + │ ━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface ERC20 { + │ ━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface IERC20Incorrect is IERC20 { + │ ━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface IERC20Correct is IERC20 { + │ ━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface IERC20NamedCorrect { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ interface INotERC20 { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function transfer(address to, uint256 value) external returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function approve(address spender, uint256 value) external returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function transfer(address to, uint256 value) external returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function transferFrom(address from, address to, uint256 value) external returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function approve(address spender, uint256 value) external returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function allowance(address owner, address spender) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function balanceOf(address account) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + +warning[incorrect-erc20-interface]: incorrect ERC20 function interface + ╭▸ ROOT/testdata/IncorrectERC20Interface.sol:LL:CC + │ +LL │ function totalSupply() external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc20-interface + diff --git a/crates/lint/testdata/IncorrectERC721Interface.sol b/crates/lint/testdata/IncorrectERC721Interface.sol new file mode 100644 index 0000000000000..431b1d5efdc96 --- /dev/null +++ b/crates/lint/testdata/IncorrectERC721Interface.sol @@ -0,0 +1,52 @@ +//@compile-flags: --severity high med low info + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IERC721 {} + +// SHOULD FAIL: Interface named ERC721 with incorrect function signatures +interface ERC721 { + function balanceOf(address owner) external view returns (bool); //~WARN: incorrect ERC721 function interface + function ownerOf(uint256 tokenId) external view returns (bool); //~WARN: incorrect ERC721 function interface +} + +// SHOULD FAIL: Interface inheriting from IERC721 with incorrect function signatures +interface IERC721Incorrect is IERC721 { + function balanceOf(address owner) external view returns (bool); //~WARN: incorrect ERC721 function interface + function ownerOf(uint256 tokenId) external view returns (bool); //~WARN: incorrect ERC721 function interface + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external returns (bool); //~WARN: incorrect ERC721 function interface + function safeTransferFrom(address from, address to, uint256 tokenId) external returns (bool); //~WARN: incorrect ERC721 function interface + function transferFrom(address from, address to, uint256 tokenId) external returns (bool); //~WARN: incorrect ERC721 function interface + function approve(address to, uint256 tokenId) external returns (bool); //~WARN: incorrect ERC721 function interface + function setApprovalForAll(address operator, bool approved) external returns (bool); //~WARN: incorrect ERC721 function interface + function getApproved(uint256 tokenId) external view returns (bool); //~WARN: incorrect ERC721 function interface + function isApprovedForAll(address owner, address operator) external view returns (address); //~WARN: incorrect ERC721 function interface + function supportsInterface(bytes4 interfaceId) external view returns (uint256); //~WARN: incorrect ERC721 function interface +} + +// SHOULD PASS: Correct ERC721 interface inheriting from IERC721 +interface IERC721Correct is IERC721 { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +// SHOULD PASS: Interface named IERC721 with correct function signatures +interface IERC721NamedCorrect { + function balanceOf(address owner) external view returns (uint256); + function ownerOf(uint256 tokenId) external view returns (address); +} + +// SHOULD PASS: Contract that is NOT named ERC721 and does not inherit from one +interface INotERC721 { + function balanceOf(address owner) external view returns (bool); + function ownerOf(uint256 tokenId) external view returns (bool); +} diff --git a/crates/lint/testdata/IncorrectERC721Interface.stderr b/crates/lint/testdata/IncorrectERC721Interface.stderr new file mode 100644 index 0000000000000..a88db93e39b10 --- /dev/null +++ b/crates/lint/testdata/IncorrectERC721Interface.stderr @@ -0,0 +1,152 @@ +note[interface-naming]: interface names should be prefixed with 'I' + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface ERC721 { + │ ━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#interface-naming + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface IERC721 {} + │ ━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface ERC721 { + │ ━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface IERC721Incorrect is IERC721 { + │ ━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface IERC721Correct is IERC721 { + │ ━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface IERC721NamedCorrect { + │ ━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +note[multi-contract-file]: prefer having only one contract, interface or library per file + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ interface INotERC721 { + │ ━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#multi-contract-file + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function balanceOf(address owner) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function ownerOf(uint256 tokenId) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function balanceOf(address owner) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function ownerOf(uint256 tokenId) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function safeTransferFrom(address from, address to, uint256 tokenId) external returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function transferFrom(address from, address to, uint256 tokenId) external returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function approve(address to, uint256 tokenId) external returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function setApprovalForAll(address operator, bool approved) external returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function getApproved(uint256 tokenId) external view returns (bool); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function isApprovedForAll(address owner, address operator) external view returns (address); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + +warning[incorrect-erc721-interface]: incorrect ERC721 function interface + ╭▸ ROOT/testdata/IncorrectERC721Interface.sol:LL:CC + │ +LL │ function supportsInterface(bytes4 interfaceId) external view returns (uint256); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#incorrect-erc721-interface + diff --git a/crates/macros/src/console_fmt.rs b/crates/macros/src/console_fmt.rs index 0c95841d1e653..245ba6e5b78e2 100644 --- a/crates/macros/src/console_fmt.rs +++ b/crates/macros/src/console_fmt.rs @@ -7,7 +7,7 @@ use syn::{ pub fn console_fmt(input: &DeriveInput) -> TokenStream { let name = &input.ident; let tokens = match &input.data { - Data::Struct(s) => derive_struct(s), + Data::Struct(s) => derive_struct(s, name), Data::Enum(e) => derive_enum(e), Data::Union(_) => return quote!(compile_error!("Unions are unsupported");), }; @@ -18,8 +18,8 @@ pub fn console_fmt(input: &DeriveInput) -> TokenStream { } } -fn derive_struct(s: &DataStruct) -> TokenStream { - let imp = impl_struct(s).unwrap_or_else(|| quote!(String::new())); +fn derive_struct(s: &DataStruct, name: &Ident) -> TokenStream { + let imp = impl_struct(s, name).unwrap_or_else(|| quote!(String::new())); quote! { fn fmt(&self, _spec: FormatSpec) -> String { #imp @@ -27,7 +27,7 @@ fn derive_struct(s: &DataStruct) -> TokenStream { } } -fn impl_struct(s: &DataStruct) -> Option { +fn impl_struct(s: &DataStruct, name: &Ident) -> Option { if s.fields.is_empty() { return None; } @@ -36,13 +36,49 @@ fn impl_struct(s: &DataStruct) -> Option { return None; } + let members = s.fields.members().collect::>(); let fields = s.fields.iter().collect::>(); + + // Detect table call structs: name must start with "table" (from the ABI function name) and + // all fields must be Vec types (Solidity arrays). Both conditions together prevent + // accidental table rendering for unrelated structs that happen to have Vec fields. + let is_table = name.to_string().starts_with("table") + && !fields.is_empty() + && fields.iter().all(|f| match &f.ty { + Type::Path(path) => path.path.segments.last().is_some_and(|seg| seg.ident == "Vec"), + _ => false, + }); + if is_table { + let member_ref = |m: &Member| match m { + Member::Named(ident) => quote!(&self.#ident), + Member::Unnamed(idx) => quote!(&self.#idx), + }; + let imp = if members.len() == 1 { + let vals = member_ref(&members[0]); + quote! { + let values: ::std::vec::Vec<&dyn ConsoleFmt> = + (#vals).iter().map(|v| v as &dyn ConsoleFmt).collect(); + console_table_format(None, &values) + } + } else { + let keys = member_ref(&members[0]); + let vals = member_ref(&members[1]); + quote! { + let keys: ::std::vec::Vec<&dyn ConsoleFmt> = + (#keys).iter().map(|v| v as &dyn ConsoleFmt).collect(); + let values: ::std::vec::Vec<&dyn ConsoleFmt> = + (#vals).iter().map(|v| v as &dyn ConsoleFmt).collect(); + console_table_format(Some(&keys), &values) + } + }; + return Some(imp); + } + let first_ty = match &fields.first().unwrap().ty { Type::Path(path) => path.path.segments.last().unwrap().ident.to_string(), _ => String::new(), }; - let members = s.fields.members().collect::>(); let args: Punctuated = members .into_iter() .map(|member| match member { diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 6f30ac99b970a..ab4aafcc294c0 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,6 +1,6 @@ use alloy_consensus::{ - Sealed, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType, - Typed2718, + Sealed, Signed, Transaction as _, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, + TxLegacy, TxType, Typed2718, crypto::RecoveryError, transaction::{ SignerRecoverable, TxEip7702, TxHashRef, @@ -12,7 +12,10 @@ use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse}; use alloy_op_evm::OpTx; use alloy_primitives::{Address, B256, Bytes, TxHash}; use alloy_rpc_types::ConversionError; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, TxDeposit}; +use op_alloy_consensus::{ + DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, POST_EXEC_TX_TYPE_ID, TxDeposit, + TxPostExec, +}; use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts}; use revm::context::TxEnv; use tempo_primitives::{AASigned, TempoTransaction}; @@ -56,6 +59,9 @@ pub enum FoundryTxEnvelope { /// See . #[envelope(ty = 126)] Deposit(Sealed), + /// OP stack post-execution synthetic transaction. + #[envelope(ty = 0x7D)] + PostExec(Sealed), /// Tempo transaction type. /// /// See . @@ -75,6 +81,7 @@ impl FoundryTxEnvelope { Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)), Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)), Self::Deposit(_) => Err(self), + Self::PostExec(_) => Err(self), Self::Tempo(_) => Err(self), } } @@ -103,6 +110,7 @@ impl FoundryTxEnvelope { Self::Eip4844(t) => *t.hash(), Self::Eip7702(t) => *t.hash(), Self::Deposit(t) => t.tx_hash(), + Self::PostExec(t) => t.tx_hash(), Self::Tempo(t) => *t.hash(), } } @@ -121,6 +129,7 @@ impl FoundryTxEnvelope { Self::Eip4844(tx) => tx.recover_signer()?, Self::Eip7702(tx) => tx.recover_signer()?, Self::Deposit(tx) => tx.from, + Self::PostExec(tx) => tx.inner().signer_address(), Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?, }) } @@ -135,6 +144,7 @@ impl TxHashRef for FoundryTxEnvelope { Self::Eip4844(t) => t.hash(), Self::Eip7702(t) => t.hash(), Self::Deposit(t) => t.hash_ref(), + Self::PostExec(t) => t.hash_ref(), Self::Tempo(t) => t.hash(), } } @@ -161,6 +171,10 @@ impl OpTransactionTrait for FoundryTxEnvelope { _ => None, } } + + fn as_post_exec(&self) -> Option<&Sealed> { + if let Self::PostExec(tx) = self { Some(tx) } else { None } + } } impl TryFrom for TxEnvelope { @@ -191,6 +205,7 @@ impl From for FoundryTxEnvelope { op_alloy_consensus::OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx), op_alloy_consensus::OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx), op_alloy_consensus::OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), + op_alloy_consensus::OpTxEnvelope::PostExec(tx) => Self::PostExec(tx), } } } @@ -233,7 +248,18 @@ impl TryFrom for FoundryTxEnvelope { })?; return Ok(Self::Deposit(Sealed::new(deposit_tx))); - }; + } + + if tx.ty() == POST_EXEC_TX_TYPE_ID { + let post_exec_tx = + tx.inner.fields.deserialize_into::().map_err(|e| { + ConversionError::Custom(format!( + "Failed to deserialize post-exec tx: {e}" + )) + })?; + + return Ok(Self::PostExec(Sealed::new(post_exec_tx))); + } let tx_type = tx.ty(); Err(ConversionError::Custom(format!("Unknown transaction type: 0x{tx_type:02X}"))) @@ -262,6 +288,16 @@ impl FromRecoveredTx for TxEnv { ..Default::default() } } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + Self { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + } + } FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"), } } @@ -308,6 +344,17 @@ impl FromRecoveredTx for OpTransaction { }; Self { base, enveloped_tx: None, deposit } } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: None, deposit: Default::default() } + } FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), } } @@ -338,6 +385,7 @@ impl FromRecoveredTx for TempoTxEnv { Self::from(TxEnv::from_recovered_tx(signed_tx, caller)) } FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"), + FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"), FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller), } } @@ -402,6 +450,17 @@ impl FromTxWithEncoded for OpTransaction { }; Self { base, enveloped_tx: Some(encoded), deposit } } + FoundryTxEnvelope::PostExec(sealed_tx) => { + let tx = sealed_tx.inner(); + let base = TxEnv { + tx_type: tx.ty(), + caller, + kind: tx.kind(), + data: tx.input.clone(), + ..Default::default() + }; + Self { base, enveloped_tx: Some(encoded), deposit: Default::default() } + } FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"), } } @@ -416,6 +475,7 @@ impl std::fmt::Display for FoundryTxType { Self::Eip4844 => write!(f, "eip4844"), Self::Eip7702 => write!(f, "eip7702"), Self::Deposit => write!(f, "deposit"), + Self::PostExec => write!(f, "post-exec"), Self::Tempo => write!(f, "tempo"), } } @@ -442,6 +502,7 @@ impl From for FoundryTypedTx { FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()), FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()), FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()), + FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.into_inner()), FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()), } } diff --git a/crates/primitives/src/transaction/receipt.rs b/crates/primitives/src/transaction/receipt.rs index 7df6494fc584a..fe209fc72c907 100644 --- a/crates/primitives/src/transaction/receipt.rs +++ b/crates/primitives/src/transaction/receipt.rs @@ -8,7 +8,9 @@ use alloy_network::eip2718::{ use alloy_primitives::{Bloom, Log, TxHash, logs_bloom}; use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes}; use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt}; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom}; +use op_alloy_consensus::{ + DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID, +}; use serde::{Deserialize, Serialize}; use tempo_primitives::TEMPO_TX_TYPE_ID; @@ -27,6 +29,8 @@ pub enum FoundryReceiptEnvelope { Eip4844(ReceiptWithBloom>), #[serde(rename = "0x4", alias = "0x04")] Eip7702(ReceiptWithBloom>), + #[serde(rename = "0x7D", alias = "0x7d")] + PostExec(ReceiptWithBloom>), #[serde(rename = "0x7E", alias = "0x7e")] Deposit(OpDepositReceiptWithBloom), #[serde(rename = "0x76")] @@ -63,6 +67,9 @@ impl FoundryReceiptEnvelope { FoundryTxType::Eip7702 => { Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) } + FoundryTxType::PostExec => { + Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom }) + } FoundryTxType::Deposit => { let inner = OpDepositReceiptWithBloom { receipt: OpDepositReceipt { @@ -125,6 +132,7 @@ impl FoundryReceiptEnvelope { Self::Eip1559(_) => FoundryTxType::Eip1559, Self::Eip4844(_) => FoundryTxType::Eip4844, Self::Eip7702(_) => FoundryTxType::Eip7702, + Self::PostExec(_) => FoundryTxType::PostExec, Self::Deposit(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -150,6 +158,7 @@ impl FoundryReceiptEnvelope { Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)), Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)), Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)), + Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)), Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(r.map_receipt(|r| r.map_logs(f))), Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)), } @@ -173,6 +182,7 @@ impl FoundryReceiptEnvelope { Self::Eip1559(t) => &t.logs_bloom, Self::Eip4844(t) => &t.logs_bloom, Self::Eip7702(t) => &t.logs_bloom, + Self::PostExec(t) => &t.logs_bloom, Self::Deposit(t) => &t.logs_bloom, Self::Tempo(t) => &t.logs_bloom, } @@ -212,6 +222,7 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) + | Self::PostExec(t) | Self::Tempo(t) => t.receipt, Self::Deposit(t) => t.receipt.into_inner(), } @@ -225,6 +236,7 @@ impl FoundryReceiptEnvelope { | Self::Eip1559(t) | Self::Eip4844(t) | Self::Eip7702(t) + | Self::PostExec(t) | Self::Tempo(t) => &t.receipt, Self::Deposit(t) => &t.receipt.inner, } @@ -275,6 +287,7 @@ impl Encodable for FoundryReceiptEnvelope { Self::Eip1559(r) => r.length() + 1, Self::Eip4844(r) => r.length() + 1, Self::Eip7702(r) => r.length() + 1, + Self::PostExec(r) => r.length() + 1, Self::Deposit(r) => r.length() + 1, Self::Tempo(r) => r.length() + 1, _ => unreachable!("receipt already matched"), @@ -301,6 +314,11 @@ impl Encodable for FoundryReceiptEnvelope { EIP7702_TX_TYPE_ID.encode(out); r.encode(out); } + Self::PostExec(r) => { + Header { list: true, payload_length: payload_len }.encode(out); + POST_EXEC_TX_TYPE_ID.encode(out); + r.encode(out); + } Self::Deposit(r) => { Header { list: true, payload_length: payload_len }.encode(out); DEPOSIT_TX_TYPE_ID.encode(out); @@ -353,6 +371,10 @@ impl Decodable for FoundryReceiptEnvelope { buf.advance(1); ::decode(buf) .map(FoundryReceiptEnvelope::Eip7702) + } else if receipt_type == POST_EXEC_TX_TYPE_ID { + buf.advance(1); + ::decode(buf) + .map(FoundryReceiptEnvelope::PostExec) } else if receipt_type == DEPOSIT_TX_TYPE_ID { buf.advance(1); ::decode(buf) @@ -382,6 +404,7 @@ impl Typed2718 for FoundryReceiptEnvelope { Self::Eip1559(_) => EIP1559_TX_TYPE_ID, Self::Eip4844(_) => EIP4844_TX_TYPE_ID, Self::Eip7702(_) => EIP7702_TX_TYPE_ID, + Self::PostExec(_) => POST_EXEC_TX_TYPE_ID, Self::Deposit(_) => DEPOSIT_TX_TYPE_ID, Self::Tempo(_) => TEMPO_TX_TYPE_ID, } @@ -396,6 +419,7 @@ impl Encodable2718 for FoundryReceiptEnvelope { Self::Eip1559(r) => 1 + r.length(), Self::Eip4844(r) => 1 + r.length(), Self::Eip7702(r) => 1 + r.length(), + Self::PostExec(r) => 1 + r.length(), Self::Deposit(r) => 1 + r.length(), Self::Tempo(r) => 1 + r.length(), } @@ -411,6 +435,7 @@ impl Encodable2718 for FoundryReceiptEnvelope { | Self::Eip1559(r) | Self::Eip4844(r) | Self::Eip7702(r) + | Self::PostExec(r) | Self::Tempo(r) => r.encode(out), Self::Deposit(r) => r.encode(out), } @@ -425,6 +450,9 @@ impl Decodable2718 for FoundryReceiptEnvelope { if ty == TEMPO_TX_TYPE_ID { return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?)); } + if ty == POST_EXEC_TX_TYPE_ID { + return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?)); + } match ReceiptEnvelope::typed_decode(ty, buf)? { ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)), ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)), diff --git a/crates/primitives/src/transaction/request.rs b/crates/primitives/src/transaction/request.rs index 426d3c818f700..2c4dbae8fdcd4 100644 --- a/crates/primitives/src/transaction/request.rs +++ b/crates/primitives/src/transaction/request.rs @@ -6,7 +6,7 @@ use alloy_network::{ use alloy_primitives::{Address, B256, ChainId, TxKind, U256}; use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest}; use alloy_serde::{OtherFields, WithOtherFields}; -use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; +use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit}; use op_revm::transaction::deposit::DepositTransactionParts; use serde::{Deserialize, Serialize}; use tempo_alloy::rpc::TempoTransactionRequest; @@ -69,6 +69,9 @@ impl FoundryTransactionRequest { pub fn preferred_type(&self) -> FoundryTxType { match self { Self::Ethereum(tx) => tx.preferred_type().into(), + Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => { + FoundryTxType::PostExec + } Self::Op(_) => FoundryTxType::Deposit, Self::Tempo(_) => FoundryTxType::Tempo, } @@ -121,6 +124,7 @@ impl FoundryTransactionRequest { FoundryTxType::Eip4844 => self.complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), FoundryTxType::Deposit => self.complete_deposit(), + FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } { Err((pref, missing)) @@ -242,6 +246,7 @@ impl From> for FoundryTransactionRequest { } Self::Tempo(Box::new(tempo_tx_req)) } else if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID) + || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID) || get_deposit_tx_parts(&tx.other).is_ok() { Self::Op(tx) @@ -267,6 +272,11 @@ impl From for FoundryTransactionRequest { ]); WithOtherFields { inner: Into::::into(tx), other }.into() } + FoundryTypedTx::PostExec(tx) => WithOtherFields { + inner: Into::::into(tx), + other: OtherFields::default(), + } + .into(), FoundryTypedTx::Tempo(tx) => { let mut other = OtherFields::default(); if let Some(fee_token) = tx.fee_token { @@ -428,6 +438,7 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip4844 => self.as_ref().complete_4844(), FoundryTxType::Eip7702 => self.as_ref().complete_7702(), FoundryTxType::Deposit => self.complete_deposit(), + FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]), FoundryTxType::Tempo => self.complete_tempo(), } } @@ -455,6 +466,7 @@ impl NetworkTransactionBuilder for FoundryTransactionRequest { FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(), FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(), FoundryTxType::Deposit => self.complete_deposit().ok(), + FoundryTxType::PostExec => self.complete_type(pref).ok(), FoundryTxType::Tempo => self.complete_tempo().ok(), }?; Some(pref) diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 4146e07d2248b..acdcbbdbf95ea 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -23,7 +23,7 @@ foundry-evm-networks.workspace = true alloy-evm.workspace = true foundry-debugger.workspace = true foundry-cheatcodes.workspace = true -foundry-wallets.workspace = true +foundry-wallets = { workspace = true, features = ["browser", "tempo"] } foundry-linking.workspace = true forge-script-sequence.workspace = true diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 162d061462b66..627bcfffd3802 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -168,6 +168,7 @@ impl PreExecutionState { setup_result.traces.extend(script_result.traces); setup_result.labeled_addresses.extend(script_result.labeled_addresses); setup_result.returned = script_result.returned; + setup_result.exit_reason = script_result.exit_reason; setup_result.breakpoints = script_result.breakpoints; match (&mut setup_result.transactions, script_result.transactions) { @@ -422,7 +423,11 @@ impl PreSimulationState { if !self.execution_result.success { return Err(eyre::eyre!( "script failed: {}", - &self.execution_artifacts.decoder.revert_decoder.decode(&result.returned[..], None) + &self + .execution_artifacts + .decoder + .revert_decoder + .decode(&result.returned[..], result.exit_reason) )); } @@ -505,7 +510,11 @@ impl PreSimulationState { if !result.success { return Err(eyre::eyre!( "script failed: {}", - &self.execution_artifacts.decoder.revert_decoder.decode(&result.returned[..], None) + &self + .execution_artifacts + .decoder + .revert_decoder + .decode(&result.returned[..], result.exit_reason) )); } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index d8d83913edb40..f5e4d46de0344 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -57,6 +57,7 @@ use foundry_evm::{ cheatcodes::{BroadcastableTransactions, Wallets}, }, opts::EvmOpts, + revm::interpreter::InstructionResult, traces::{TraceMode, Traces}, }; use foundry_evm_networks::NetworkConfigs; @@ -637,6 +638,8 @@ pub struct ScriptResult { #[serde(skip)] pub transactions: Option>, pub returned: Bytes, + #[serde(skip)] + pub exit_reason: Option, pub address: Option
, #[serde(skip)] pub breakpoints: Breakpoints, @@ -652,6 +655,7 @@ impl Default for ScriptResult { labeled_addresses: Default::default(), transactions: Default::default(), returned: Default::default(), + exit_reason: Default::default(), address: Default::default(), breakpoints: Default::default(), } diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index de80b1bf98c47..e2404d60ce2b9 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -275,7 +275,7 @@ impl ScriptRunner { value.unwrap_or(U256::ZERO), None, ); - let (address, RawCallResult { gas_used, logs, traces, .. }) = match res { + let (address, RawCallResult { gas_used, logs, traces, exit_reason, .. }) = match res { Ok(DeployResult { address, raw }) => (address, raw), Err(EvmError::Execution(err)) => { let ExecutionErr { raw, reason } = *err; @@ -294,6 +294,7 @@ impl ScriptRunner { traces: traces .map(|traces| vec![(TraceKind::Execution, traces)]) .unwrap_or_default(), + exit_reason, address: Some(address), ..Default::default() }) @@ -348,7 +349,9 @@ impl ScriptRunner { } } - let RawCallResult { result, reverted, logs, traces, labels, transactions, .. } = res; + let RawCallResult { + result, reverted, logs, traces, labels, transactions, exit_reason, .. + } = res; let breakpoints = res.cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); Ok(ScriptResult { @@ -365,6 +368,7 @@ impl ScriptRunner { .unwrap_or_default(), labeled_addresses: labels, transactions, + exit_reason, address: None, breakpoints, }) diff --git a/crates/verify/src/etherscan/flatten.rs b/crates/verify/src/etherscan/flatten.rs index 731779714776f..81bd48bda4b88 100644 --- a/crates/verify/src/etherscan/flatten.rs +++ b/crates/verify/src/etherscan/flatten.rs @@ -89,7 +89,7 @@ impl EtherscanFlattenedSource { if out.errors.iter().any(|e| e.is_error()) { let mut o = AggregatedCompilerOutput::::default(); o.extend(version, RawBuildInfo::new(&input, &out, false)?, "default", out); - let diags = o.diagnostics(&[], &[], Default::default()); + let diags = o.diagnostics(&[], &[], Default::default(), Default::default()); eyre::bail!( "\ diff --git a/deny.toml b/deny.toml index b9dae10bfabf3..1a0e1e8e53005 100644 --- a/deny.toml +++ b/deny.toml @@ -100,10 +100,7 @@ unknown-git = "deny" allow-git = [ "https://github.com/alloy-rs/alloy", "https://github.com/alloy-rs/evm", - "https://github.com/foundry-rs/compilers", - "https://github.com/foundry-rs/foundry-fork-db", "https://github.com/foundry-rs/foundry-core", - "https://github.com/foundry-rs/optimism", "https://github.com/paradigmxyz/revm-inspectors", "https://github.com/paradigmxyz/solar", "https://github.com/bluealloy/revm", @@ -114,4 +111,7 @@ allow-git = [ "https://github.com/tempoxyz/mpp-rs", # Transitive dependency of Tempo "https://github.com/paradigmxyz/reth", + "https://github.com/paradigmxyz/reth-core", + # Temporary: upstream OP crates until release is published. + "https://github.com/ethereum-optimism/optimism", ] diff --git a/flake.lock b/flake.lock index 4fa81efa24f11..27f426f491da6 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1776497206, - "narHash": "sha256-Em+RSdFnwyyKPGUBFtQYtVjm+1UvIc9gOR91Y22zlzg=", + "lastModified": 1777102577, + "narHash": "sha256-ycoy9svZOQgyInu/lwO7IEQtlP5liqYhEcF9m9hPRbM=", "owner": "nix-community", "repo": "fenix", - "rev": "df2295365fb081fe0745449762a771290782c22d", + "rev": "f37403486c59376cd285f9685a8ef8ff25c09a3c", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776329215, - "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", + "lastModified": 1776949667, + "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b86751bc4085f48661017fa226dee99fab6c651b", + "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1776441750, - "narHash": "sha256-1rVfG+mj8R4ze+lSYCa4iAv7FzrB03Cprtxmd1MfZak=", + "lastModified": 1776800521, + "narHash": "sha256-f8YJfwAOsLFpIoqZuX3yF69UvMLrkx7iVzMH1pJU7cM=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "251df518d73abb5c5d573c4d5d266a3edae9ca5a", + "rev": "8954b66d43225e62c92e8bbcc8500191b5cceb1e", "type": "github" }, "original": { diff --git a/foundryup/foundryup b/foundryup/foundryup index 5aadb284917bb..6d1ac99c09e41 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -3,7 +3,7 @@ set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -FOUNDRYUP_INSTALLER_VERSION="1.6.1" +FOUNDRYUP_INSTALLER_VERSION="1.7.0" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} @@ -102,14 +102,12 @@ main() { exit 0 fi - # If Tempo network is set, use the Tempo fork of Foundry - if [[ "$FOUNDRYUP_NETWORK" == "tempo" ]]; then - FOUNDRYUP_REPO="tempoxyz/tempo-foundry" - else - # Default to Foundry repo - FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} + if [[ -n "$FOUNDRYUP_NETWORK" ]]; then + warn "--network is deprecated and ignored; installing the regular Foundry release" fi + FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} + # Install by downloading binaries if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-stable} @@ -315,58 +313,6 @@ main() { FOUNDRYUP_VERSION=$FOUNDRYUP_TAG use - say "done!" - # Install from Tempo's fork of Foundry if --network tempo is set - elif [[ "$FOUNDRYUP_NETWORK" == "tempo" ]]; then - FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-nightly} - FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" - - say "installing tempo-foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})" - - # Detect platform and architecture. - detect_platform_arch - - # Compute the URL of the release tarball in the Tempo Foundry repository. - RELEASE_URL="https://github.com/${FOUNDRYUP_REPO}/releases/download/${FOUNDRYUP_TAG}/" - BIN_ARCHIVE_URL="${RELEASE_URL}foundry_${FOUNDRYUP_VERSION}_${PLATFORM}_${ARCHITECTURE}.$EXT" - MAN_TARBALL_URL="${RELEASE_URL}foundry_man_${FOUNDRYUP_VERSION}.tar.gz" - - ensure mkdir -p "$FOUNDRY_VERSIONS_DIR" - - # Download and extract the binaries archive - say "downloading forge, cast, anvil, and chisel for $FOUNDRYUP_TAG version" - if [ "$PLATFORM" = "win32" ]; then - tmp="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" - tmp="$tmp/foundry.zip" - ensure download "$BIN_ARCHIVE_URL" "$tmp" - ensure unzip "$tmp" -d "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" - rm -f "$tmp" - else - tmp="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" - tmp="$tmp/foundry.tar.gz" - ensure download "$BIN_ARCHIVE_URL" "$tmp" - # Make sure it's a valid tar archive. - ensure tar tf "$tmp" 1> /dev/null - ensure mkdir -p "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" - ensure tar -C "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" -xvf "$tmp" - rm -f "$tmp" - fi - - # Optionally download the manuals - if check_cmd tar; then - say "downloading manpages" - mkdir -p "$FOUNDRY_MAN_DIR" - if ! download "$MAN_TARBALL_URL" | tar -xzC "$FOUNDRY_MAN_DIR"; then - warn "skipping manpage download: unavailable or invalid archive" - fi - else - say 'skipping manpage download: missing "tar"' - fi - - # Use newly installed version. - FOUNDRYUP_VERSION=$FOUNDRYUP_TAG - use - say "done!" # Install by cloning the repo with the provided branch/tag else @@ -455,8 +401,8 @@ OPTIONS: -r, --repo Build and install from a remote GitHub repo (uses default branch if no other options are set) -p, --path Build and install a local repository -j, --jobs Number of CPUs to use for building Foundry (default: all CPUs) + -n, --network Deprecated and ignored; installs the regular Foundry release -f, --force Skip SHA verification for downloaded binaries (INSECURE - use with caution) - -n, --network Install binaries for a specific network (e.g., tempo) --arch Install a specific architecture (supports amd64 and arm64) --platform Install a specific platform (supports win32, linux, darwin and alpine) EOF diff --git a/npm/bun.lock b/npm/bun.lock index e8ef087be9d50..d9e8fed5ce479 100644 --- a/npm/bun.lock +++ b/npm/bun.lock @@ -1,12 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { "@types/bun": "^1.3.1", - "@types/node": "^24.9.1", + "@types/node": "^25.5.2", "bun": "^1.3.1", - "typescript": "^5.9.3", + "typescript": "^6.0.2", }, }, }, @@ -35,7 +36,7 @@ "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], - "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], @@ -45,8 +46,12 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "bun-types/@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/npm/scripts/publish.mjs b/npm/scripts/publish.mjs index 0f582c8ebc555..deee29e46c92f 100644 --- a/npm/scripts/publish.mjs +++ b/npm/scripts/publish.mjs @@ -7,17 +7,12 @@ import { colors } from '#const.mjs' const REGISTRY_URL = Bun.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org' -const NPM_TOKEN = Bun.env.NPM_TOKEN -if (!NPM_TOKEN) throw new Error('NPM_TOKEN is required') - main().catch(error => { console.error(error) process.exit(1) }) async function main() { - const npmToken = Bun.env.NPM_TOKEN - if (!npmToken) throw new Error('NPM_TOKEN is required') const inputPath = Bun.argv[2] if (!inputPath) throw new Error('Package path is required') @@ -114,11 +109,6 @@ async function setPackageVersion(packagePath, version) { console.info(colors.green, 'Setting package version:', version) const result = await Bun.$`npm version ${version} --allow-same-version --no-git-tag-version` .cwd(packagePath) - .env({ - ...Bun.env, - ...process.env, - NPM_TOKEN - }) .quiet() .nothrow() diff --git a/npm/tsconfig.json b/npm/tsconfig.json index ca8e3c92f2daa..a7a0093d7ee56 100644 --- a/npm/tsconfig.json +++ b/npm/tsconfig.json @@ -2,7 +2,6 @@ "schema": "https://json.schemastore.org/tsconfig.json", "compilerOptions": { "strict": true, - "baseUrl": ".", "noEmit": true, "allowJs": true, "checkJs": true, diff --git a/testdata/default/cheats/RecordDebugTrace.t.sol b/testdata/default/cheats/RecordDebugTrace.t.sol index f7c0b895761e4..b4f15e1aad5b6 100644 --- a/testdata/default/cheats/RecordDebugTrace.t.sol +++ b/testdata/default/cheats/RecordDebugTrace.t.sol @@ -162,4 +162,19 @@ contract RecordDebugTraceTest is Test { } assertTrue(isOOG, "should OOG"); } + + /// forge-config: default.allow_internal_expect_revert = true + function testCannotRestartDebugTraceRecordingWhileActive() public { + MStoreAndMLoadCaller testContract = new MStoreAndMLoadCaller(); + + vm.startDebugTraceRecording(); + + vm.expectRevert("vm.startDebugTraceRecording: debug trace recording was already started"); + vm.startDebugTraceRecording(); + + testContract.storeAndLoadValueFromMemory(); + + Vm.DebugStep[] memory steps = vm.stopAndReturnDebugTraceRecording(); + assertGt(steps.length, 0, "first recording should remain active after failed restart"); + } } diff --git a/testdata/forge-std-rev b/testdata/forge-std-rev index 8bc543e5cf68b..b1716a0a12950 100644 --- a/testdata/forge-std-rev +++ b/testdata/forge-std-rev @@ -1 +1 @@ -0844d7e1fc5e60d77b68e469bff60265f236c398 \ No newline at end of file +8987040ede9553cea20c95ad40d0455930f9c8e0 \ No newline at end of file From 2de52dc4f3e6c9ac9c9bf76ba066cfcfc8150582 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:50:14 +0700 Subject: [PATCH 08/25] Update benches/src/lib.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- benches/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 12fa2bac0ea28..a6da2f6536cdc 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -146,8 +146,12 @@ impl BenchmarkProject { Ok(p) => p, Err(_) => continue, // Skip if unable to canonicalize }; + let canon_root = match root_path.canonicalize() { + Ok(p) => p, + Err(_) => root_path.clone(), + }; // Ensure canonicalized path stays strictly within root_path (TempProject root) - if !canon.starts_with(&root_path) { + if !canon.starts_with(&canon_root) { sh_eprintln!("⚠️ Skipping suspicious path during cleanup: {:?}", canon); continue; } From c8f1a726014b846b0180c62c7ccbc4c6389446ea Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:51:00 +0700 Subject: [PATCH 09/25] Update crates/lint/src/linter.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/lint/src/linter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 2c11e0222a286..a3ba693564b56 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -45,7 +45,7 @@ impl<'s> LintContext<'s> { } // Helper method to emit diagnostics easily from passes - pub fn emit(&self, lint: &'static L, span: Span) { + pub fn emit(&self, lint: &L, span: Span) { let desc = if self.desc { lint.description() } else { "" }; let diag: DiagBuilder<'_, ()> = self .sess From b02f1b5183178d785be6704dbe1a04c9ceeee00f Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:51:33 +0700 Subject: [PATCH 10/25] Update crates/lint/src/linter.rs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- crates/lint/src/linter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index a3ba693564b56..eaa46d0e0b492 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -60,7 +60,7 @@ impl<'s> LintContext<'s> { } /// Trait for lints that operate directly on the AST. -/// Its methods mirror `solar_ast::visit::Visit`, with the addition of `LintCotext`. +/// Its methods mirror `solar_ast::visit::Visit`, with the addition of `LintContext`. pub trait EarlyLintPass<'ast>: Send + Sync { fn check_expr(&mut self, _ctx: &LintContext<'_>, _expr: &'ast Expr<'ast>) {} fn check_item_struct(&mut self, _ctx: &LintContext<'_>, _struct: &'ast ItemStruct<'ast>) {} From d0dd93eafba2acf949b013337fb6de5b9d7c9558 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:52:27 +0700 Subject: [PATCH 11/25] Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 53a505774ac88..450cfc4ef5134 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,6 +25,7 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] + - Browser [e.g. Chrome, Safari] - Browser [e.g. Chrome, safari] - Version [e.g. 22] From 694b35f4037c6995af206b5c0015f179c85307c5 Mon Sep 17 00:00:00 2001 From: figtracer Date: Tue, 16 Jun 2026 08:20:25 +0100 Subject: [PATCH 12/25] fix(debugger): identify forked script contracts (#15185) * fix(debugger): identify forked script contracts * fix(debugger): gate script debug bytecodes --- crates/forge/tests/cli/script.rs | 82 +++++++++++++++++++++++++- crates/forge/tests/cli/test_cmd/mod.rs | 18 +----- crates/forge/tests/cli/utils.rs | 12 ++++ crates/script/src/execute.rs | 15 ++++- crates/script/src/lib.rs | 6 +- crates/script/src/runner.rs | 82 ++++++++++++++++++++++---- 6 files changed, 184 insertions(+), 31 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 910cc1260189f..e8a06bcf0992a 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -1,6 +1,9 @@ //! Contains various tests related to `forge script`. -use crate::{constants::TEMPLATE_CONTRACT, utils::generate_large_runtime_contract}; +use crate::{ + constants::TEMPLATE_CONTRACT, + utils::{assert_debug_dump_identifies_contract, generate_large_runtime_contract}, +}; use alloy_hardforks::EthereumHardfork; use alloy_network::Ethereum; use alloy_primitives::{Address, Bytes, address, hex}; @@ -43,6 +46,83 @@ contract ContractScript is Script { } ); +forgetest_async!(script_debug_dump_identifies_contracts_loaded_from_fork, |prj, cmd| { + prj.add_source( + "ScriptForkDebugTarget.sol", + r#" +contract ScriptForkDebugTarget { + uint256 public value; + + function set(uint256 newValue) public { + value = newValue; + } +} +"#, + ); + + let (_api, handle) = spawn(NodeConfig::test()).await; + let rpc = handle.http_endpoint(); + let pk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + + let create_output = cmd + .forge_fuse() + .args([ + "create", + "./src/ScriptForkDebugTarget.sol:ScriptForkDebugTarget", + "--rpc-url", + rpc.as_str(), + "--private-key", + pk, + "--broadcast", + "--json", + ]) + .assert_success() + .get_output() + .stdout + .clone(); + let create_output: Value = serde_json::from_slice(&create_output).unwrap(); + let deployed = create_output["deployedTo"].as_str().unwrap().to_owned(); + + prj.add_script( + "DebugRemote.s.sol", + &format!( + r#" +interface IScriptForkDebugTarget {{ + function set(uint256 newValue) external; + function value() external view returns (uint256); +}} + +contract DebugRemote {{ + IScriptForkDebugTarget private target = IScriptForkDebugTarget({deployed}); + + function setUp() public {{ + target.set(7); + require(target.value() == 7, "setup value"); + }} + + function run() public {{ + target.set(19); + require(target.value() == 19, "value"); + }} +}} +"# + ), + ); + + let dump_path = prj.root().join("script_dump.json"); + cmd.forge_fuse().args([ + "script", + "script/DebugRemote.s.sol:DebugRemote", + "--fork-url", + rpc.as_str(), + "--debug", + "--dump", + dump_path.to_str().unwrap(), + ]); + cmd.assert_success(); + assert_debug_dump_identifies_contract(&dump_path, &deployed, "ScriptForkDebugTarget"); +}); + // Tests that the `run` command works correctly forgetest!(can_execute_script_command2, |prj, cmd| { let script = prj.add_source( diff --git a/crates/forge/tests/cli/test_cmd/mod.rs b/crates/forge/tests/cli/test_cmd/mod.rs index 4f7b8cfba58f1..c5dc47566daba 100644 --- a/crates/forge/tests/cli/test_cmd/mod.rs +++ b/crates/forge/tests/cli/test_cmd/mod.rs @@ -1,5 +1,6 @@ //! Contains various tests for `forge test`. +use crate::utils::assert_debug_dump_identifies_contract; use alloy_primitives::{Address, U256}; use anvil::{NodeConfig, spawn}; use foundry_test_utils::{ @@ -9,11 +10,7 @@ use foundry_test_utils::{ util::{OTHER_SOLC_VERSION, OutputExt, SOLC_VERSION}, }; use similar_asserts::assert_eq; -use std::{ - io::Write, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{io::Write, path::PathBuf, str::FromStr}; mod core; mod fuzz; @@ -2853,17 +2850,6 @@ contract ForkDebugTest {{ assert_debug_dump_identifies_contract(&table_dump_path, &deployed, "ForkDebugTarget"); }); -fn assert_debug_dump_identifies_contract(dump_path: &Path, address: &str, contract_name: &str) { - let dump: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(dump_path).unwrap()).unwrap(); - let identified = dump["contracts"]["identified_contracts"].as_object().unwrap(); - let target_identified = identified.iter().any(|(identified_address, name)| { - identified_address.eq_ignore_ascii_case(address) - && name.as_str().is_some_and(|name| name == contract_name) - }); - assert!(target_identified, "forked target was not identified in debugger dump: {identified:?}"); -} - forgetest_init!(test_assume_no_revert_with_data, |prj, cmd| { prj.update_config(|config| { config.fuzz.seed = Some(U256::from(111)); diff --git a/crates/forge/tests/cli/utils.rs b/crates/forge/tests/cli/utils.rs index 319fc5912ea78..58e96bc9de326 100644 --- a/crates/forge/tests/cli/utils.rs +++ b/crates/forge/tests/cli/utils.rs @@ -3,6 +3,7 @@ use alloy_chains::NamedChain; use alloy_primitives::Address; use alloy_signer_local::PrivateKeySigner; +use std::path::Path; /// Returns the current millis since unix epoch. /// @@ -182,6 +183,17 @@ pub fn parse_deployed_address(out: &str) -> Option { None } +pub fn assert_debug_dump_identifies_contract(dump_path: &Path, address: &str, contract_name: &str) { + let dump: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(dump_path).unwrap()).unwrap(); + let identified = dump["contracts"]["identified_contracts"].as_object().unwrap(); + let target_identified = identified.iter().any(|(identified_address, name)| { + identified_address.eq_ignore_ascii_case(address) + && name.as_str().is_some_and(|name| name == contract_name) + }); + assert!(target_identified, "forked target was not identified in debugger dump: {identified:?}"); +} + pub fn parse_verification_guid(out: &str) -> Option { let mut lines = out.lines().map(str::trim).filter(|line| !line.is_empty()); let line = lines.next()?; diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 52e334a1c62f4..b132604e112fd 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -168,6 +168,9 @@ impl PreExecutionState { setup_result.logs.extend(script_result.logs); setup_result.traces.extend(script_result.traces); setup_result.labeled_addresses.extend(script_result.labeled_addresses); + if self.args.debug { + setup_result.debug_bytecodes.extend(script_result.debug_bytecodes); + } setup_result.returned = script_result.returned; setup_result.exit_reason = script_result.exit_reason; setup_result.breakpoints = script_result.breakpoints; @@ -359,9 +362,15 @@ impl ExecutedState { ) .build(); - let mut identifier = TraceIdentifiers::new() - .with_local(known_contracts) - .with_external(&self.script_config.config, chain_id)?; + let use_debug_bytecodes = + self.args.debug && !self.execution_result.debug_bytecodes.is_empty(); + let mut identifier = if use_debug_bytecodes { + TraceIdentifiers::new() + .with_local_and_bytecodes(known_contracts, &self.execution_result.debug_bytecodes) + } else { + TraceIdentifiers::new().with_local(known_contracts) + } + .with_external(&self.script_config.config, chain_id)?; for (_, trace) in &self.execution_result.traces { decoder.identify(trace, &mut identifier); diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 3aa3570a767a6..b591a6210d75c 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -684,6 +684,8 @@ pub struct ScriptResult { pub gas_used: u64, pub labeled_addresses: AddressHashMap, #[serde(skip)] + pub debug_bytecodes: AddressHashMap, + #[serde(skip)] pub transactions: Option>, pub returned: Bytes, #[serde(skip)] @@ -701,6 +703,7 @@ impl Default for ScriptResult { traces: Default::default(), gas_used: Default::default(), labeled_addresses: Default::default(), + debug_bytecodes: Default::default(), transactions: Default::default(), returned: Default::default(), exit_reason: Default::default(), @@ -877,7 +880,8 @@ impl ScriptConfig { // (e.g. script deployment, setUp) use the correct fee token for Tempo networks. tx_env.set_fee_token(self.tempo.fee_token); - Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())) + Ok(ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone()) + .with_debug_bytecodes(debug)) } } diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 22b13e9b4300a..902ae799c943d 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -3,7 +3,7 @@ use crate::build::ScriptPredeployLibraries; use alloy_eips::eip7702::SignedAuthorization; use alloy_evm::revm::context::Transaction; use alloy_network::TransactionBuilder; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{Address, Bytes, U256, map::AddressHashMap}; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; use foundry_common::TransactionMaybeSigned; @@ -26,11 +26,34 @@ use std::collections::VecDeque; pub struct ScriptRunner { pub executor: Executor, pub evm_opts: EvmOpts, + collect_debug_bytecodes: bool, } impl ScriptRunner { pub const fn new(executor: Executor, evm_opts: EvmOpts) -> Self { - Self { executor, evm_opts } + Self { executor, evm_opts, collect_debug_bytecodes: false } + } + + pub const fn with_debug_bytecodes(mut self, collect_debug_bytecodes: bool) -> Self { + self.collect_debug_bytecodes = collect_debug_bytecodes; + self + } + + fn maybe_debug_bytecodes( + &self, + debug_bytecodes: AddressHashMap, + ) -> AddressHashMap { + if self.collect_debug_bytecodes { debug_bytecodes } else { Default::default() } + } + + fn extend_debug_bytecodes( + &self, + target: &mut AddressHashMap, + debug_bytecodes: AddressHashMap, + ) { + if self.collect_debug_bytecodes { + target.extend(debug_bytecodes); + } } /// Deploys the libraries and broadcast contract. Calls setUp method if requested. @@ -65,18 +88,25 @@ impl ScriptRunner { let mut library_transactions = VecDeque::new(); let mut traces = Traces::default(); + let mut debug_bytecodes: AddressHashMap = Default::default(); // Deploy libraries match libraries { ScriptPredeployLibraries::Default(libraries) => { for code in libraries { - let result = self + let RawCallResult { + traces: deploy_traces, + debug_bytecodes: deploy_debug_bytecodes, + .. + } = self .executor .deploy(self.evm_opts.sender, code.clone(), U256::ZERO, None) .expect("couldn't deploy library") .raw; - if let Some(deploy_traces) = result.traces { + self.extend_debug_bytecodes(&mut debug_bytecodes, deploy_debug_bytecodes); + + if let Some(deploy_traces) = deploy_traces { traces.push((TraceKind::Deployment, deploy_traces)); } @@ -102,7 +132,11 @@ impl ScriptRunner { continue; } let calldata = [salt.as_ref(), library.as_ref()].concat(); - let result = self + let RawCallResult { + traces: deploy_traces, + debug_bytecodes: deploy_debug_bytecodes, + .. + } = self .executor .transact_raw( self.evm_opts.sender, @@ -112,7 +146,9 @@ impl ScriptRunner { ) .expect("couldn't deploy library"); - if let Some(deploy_traces) = result.traces { + self.extend_debug_bytecodes(&mut debug_bytecodes, deploy_debug_bytecodes); + + if let Some(deploy_traces) = deploy_traces { traces.push((TraceKind::Deployment, deploy_traces)); } @@ -159,7 +195,13 @@ impl ScriptRunner { // Deploy an instance of the contract let DeployResult { address, - raw: RawCallResult { mut logs, traces: constructor_traces, .. }, + raw: + RawCallResult { + mut logs, + traces: constructor_traces, + debug_bytecodes: constructor_debug_bytecodes, + .. + }, } = self .executor .deploy(CALLER, code, U256::ZERO, None) @@ -175,6 +217,7 @@ impl ScriptRunner { } traces.extend(constructor_traces.map(|traces| (TraceKind::Deployment, traces))); + self.extend_debug_bytecodes(&mut debug_bytecodes, constructor_debug_bytecodes); // Optionally call the `setUp` function let (success, gas_used, labeled_addresses, transactions) = if setup { @@ -185,11 +228,13 @@ impl ScriptRunner { labels, logs: setup_logs, gas_used, + debug_bytecodes: setup_debug_bytecodes, transactions: setup_transactions, .. }) => { traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); logs.extend_from_slice(&setup_logs); + self.extend_debug_bytecodes(&mut debug_bytecodes, setup_debug_bytecodes); if let Some(txs) = setup_transactions { library_transactions.extend(txs); @@ -204,11 +249,13 @@ impl ScriptRunner { labels, logs: setup_logs, gas_used, + debug_bytecodes: setup_debug_bytecodes, transactions, .. } = err.raw; traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces))); logs.extend_from_slice(&setup_logs); + self.extend_debug_bytecodes(&mut debug_bytecodes, setup_debug_bytecodes); if let Some(txs) = transactions { library_transactions.extend(txs); @@ -230,6 +277,7 @@ impl ScriptRunner { success, gas_used, labeled_addresses, + debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes), transactions, logs, traces, @@ -273,7 +321,10 @@ impl ScriptRunner { value.unwrap_or(U256::ZERO), None, ); - let (address, RawCallResult { gas_used, logs, traces, exit_reason, .. }) = match res { + let ( + address, + RawCallResult { gas_used, logs, traces, debug_bytecodes, exit_reason, .. }, + ) = match res { Ok(DeployResult { address, raw }) => (address, raw), Err(EvmError::Execution(err)) => { let ExecutionErr { raw, reason } = *err; @@ -288,6 +339,7 @@ impl ScriptRunner { success: address != Address::ZERO, gas_used, logs, + debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes), // Manually adjust gas for the trace to add back the stipend/real used gas traces: traces .map(|traces| vec![(TraceKind::Execution, traces)]) @@ -348,15 +400,25 @@ impl ScriptRunner { } let RawCallResult { - result, reverted, logs, traces, labels, transactions, exit_reason, .. + result, + reverted, + logs, + traces, + labels, + transactions, + debug_bytecodes, + exit_reason, + cheatcodes, + .. } = res; - let breakpoints = res.cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); + let breakpoints = cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); Ok(ScriptResult { returned: result, success: !reverted, gas_used, logs, + debug_bytecodes: self.maybe_debug_bytecodes(debug_bytecodes), traces: traces .map(|traces| { // Manually adjust gas for the trace to add back the stipend/real used gas From 92eba2494f639d3118f8541c6a0de7969378552b Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:11:31 +0200 Subject: [PATCH 13/25] test: clarify expect emit mismatch output (#15194) test: clarify expect emit failure assertions --- crates/cheatcodes/src/test/expect.rs | 15 +++++++++++++++ crates/forge/tests/cli/failure_assertions.rs | 4 ++-- crates/forge/tests/cli/test_cmd/repros.rs | 4 ++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index 55709c268e16c..318ea8449a361 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -1172,6 +1172,17 @@ pub(crate) fn get_emit_mismatch_message( // 1. Different number of topics if actual.topics().len() != expected.topics().len() { + let expected_name = expected_decoded.and_then(|d| d.name.as_deref()).unwrap_or("log"); + let actual_name = actual_decoded.and_then(|d| d.name.as_deref()).unwrap_or("log"); + let expected_topics = checked_topic_count(expected, is_anonymous); + let actual_topics = checked_topic_count(actual, is_anonymous); + + if expected_name == actual_name { + return format!( + "{actual_name} indexed topic count mismatch: expected {expected_topics}, got {actual_topics}" + ); + } + return name_mismatched_logs(expected_decoded, actual_decoded); } @@ -1314,6 +1325,10 @@ fn name_mismatched_logs( format!("{actual_name} != expected {expected_name}") } +fn checked_topic_count(log: &RawLog, is_anonymous: bool) -> usize { + if is_anonymous { log.topics().len() } else { log.topics().len().saturating_sub(1) } +} + fn expect_safe_memory( state: &mut Cheatcodes, start: u64, diff --git a/crates/forge/tests/cli/failure_assertions.rs b/crates/forge/tests/cli/failure_assertions.rs index e9412f8df1ead..942ed6f854db9 100644 --- a/crates/forge/tests/cli/failure_assertions.rs +++ b/crates/forge/tests/cli/failure_assertions.rs @@ -209,7 +209,7 @@ forgetest!(flaky_expect_emit_tests_should_fail, |prj, cmd| { cmd.forge_fuse().args(["test", "--mc", "ExpectEmitFailureTest"]).assert_failure().stdout_eq(str![[r#"[COMPILING_FILES] with [SOLC_VERSION] ... [FAIL: E != expected A] testShouldFailCanMatchConsecutiveEvents() ([GAS]) -[FAIL: log != expected SomethingElse] testShouldFailDifferentIndexedParameters() ([GAS]) +[FAIL: SomethingElse indexed topic count mismatch: expected 1, got 0] testShouldFailDifferentIndexedParameters() ([GAS]) [FAIL: log != expected log] testShouldFailEmitOnlyAppliesToNextCall() ([GAS]) [FAIL: next call did not revert as expected] testShouldFailEmitWindowWithRevertDisallowed() ([GAS]) [FAIL: E != expected A] testShouldFailEventsOnTwoCalls() ([GAS]) @@ -235,7 +235,7 @@ Suite result: FAILED. 0 passed; 15 failed; 0 skipped; [ELAPSED] ... [FAIL: log != expected log] testShouldFailCountEmitsFromAddress() ([GAS]) [FAIL: log != expected log] testShouldFailCountLessEmits() ([GAS]) -[FAIL: log != expected Something] testShouldFailEmitSomethingElse() ([GAS]) +[FAIL: SomethingElse != expected Something] testShouldFailEmitSomethingElse() ([GAS]) [FAIL: log emitted but expected 0 times] testShouldFailNoEmit() ([GAS]) [FAIL: log emitted but expected 0 times] testShouldFailNoEmitFromAddress() ([GAS]) Suite result: FAILED. 0 passed; 5 failed; 0 skipped; [ELAPSED] diff --git a/crates/forge/tests/cli/test_cmd/repros.rs b/crates/forge/tests/cli/test_cmd/repros.rs index 1f7d6c4b73e84..1b2d0f30c1cd7 100644 --- a/crates/forge/tests/cli/test_cmd/repros.rs +++ b/crates/forge/tests/cli/test_cmd/repros.rs @@ -255,14 +255,14 @@ contract Issue6170Test is Test { Compiler run successful! Ran 1 test for test/Issue6170.t.sol:Issue6170Test -[FAIL: Values != expected Values] test() ([GAS]) +[FAIL: Values indexed topic count mismatch: expected 1, got 2] test() ([GAS]) Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] Ran 1 test suite [ELAPSED]: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/Issue6170.t.sol:Issue6170Test -[FAIL: Values != expected Values] test() ([GAS]) +[FAIL: Values indexed topic count mismatch: expected 1, got 2] test() ([GAS]) Encountered a total of 1 failing tests, 0 tests succeeded From 4b0f1f4b0b5bc11c8362fc144f7322c83df1bc47 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:56:15 +0200 Subject: [PATCH 14/25] chore(script): remove redundant debug bytecode guard (#15196) --- crates/script/src/execute.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index b132604e112fd..a6807f2af14a0 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -168,9 +168,7 @@ impl PreExecutionState { setup_result.logs.extend(script_result.logs); setup_result.traces.extend(script_result.traces); setup_result.labeled_addresses.extend(script_result.labeled_addresses); - if self.args.debug { - setup_result.debug_bytecodes.extend(script_result.debug_bytecodes); - } + setup_result.debug_bytecodes.extend(script_result.debug_bytecodes); setup_result.returned = script_result.returned; setup_result.exit_reason = script_result.exit_reason; setup_result.breakpoints = script_result.breakpoints; From 27717f4a1bbde3be3b456a04edc8b3cce2f7798d Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:34:26 +0200 Subject: [PATCH 15/25] feat(lint): add `incorrect_modifier` (#15201) --- crates/lint/README.md | 1 + crates/lint/docs/incorrect-modifier.md | 47 +++++ .../sol/codesize/unwrapped_modifier_logic.rs | 52 ++++- crates/lint/src/sol/low/incorrect_modifier.rs | 192 ++++++++++++++++++ crates/lint/src/sol/low/mod.rs | 4 + crates/lint/testdata/IncorrectModifier.sol | 152 ++++++++++++++ crates/lint/testdata/IncorrectModifier.stderr | 88 ++++++++ 7 files changed, 531 insertions(+), 5 deletions(-) create mode 100644 crates/lint/docs/incorrect-modifier.md create mode 100644 crates/lint/src/sol/low/incorrect_modifier.rs create mode 100644 crates/lint/testdata/IncorrectModifier.sol create mode 100644 crates/lint/testdata/IncorrectModifier.stderr diff --git a/crates/lint/README.md b/crates/lint/README.md index 1623eff7bb1b3..157dcb6ae4ac7 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -37,6 +37,7 @@ It helps enforce best practices and improve code quality within Foundry projects - `block-timestamp`: Warns when `block.timestamp` is used in a comparison, as it may be manipulated by validators. - `calls-loop`: External calls inside loops can cause denial-of-service if a call reverts or exhausts gas. - `delegatecall-loop`: Payable functions should not use `delegatecall` inside a loop. + - `incorrect-modifier`: Modifiers should not be able to finish without executing `_` or reverting. - `missing-events-access-control`: Access control changes should emit events. - `missing-zero-check`: Address parameter is used in a state write or value transfer without a zero-address check. - `reentrancy-events`: Events emitted after external calls can be reordered or fabricated by a reentrant callee and mislead off-chain consumers. diff --git a/crates/lint/docs/incorrect-modifier.md b/crates/lint/docs/incorrect-modifier.md new file mode 100644 index 0000000000000..2429a1fdd8806 --- /dev/null +++ b/crates/lint/docs/incorrect-modifier.md @@ -0,0 +1,47 @@ +# Incorrect Modifier + +**Severity**: `Low` +**ID**: `incorrect-modifier` + +Reports modifiers that can finish without executing the modified function body or reverting. + +## What it does + +Flags Solidity modifiers where at least one path can complete before reaching the `_` placeholder. +Paths that revert before `_` are not flagged. + +Only a path that provably reverts or otherwise halts is treated as safe: an explicit `revert` +statement or `revert(...)` builtin call, and the Yul halting builtins (`revert`/`invalid` to fail, +and `return`/`stop`/`selfdestruct`, which halt *successfully* without running the function body and +are therefore flagged). A regular function call that *might* revert (e.g. `require(...)`, an +internal helper, or an external call) does not count, so a path that performs such a call and then +returns without reaching `_` is still flagged. This is intentionally stricter than some other +tools. + +## Why is this bad? + +A modifier that falls through before `_` silently skips the function body. This can make calls look +successful even though the protected action never ran. + +## Example + +### Bad + +```solidity +modifier onlyWhenEnabled() { + if (enabled) { + _; + } +} +``` + +### Good + +```solidity +modifier onlyWhenEnabled() { + if (!enabled) { + revert Disabled(); + } + _; +} +``` diff --git a/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs b/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs index 994f66ab1865f..754c0f93f7962 100644 --- a/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs +++ b/crates/lint/src/sol/codesize/unwrapped_modifier_logic.rs @@ -1,7 +1,7 @@ use super::UnwrappedModifierLogic; use crate::{ linter::{LateLintPass, LintContext, Suggestion}, - sol::{Severity, SolLint}, + sol::{Severity, SolLint, low::incorrect_modifier}, }; use solar::{ ast, @@ -29,12 +29,27 @@ impl<'hir> LateLintPass<'hir> for UnwrappedModifierLogic { _ => return, }; + if incorrect_modifier::block_outcome(*body).can_skip_placeholder() { + return; + } + + // Only handle modifiers with exactly one placeholder, *and* require it to be top-level. + // Counting recursively (rather than just top-level statements) ensures a placeholder nested + // inside an `if`/loop/etc. is never extracted into a helper function, which would produce + // an invalid, behavior-changing rewrite. + if count_placeholders(body.stmts) != 1 { + return; + } + let Some(idx) = + body.stmts.iter().position(|s| matches!(s.kind, hir::StmtKind::Placeholder)) + else { + // The single placeholder is nested; splitting it out would be unsafe. + return; + }; + // Split statements into before and after the placeholder `_`. let stmts = body.stmts[..].as_ref(); - let (before, after) = stmts - .iter() - .position(|s| matches!(s.kind, hir::StmtKind::Placeholder)) - .map_or((stmts, &[][..]), |idx| (&stmts[..idx], &stmts[idx + 1..])); + let (before, after) = (&stmts[..idx], &stmts[idx + 1..]); // Generate a fix suggestion if the modifier logic should be wrapped. if let Some(suggestion) = self.get_snippet(ctx, hir, func, before, after) { @@ -181,3 +196,30 @@ impl UnwrappedModifierLogic { ) } } + +/// Recursively counts placeholder (`_`) statements within a list of statements, descending into +/// nested blocks, conditionals, loops, `try`/`catch`, and Yul `switch` cases. +fn count_placeholders(stmts: &[hir::Stmt<'_>]) -> usize { + stmts.iter().map(count_placeholders_in_stmt).sum() +} + +fn count_placeholders_in_stmt(stmt: &hir::Stmt<'_>) -> usize { + match &stmt.kind { + hir::StmtKind::Placeholder => 1, + hir::StmtKind::Block(block) + | hir::StmtKind::UncheckedBlock(block) + | hir::StmtKind::AssemblyBlock(block) + | hir::StmtKind::Loop(block, _) => count_placeholders(block.stmts), + hir::StmtKind::If(_, then_stmt, else_stmt) => { + count_placeholders_in_stmt(then_stmt) + + else_stmt.map_or(0, |s| count_placeholders_in_stmt(s)) + } + hir::StmtKind::Try(try_stmt) => { + try_stmt.clauses.iter().map(|clause| count_placeholders(clause.block.stmts)).sum() + } + hir::StmtKind::Switch(switch) => { + switch.cases.iter().map(|case| count_placeholders(case.body.stmts)).sum() + } + _ => 0, + } +} diff --git a/crates/lint/src/sol/low/incorrect_modifier.rs b/crates/lint/src/sol/low/incorrect_modifier.rs new file mode 100644 index 0000000000000..48e28062ebf2f --- /dev/null +++ b/crates/lint/src/sol/low/incorrect_modifier.rs @@ -0,0 +1,192 @@ +use super::IncorrectModifier; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast, + sema::{ + Gcx, Hir, + builtins::Builtin, + hir::{Block, Expr, ExprKind, Function, LoopSource, Res, Stmt, StmtKind}, + }, +}; + +declare_forge_lint!( + INCORRECT_MODIFIER, + Severity::Low, + "incorrect-modifier", + "modifier can finish without executing the modified function" +); + +impl<'hir> LateLintPass<'hir> for IncorrectModifier { + fn check_function( + &mut self, + ctx: &LintContext, + _gcx: Gcx<'hir>, + _hir: &'hir Hir<'hir>, + func: &'hir Function<'hir>, + ) { + let (ast::FunctionKind::Modifier, Some(body)) = (func.kind, func.body) else { + return; + }; + + if block_outcome(body).can_skip_placeholder() { + ctx.emit(&INCORRECT_MODIFIER, func.span); + } + } +} + +/// Summary of how control flow can leave a statement or block *without* having executed the +/// placeholder (`_`) or reverted. +/// +/// Each flag tracks whether there is at least one such path. If every path reaches `_` or reverts, +/// all flags are `false` ([`Outcome::COVERED`]). +#[derive(Clone, Copy)] +pub(crate) struct Outcome { + /// Control can reach the end of the construct normally and continue to the next statement. + falls_through: bool, + /// Control can exit the modifier via `return` before reaching `_`. + returns: bool, + /// Control can exit the enclosing loop via `break` before reaching `_`. + breaks: bool, + /// Control can jump to the enclosing loop's next iteration via `continue` before reaching `_`. + continues: bool, +} + +impl Outcome { + /// Every path reaches `_` or reverts. + const COVERED: Self = + Self { falls_through: false, returns: false, breaks: false, continues: false }; + const FALLTHROUGH: Self = Self { falls_through: true, ..Self::COVERED }; + const RETURNS: Self = Self { returns: true, ..Self::COVERED }; + const BREAKS: Self = Self { breaks: true, ..Self::COVERED }; + const CONTINUES: Self = Self { continues: true, ..Self::COVERED }; + + /// Whether the modifier body can finish without executing `_`. Only fall-through and `return` + /// reach the modifier's end; `break`/`continue` are always consumed by an enclosing loop. + pub(crate) const fn can_skip_placeholder(self) -> bool { + self.falls_through || self.returns + } + + const fn merge(self, other: Self) -> Self { + Self { + falls_through: self.falls_through || other.falls_through, + returns: self.returns || other.returns, + breaks: self.breaks || other.breaks, + continues: self.continues || other.continues, + } + } +} + +pub(crate) fn block_outcome(block: Block<'_>) -> Outcome { + let mut outcome = Outcome::FALLTHROUGH; + for stmt in block.stmts { + // Once a statement cannot fall through, the rest of the block is unreachable. + if !outcome.falls_through { + return outcome; + } + let stmt_outcome = stmt_outcome(stmt); + outcome = Outcome { + falls_through: stmt_outcome.falls_through, + returns: outcome.returns || stmt_outcome.returns, + breaks: outcome.breaks || stmt_outcome.breaks, + continues: outcome.continues || stmt_outcome.continues, + }; + } + outcome +} + +fn stmt_outcome(stmt: &Stmt<'_>) -> Outcome { + match &stmt.kind { + StmtKind::Placeholder => Outcome::COVERED, + StmtKind::Return(_) => Outcome::RETURNS, + StmtKind::Break => Outcome::BREAKS, + StmtKind::Continue => Outcome::CONTINUES, + StmtKind::Expr(expr) => call_outcome(expr).unwrap_or(Outcome::FALLTHROUGH), + StmtKind::Revert(_) => Outcome::COVERED, + StmtKind::Block(block) + | StmtKind::UncheckedBlock(block) + | StmtKind::AssemblyBlock(block) => block_outcome(*block), + StmtKind::If(_, then_stmt, else_stmt) => { + let then_outcome = stmt_outcome(then_stmt); + let else_outcome = else_stmt.map_or(Outcome::FALLTHROUGH, stmt_outcome); + then_outcome.merge(else_outcome) + } + StmtKind::Loop(block, source) => { + // `for`/`while`/`do-while` are all desugared to a `Loop` whose body holds the condition + // as a synthetic `else break`. The loop can be left (and thus fall through to the + // following statement) via a `break`, including that synthetic condition break; a loop + // without any `break` (e.g. `for (;;)`) never falls through. For `do-while` the + // condition sits *after* the body, so a `continue` in the body also reaches it and can + // exit the loop. `break`/`continue` are otherwise consumed by the loop; only `return` + // keeps escaping toward the modifier's end. + let body = block_outcome(*block); + let falls_through = + body.breaks || (matches!(source, LoopSource::DoWhile) && body.continues); + Outcome { falls_through, returns: body.returns, ..Outcome::COVERED } + } + StmtKind::Try(try_stmt) => { + // Every execution enters exactly one clause (the `returns` clause on success or a + // matching `catch`), or the call reverts uncaught. There is no implicit fall-through + // path that skips all clauses, so start from `COVERED`. + let mut outcome = Outcome::COVERED; + for clause in try_stmt.clauses { + outcome = outcome.merge(block_outcome(clause.block)); + } + outcome + } + StmtKind::Switch(switch) => { + // A Yul `switch` value that matches no `case` falls through unless a `default` clause + // is present (stored last, with no constant). + let has_default = switch.cases.last().is_some_and(|case| case.constant.is_none()); + let mut outcome = if has_default { Outcome::COVERED } else { Outcome::FALLTHROUGH }; + for case in switch.cases { + outcome = outcome.merge(block_outcome(case.body)); + } + outcome + } + StmtKind::DeclSingle(_) + | StmtKind::DeclMulti(_, _) + | StmtKind::Emit(_) + | StmtKind::Err(_) => Outcome::FALLTHROUGH, + } +} + +/// Classifies a statement-level call expression that terminates the current path before reaching +/// `_`, if any. Covers both the Solidity `revert`/`revert(...)` builtins and the Yul halting +/// builtins reachable when recursing into an `assembly { .. }` block. +/// +/// - Failing halts (`revert`, Yul `revert`/`invalid`) leave every path either reverting or reaching +/// `_`, so they are [`Outcome::COVERED`] (not flagged). +/// - Successful halts (Yul `return`/`stop`, `selfdestruct`) let the surrounding call finish +/// *without* running the modified function body, which is exactly what this lint flags, so they +/// behave like a `return` ([`Outcome::RETURNS`]). +fn call_outcome(expr: &Expr<'_>) -> Option { + let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return None }; + let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return None }; + resolutions.iter().find_map(|res| match res { + Res::Builtin( + Builtin::Revert | Builtin::RevertMsg | Builtin::YulRevert | Builtin::YulInvalid, + ) => Some(Outcome::COVERED), + Res::Builtin(Builtin::Require | Builtin::Assert) + if args.exprs().next().is_some_and(is_literal_false) => + { + Some(Outcome::COVERED) + } + Res::Builtin( + Builtin::YulReturn + | Builtin::YulStop + | Builtin::YulSelfdestruct + | Builtin::Selfdestruct, + ) => Some(Outcome::RETURNS), + _ => None, + }) +} + +fn is_literal_false(expr: &Expr<'_>) -> bool { + matches!( + &expr.peel_parens().kind, + ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Bool(false)) + ) +} diff --git a/crates/lint/src/sol/low/mod.rs b/crates/lint/src/sol/low/mod.rs index ec99565a73e56..ce516419799e6 100644 --- a/crates/lint/src/sol/low/mod.rs +++ b/crates/lint/src/sol/low/mod.rs @@ -9,6 +9,9 @@ use calls_loop::CALLS_LOOP; mod delegatecall_loop; use delegatecall_loop::DELEGATECALL_LOOP; +pub(crate) mod incorrect_modifier; +use incorrect_modifier::INCORRECT_MODIFIER; + mod msg_value_loop; use msg_value_loop::MSG_VALUE_LOOP; @@ -33,6 +36,7 @@ register_lints!( (BlockTimestamp, late, (BLOCK_TIMESTAMP)), (CallsLoop, late, (CALLS_LOOP)), (DelegatecallLoop, late, (DELEGATECALL_LOOP)), + (IncorrectModifier, late, (INCORRECT_MODIFIER)), (MsgValueLoop, late, (MSG_VALUE_LOOP)), (MissingEventsAccessControl, late, (MISSING_EVENTS_ACCESS_CONTROL)), (MissingEventsArithmetic, late, (MISSING_EVENTS_ARITHMETIC)), diff --git a/crates/lint/testdata/IncorrectModifier.sol b/crates/lint/testdata/IncorrectModifier.sol new file mode 100644 index 0000000000000..68ea0ba9cf3ce --- /dev/null +++ b/crates/lint/testdata/IncorrectModifier.sol @@ -0,0 +1,152 @@ +//@compile-flags: --only-lint incorrect-modifier + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract IncorrectModifier { + bool enabled; + bool paused; + + modifier conditionalPlaceholder() { //~WARN: modifier can finish without executing the modified function + if (enabled) { + _; + } + } + + modifier returnsBeforePlaceholder() { //~WARN: modifier can finish without executing the modified function + if (paused) { + return; + } + _; + } + + modifier nestedConditionalPlaceholder() { //~WARN: modifier can finish without executing the modified function + if (enabled) { + if (!paused) { + _; + } + } else { + revert("disabled"); + } + } + + modifier loopOnlyPlaceholder() { //~WARN: modifier can finish without executing the modified function + while (enabled) { + _; + } + } + + modifier alwaysExecutes() { + _; + } + + modifier revertsOrExecutes() { + if (!enabled) { + revert("disabled"); + } + _; + } + + modifier conditionalElseReverts() { + if (enabled) { + _; + } else { + revert("disabled"); + } + } + + modifier requireFalseOrExecutes() { + if (paused) { + require(false, "disabled"); + return; + } + _; + } + + modifier assertFalseOrExecutes() { + if (paused) { + assert(false); + return; + } + _; + } + + modifier afterPlaceholderIsIrrelevant() { + _; + if (paused) { + return; + } + } + + // A `do/while` body always runs at least once, so `_` is always reached. + modifier doWhileAlwaysRuns() { + do { + _; + } while (enabled); + } + + // A loop before an unconditional placeholder still always reaches `_`. + modifier loopBeforePlaceholder() { + while (enabled) { + enabled = false; + } + _; + } + + modifier forLoopBeforePlaceholder() { + for (uint256 i = 0; i < 3; i++) { + enabled = false; + } + _; + } + + // Every try/catch path reaches `_` or reverts. + modifier tryCatchCovered() { + try this.poke() { + _; + } catch { + revert("failed"); + } + } + + // A `break` that skips `_` with no later placeholder is still flagged. + modifier breakSkipsPlaceholder() { //~WARN: modifier can finish without executing the modified function + while (enabled) { + if (paused) { + break; + } + _; + } + } + + // A `do/while` body runs once, but a `continue` reaches the trailing condition, which can then + // exit the loop before `_`. + modifier doWhileContinueSkipsPlaceholder() { //~WARN: modifier can finish without executing the modified function + do { + if (paused) { + continue; + } + _; + } while (enabled); + } + + // Assembly that always reverts before `_` reaches no normal exit, so it is not flagged. + modifier yulRevertOrExecutes() { + if (!enabled) { + assembly { + revert(0, 0) + } + } + _; + } + + // A `return(...)` in assembly halts successfully without running `_`, so it is flagged. + modifier yulReturnBeforePlaceholder() { //~WARN: modifier can finish without executing the modified function + assembly { + return(0, 0) + } + _; + } + + function poke() external {} +} diff --git a/crates/lint/testdata/IncorrectModifier.stderr b/crates/lint/testdata/IncorrectModifier.stderr new file mode 100644 index 0000000000000..8f9555f6a93cb --- /dev/null +++ b/crates/lint/testdata/IncorrectModifier.stderr @@ -0,0 +1,88 @@ +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier conditionalPlaceholder() { +LL │ ┃ if (enabled) { +LL │ ┃ _; +LL │ ┃ } +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier returnsBeforePlaceholder() { +LL │ ┃ if (paused) { +LL │ ┃ return; + ‡ ┃ +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier nestedConditionalPlaceholder() { +LL │ ┃ if (enabled) { +LL │ ┃ if (!paused) { +LL │ ┃ _; + ‡ ┃ +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier loopOnlyPlaceholder() { +LL │ ┃ while (enabled) { +LL │ ┃ _; +LL │ ┃ } +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier breakSkipsPlaceholder() { +LL │ ┃ while (enabled) { +LL │ ┃ if (paused) { +LL │ ┃ break; + ‡ ┃ +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier doWhileContinueSkipsPlaceholder() { +LL │ ┃ do { +LL │ ┃ if (paused) { +LL │ ┃ continue; + ‡ ┃ +LL │ ┃ } while (enabled); +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + +warning[incorrect-modifier]: modifier can finish without executing the modified function + ╭▸ ROOT/testdata/IncorrectModifier.sol:LL:CC + │ +LL │ ┏ modifier yulReturnBeforePlaceholder() { +LL │ ┃ assembly { +LL │ ┃ return(0, 0) + ‡ ┃ +LL │ ┃ } + │ ┗━━━━━┛ + │ + ╰ help: https://getfoundry.sh/forge/linting/incorrect-modifier + From 1b68c865dce7d2673c9096c40b5b986da916e966 Mon Sep 17 00:00:00 2001 From: strmfos <155266597+strmfos@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:47:53 +0300 Subject: [PATCH 16/25] fix(chisel): remove unused `latest` variable in next_cached_session (#13923) * fix(chisel): remove unused `latest` variable in next_cached_session * update --------- Co-authored-by: figtracer --- crates/chisel/src/session.rs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/crates/chisel/src/session.rs b/crates/chisel/src/session.rs index 87eb1fb140f10..c3a619c6ff7f0 100644 --- a/crates/chisel/src/session.rs +++ b/crates/chisel/src/session.rs @@ -111,26 +111,8 @@ impl ChiselSession { /// Optionally, returns a tuple containing the next cached session's id and file name. pub fn next_cached_session() -> Result<(String, String)> { let cache_dir = Self::cache_dir()?; - let mut entries = std::fs::read_dir(&cache_dir)?; - - // If there are no existing cached sessions, just create the first one: "chisel-0.json" - let mut latest = if let Some(e) = entries.next() { - e? - } else { - return Ok((String::from("0"), format!("{cache_dir}chisel-0.json"))); - }; - - let mut session_num = 1; - // Get the latest cached session - for entry in entries { - let entry = entry?; - if entry.metadata()?.modified()? >= latest.metadata()?.modified()? { - latest = entry; - } - - // Increase session_num counter rather than cloning the iterator and using `.count` - session_num += 1; - } + let entries = std::fs::read_dir(&cache_dir)?; + let session_num = entries.filter(Result::is_ok).count(); Ok((format!("{session_num}"), format!("{cache_dir}chisel-{session_num}.json"))) } From 0e526570be2a832ffcfb52f489788cd5751cb130 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:50:12 +0200 Subject: [PATCH 17/25] feat(fuzz): update invariant pulse metrics (#15198) * feat(fuzz): update invariant pulse metrics * fix: according fig's review --- crates/evm/evm/src/executors/invariant/mod.rs | 160 ++++++++++++------ 1 file changed, 109 insertions(+), 51 deletions(-) diff --git a/crates/evm/evm/src/executors/invariant/mod.rs b/crates/evm/evm/src/executors/invariant/mod.rs index 7ba300e3d0dbb..7efb8f64b1ad3 100644 --- a/crates/evm/evm/src/executors/invariant/mod.rs +++ b/crates/evm/evm/src/executors/invariant/mod.rs @@ -160,12 +160,12 @@ struct InvariantThroughputMetrics { } impl InvariantThroughputMetrics { - fn tx_per_sec(self, elapsed: Duration) -> f64 { - rate_per_sec(self.total_txs as f64, elapsed) + fn tps(self, elapsed: Duration) -> f64 { + round_rate_for_progress(rate_per_sec(self.total_txs as f64, elapsed)) } - fn gas_per_sec(self, elapsed: Duration) -> f64 { - rate_per_sec(self.total_gas as f64, elapsed) + fn gps(self, elapsed: Duration) -> f64 { + round_rate_for_progress(rate_per_sec(self.total_gas as f64, elapsed)) } } @@ -268,6 +268,10 @@ fn rate_per_sec(total: f64, elapsed: Duration) -> f64 { if elapsed_secs > 0.0 { total / elapsed_secs } else { 0.0 } } +fn round_rate_for_progress(rate: f64) -> f64 { + (rate * 100.0).round() / 100.0 +} + /// Tracks invariant failure counts during a campaign. #[derive(Clone, Debug, Default)] struct InvariantFailureMetrics { @@ -312,6 +316,16 @@ fn record_new_invariant_failures( } } +struct InvariantProgressContext<'a> { + timestamp_secs: u64, + contract_name: &'a str, + optimization_best: Option, + throughput: InvariantThroughputMetrics, + elapsed: Duration, + worker_id: u32, + worker_count: usize, +} + /// Builds the machine-readable invariant progress payload emitted during a /// campaign. /// @@ -319,35 +333,32 @@ fn record_new_invariant_failures( /// derived throughput fields so downstream benchmark tooling can consume a /// single JSON event shape. fn build_invariant_progress_json( - timestamp_secs: u64, - invariant_name: &str, + context: InvariantProgressContext<'_>, corpus_metrics: &M, - optimization_best: Option, - throughput: InvariantThroughputMetrics, failure_metrics: &InvariantFailureMetrics, - elapsed: Duration, ) -> serde_json::Value { let mut metrics = serde_json::to_value(corpus_metrics).unwrap_or_default(); if let Some(obj) = metrics.as_object_mut() { - obj.insert("failures".to_string(), json!(failure_metrics.failures)); - obj.insert("unique_failures".to_string(), json!(failure_metrics.unique_failures.len())); - // Surface unique handler-side assertion bugs in live progress, separate from - // invariant predicate violations counted by `failures`. - obj.insert("broken_handlers".to_string(), json!(failure_metrics.broken_handlers)); + obj.insert("broken_invariants".to_string(), json!(failure_metrics.unique_failures.len())); + obj.insert("broken_assertions".to_string(), json!(failure_metrics.broken_handlers)); } let mut payload = json!({ - "timestamp": timestamp_secs, + "timestamp": context.timestamp_secs, "event": "pulse", - "invariant": invariant_name, + "contract": context.contract_name, "metrics": metrics, - "total_txs": throughput.total_txs, - "total_gas": throughput.total_gas, - "tx_per_sec": throughput.tx_per_sec(elapsed), - "gas_per_sec": throughput.gas_per_sec(elapsed), + "total_txs": context.throughput.total_txs, + "total_gas": context.throughput.total_gas, + "tps": context.throughput.tps(context.elapsed), + "gps": context.throughput.gps(context.elapsed), + "worker": { + "id": context.worker_id, + "count": context.worker_count, + }, }); - if let Some(best) = optimization_best { + if let Some(best) = context.optimization_best { payload["optimization_best"] = json!(best.to_string()); } @@ -715,6 +726,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { corpus_seed .clone_for_worker(worker_plan.worker_id as usize, actual_worker_count), corpus_persistence, + actual_worker_count, gas_report_samples, ); debug!("finished in {:?}", timer.elapsed()); @@ -741,6 +753,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { campaign_seed, corpus_seed.clone(), corpus_persistence, + actual_worker_count, gas_report_samples, )?] }; @@ -790,6 +803,7 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { campaign_seed: InvariantCampaignSeed, corpus_seed: WorkerCorpusSeed, corpus_persistence: InvariantCorpusPersistence, + worker_count: usize, gas_report_samples: usize, ) -> Result { // Note: invariant function signatures (no inputs) are validated upstream in the @@ -1196,13 +1210,17 @@ impl<'a, FEN: FoundryEvmNetwork> InvariantExecutor<'a, FEN> { let throughput = InvariantThroughputMetrics { total_txs, total_gas }; // Display corpus metrics inline as JSON. let metrics = build_invariant_progress_json( - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), - &invariant_contract.anchor().name, + InvariantProgressContext { + timestamp_secs: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + contract_name: invariant_contract.name, + optimization_best: invariant_test.test_data.optimization_best_value, + throughput, + elapsed: campaign_state.elapsed(), + worker_id: plan.worker_id, + worker_count, + }, &corpus_manager.metrics, - invariant_test.test_data.optimization_best_value, - throughput, &failure_metrics, - campaign_state.elapsed(), ); let _ = sh_println!("{}", serde_json::to_string(&metrics)?); } @@ -1962,23 +1980,33 @@ mod tests { let throughput = InvariantThroughputMetrics { total_txs: 2, total_gas: 50 }; let payload = build_invariant_progress_json( - 123, - "invariant_balance", + InvariantProgressContext { + timestamp_secs: 123, + contract_name: "InvariantContract", + optimization_best: Some(I256::try_from(42).unwrap()), + throughput, + elapsed: Duration::from_secs(10), + worker_id: 1, + worker_count: 4, + }, &json!({ "corpus_count": 7 }), - Some(I256::try_from(42).unwrap()), - throughput, &InvariantFailureMetrics::default(), - Duration::from_secs(10), ); assert_eq!(payload["timestamp"], json!(123)); - assert_eq!(payload["invariant"], json!("invariant_balance")); + assert_eq!(payload["contract"], json!("InvariantContract")); + assert!(payload.get("invariant").is_none()); assert_eq!(payload["metrics"]["corpus_count"], json!(7)); - assert_eq!(payload["metrics"]["broken_handlers"], json!(0)); + assert_eq!(payload["metrics"]["broken_assertions"], json!(0)); + assert!(payload["metrics"].get("broken_handlers").is_none()); assert_eq!(payload["total_txs"], json!(2)); assert_eq!(payload["total_gas"], json!(50)); - assert!((payload["tx_per_sec"].as_f64().unwrap() - 0.2).abs() < 1e-12); - assert!((payload["gas_per_sec"].as_f64().unwrap() - 5.0).abs() < 1e-12); + assert_eq!(payload["tps"], json!(0.2)); + assert_eq!(payload["gps"], json!(5.0)); + assert!(payload.get("tx_per_sec").is_none()); + assert!(payload.get("gas_per_sec").is_none()); + assert_eq!(payload["worker"]["id"], json!(1)); + assert_eq!(payload["worker"]["count"], json!(4)); assert_eq!(payload["optimization_best"], json!("42")); } @@ -2107,22 +2135,46 @@ mod tests { let throughput = InvariantThroughputMetrics { total_txs: 1, total_gas: 21_000 }; let payload = build_invariant_progress_json( - 456, - "invariant_zero_elapsed", + InvariantProgressContext { + timestamp_secs: 456, + contract_name: "invariant_zero_elapsed", + optimization_best: None, + throughput, + elapsed: Duration::ZERO, + worker_id: 0, + worker_count: 1, + }, &json!({ "corpus_count": 1 }), - None, - throughput, &InvariantFailureMetrics::default(), - Duration::ZERO, ); - assert_eq!(payload["tx_per_sec"], json!(0.0)); - assert_eq!(payload["gas_per_sec"], json!(0.0)); + assert_eq!(payload["tps"], json!(0.0)); + assert_eq!(payload["gps"], json!(0.0)); assert!(payload.get("optimization_best").is_none()); } #[test] - fn invariant_progress_json_includes_failure_counts() { + fn invariant_progress_json_rounds_fractional_rates() { + let payload = build_invariant_progress_json( + InvariantProgressContext { + timestamp_secs: 456, + contract_name: "TestContract", + optimization_best: None, + throughput: InvariantThroughputMetrics { total_txs: 1, total_gas: 1 }, + elapsed: Duration::from_secs(3), + worker_id: 0, + worker_count: 1, + }, + &json!({ "corpus_count": 1 }), + &InvariantFailureMetrics::default(), + ); + + assert_eq!(payload["tps"], json!(0.33)); + assert_eq!(payload["gps"], json!(0.33)); + } + + #[test] + fn invariant_progress_json_includes_broken_counts() { let mut failure_metrics = InvariantFailureMetrics::default(); failure_metrics.record_failure("invariant_a", "TestContract", "revert"); failure_metrics.record_failure("invariant_a", "TestContract", "revert"); @@ -2130,18 +2182,24 @@ mod tests { failure_metrics.broken_handlers = 7; let payload = build_invariant_progress_json( - 789, - "invariant_a", + InvariantProgressContext { + timestamp_secs: 789, + contract_name: "TestContract", + optimization_best: None, + throughput: InvariantThroughputMetrics::default(), + elapsed: Duration::from_secs(1), + worker_id: 0, + worker_count: 1, + }, &json!({ "corpus_count": 5 }), - None, - InvariantThroughputMetrics::default(), &failure_metrics, - Duration::from_secs(1), ); - assert_eq!(payload["metrics"]["failures"], json!(3)); - assert_eq!(payload["metrics"]["unique_failures"], json!(2)); - assert_eq!(payload["metrics"]["broken_handlers"], json!(7)); + assert!(payload["metrics"].get("failures").is_none()); + assert!(payload["metrics"].get("unique_failures").is_none()); + assert_eq!(payload["metrics"]["broken_invariants"], json!(2)); + assert_eq!(payload["metrics"]["broken_assertions"], json!(7)); + assert!(payload["metrics"].get("broken_handlers").is_none()); } #[test] From de46fff372220d661a11bb3228a3991ad2f680fc Mon Sep 17 00:00:00 2001 From: figtracer Date: Wed, 17 Jun 2026 11:34:56 +0100 Subject: [PATCH 18/25] fix(debugger): match internal variables by signature (#15200) --- crates/debugger/src/tui/draw.rs | 110 ++++++++++++++++---- crates/evm/traces/src/debug/mod.rs | 55 +++++++++- crates/evm/traces/src/folded_stack_trace.rs | 50 +++++++++ crates/forge/tests/cli/test_cmd/mod.rs | 98 ++++++++++++++++- crates/forge/tests/cli/test_optimizer.rs | 2 +- 5 files changed, 285 insertions(+), 30 deletions(-) diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index b5ef2cbea6689..2a3bacc1ce703 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -8,7 +8,7 @@ use foundry_common::fmt::format_token; use foundry_compilers::artifacts::sourcemap::SourceElement; use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses}; use foundry_evm_traces::debug::{ - DebugSourceScope, DebugVariable, SourceData, decode_step_parameters, + DebugSourceScope, DebugVariable, SourceData, decode_step_parameters, function_signature, }; use ratatui::{ Frame, @@ -876,9 +876,17 @@ fn variable_name(variable: &DebugVariable, index: usize, fallback_prefix: &str) fn decoded_internal_name_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool { if let Some((contract_name, function_name)) = decoded_name.rsplit_once("::") { - return contract_name == scope.contract_name && function_name == scope.function_name; + return contract_name == scope.contract_name + && decoded_function_matches(function_name, scope); } - decoded_name == scope.function_name + decoded_function_matches(decoded_name, scope) +} + +fn decoded_function_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool { + if decoded_name == scope.function_name { + return true; + } + scope_function_signature(scope).as_deref().is_some_and(|signature| decoded_name == signature) } fn decode_external_parameter_values( @@ -914,20 +922,6 @@ fn function_selector(function_name: &str, types: &[DynSolType]) -> [u8; 4] { keccak256(signature.as_bytes())[..4].try_into().unwrap() } -fn function_signature(function_name: &str, types: &[DynSolType]) -> String { - let mut signature = String::new(); - signature.push_str(function_name); - signature.push('('); - for (i, ty) in types.iter().enumerate() { - if i > 0 { - signature.push(','); - } - signature.push_str(&ty.sol_type_name()); - } - signature.push(')'); - signature -} - fn decode_abi_sequence(types: &[DynSolType], data: &[u8]) -> Option> { if types.is_empty() { return Some(Vec::new()); @@ -1116,13 +1110,18 @@ mod tests { } fn internal_call_step(end_step: usize, return_data: Vec) -> CallTraceStep { + internal_call_step_named("DebugMe::foo", end_step, Some(Vec::new()), Some(return_data)) + } + + fn internal_call_step_named( + func_name: &str, + end_step: usize, + args: Option>, + return_data: Option>, + ) -> CallTraceStep { let mut step = trace_step(Vec::new()); step.decoded = Some(Box::new(DecodedTraceStep::InternalCall( - DecodedInternalCall { - func_name: "DebugMe::foo".to_string(), - args: Some(Vec::new()), - return_data: Some(return_data), - }, + DecodedInternalCall { func_name: func_name.to_string(), args, return_data }, end_step, ))); step @@ -1242,6 +1241,43 @@ mod tests { ); } + #[test] + fn decode_internal_parameter_values_accepts_matching_overload_args() { + let mut context = context_with_arena(vec![debug_node( + 0, + 0, + vec![internal_call_step_named( + "DebugMe::foo(uint256)", + 2, + Some(vec!["42".to_string()]), + None, + )], + )]); + let mut tui = TUIContext::new(&mut context); + + assert_eq!( + tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), + Some(vec!["42".to_string()]) + ); + } + + #[test] + fn decode_internal_parameter_values_rejects_wrong_overload_args() { + let mut context = context_with_arena(vec![debug_node( + 0, + 0, + vec![internal_call_step_named( + "DebugMe::foo(address)", + 2, + Some(vec!["0x000000000000000000000000000000000000002a".to_string()]), + None, + )], + )]); + let mut tui = TUIContext::new(&mut context); + + assert_eq!(tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), None); + } + #[test] fn decode_return_values_uses_absolute_internal_call_end_step() { let mut context = context_with_arena(vec![debug_node( @@ -1268,6 +1304,27 @@ mod tests { assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["7".to_string()])); } + #[test] + fn decode_return_values_rejects_wrong_overload() { + let mut context = context_with_arena(vec![debug_node( + 0, + 0, + vec![ + internal_call_step_named( + "DebugMe::foo(address)", + 1, + None, + Some(vec!["99".to_string()]), + ), + trace_step(Vec::new()), + ], + )]); + let mut tui = TUIContext::new(&mut context); + tui.current_step = 1; + + assert_eq!(tui.decode_return_values(&scope("foo", "(uint256 amount)")), None); + } + #[test] fn active_internal_call_caches_by_current_node_and_step() { let mut context = context_with_arena(vec![debug_node( @@ -1298,6 +1355,15 @@ mod tests { assert!(!super::decoded_internal_name_matches("Other::foo", &scope)); } + #[test] + fn decoded_internal_name_matches_canonical_signature_for_overloads() { + let scope = scope("foo", "(uint256 amount)"); + + assert!(super::decoded_internal_name_matches("DebugMe::foo(uint256)", &scope)); + assert!(!super::decoded_internal_name_matches("DebugMe::foo(address)", &scope)); + assert!(!super::decoded_internal_name_matches("Other::foo(uint256)", &scope)); + } + #[test] fn scope_variable_line_marks_unavailable_locals() { let variable = super::ScopeVariable { diff --git a/crates/evm/traces/src/debug/mod.rs b/crates/evm/traces/src/debug/mod.rs index 4d9bbfa121877..acf26cac1b1fa 100644 --- a/crates/evm/traces/src/debug/mod.rs +++ b/crates/evm/traces/src/debug/mod.rs @@ -206,7 +206,8 @@ impl<'a> DebugStepsWalker<'a> { /// Tries to parse the function name from the source code and detect the contract name which /// contains the given function. /// -/// Returns string in the format `Contract::function`. +/// Returns a string in the format `Contract::function(types)` when parameters can be resolved, +/// falling back to `Contract::function`. fn parse_function_from_loc(source: &SourceData, loc: &SourceElement) -> Option { let start = loc.offset() as usize; let (source_part, end) = source_span(&source.source, start, loc.length() as usize)?; @@ -217,7 +218,41 @@ fn parse_function_from_loc(source: &SourceData, loc: &SourceElement) -> Option String { + let signature = canonical_function_signature(function_name, source_part) + .unwrap_or_else(|| function_name.to_string()); + format!("{contract_name}::{signature}") +} + +fn canonical_function_signature(function_name: &str, source_part: &str) -> Option { + let source_part = source_part.replace('\n', ""); + let (inputs, _) = parse_types(&source_part); + let inputs = inputs?; + let types = + inputs.params.iter().map(|param| param.resolve().ok()).collect::>>()?; + Some(function_signature(function_name, &types)) +} + +/// Formats an ABI-style function signature from a name and canonical parameter types. +pub fn function_signature(function_name: &str, types: &[DynSolType]) -> String { + let mut signature = String::new(); + signature.push_str(function_name); + signature.push('('); + for (i, ty) in types.iter().enumerate() { + if i > 0 { + signature.push(','); + } + signature.push_str(&ty.sol_type_name()); + } + signature.push(')'); + signature } fn source_span(source: &str, start: usize, len: usize) -> Option<(&str, usize)> { @@ -347,7 +382,9 @@ fn memory_range(memory: &[u8], start: usize, len: usize) -> Option<&[u8]> { #[cfg(test)] mod tests { - use super::{decode_from_memory, decode_step_parameters, source_span}; + use super::{ + decode_from_memory, decode_step_parameters, internal_function_identifier, source_span, + }; use alloy_dyn_abi::{DynSolType, parser::Parameters}; use alloy_primitives::{Bytes, U256}; use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult}; @@ -379,6 +416,18 @@ mod tests { assert_eq!(source_span("abcdef", usize::MAX, 1), None); } + #[test] + fn internal_function_identifier_includes_canonical_signature() { + assert_eq!( + internal_function_identifier( + "DebugMe", + "foo", + "function foo(uint256 amount, bool ok) internal returns (uint256) {", + ), + "DebugMe::foo(uint256,bool)" + ); + } + #[test] fn decode_from_memory_rejects_overflow_location() { assert_eq!(decode_from_memory(&DynSolType::Bytes, &[0; 64], usize::MAX), None); diff --git a/crates/evm/traces/src/folded_stack_trace.rs b/crates/evm/traces/src/folded_stack_trace.rs index 4fc43594e1b39..883d29bf23105 100644 --- a/crates/evm/traces/src/folded_stack_trace.rs +++ b/crates/evm/traces/src/folded_stack_trace.rs @@ -228,7 +228,34 @@ impl FoldedStackTraceBuilder { } } +#[cfg(test)] mod tests { + use alloy_primitives::{Bytes, U256}; + use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult}; + use revm_inspectors::tracing::{ + CallTraceArena, + types::{CallTraceStep, DecodedInternalCall, DecodedTraceStep, TraceMemberOrder}, + }; + + fn trace_step(gas_remaining: u64) -> CallTraceStep { + CallTraceStep { + pc: 0, + op: OpCode::STOP, + stack: Some(Vec::::new().into_boxed_slice()), + push_stack: None, + memory: None, + returndata: Bytes::new(), + gas_remaining, + gas_refund_counter: 0, + gas_used: 0, + gas_cost: 0, + storage_change: None, + status: Some(InstructionResult::Stop), + immediate_bytes: None, + decoded: None, + } + } + #[test] fn test_fst_1() { let mut trace = super::FoldedStackTraceBuilder::default(); @@ -321,4 +348,27 @@ mod tests { ] ); } + + #[test] + fn folded_stack_trace_keeps_precise_internal_function_names() { + let mut arena = CallTraceArena::default(); + let root = &mut arena.nodes_mut()[0]; + root.trace.gas_used = 100; + root.trace.gas_limit = 100; + root.trace.steps = vec![trace_step(100), trace_step(70)]; + root.trace.steps[0].decoded = Some(Box::new(DecodedTraceStep::InternalCall( + DecodedInternalCall { + func_name: "DebugVarsTest::foo(uint256)".to_string(), + args: Some(vec!["42".to_string()]), + return_data: Some(vec!["43".to_string()]), + }, + 1, + ))); + root.ordering = vec![TraceMemberOrder::Step(0), TraceMemberOrder::Step(1)]; + + assert_eq!( + super::build(&arena, false), + vec!["fallback 70", "fallback;DebugVarsTest::foo(uint256) 30",] + ); + } } diff --git a/crates/forge/tests/cli/test_cmd/mod.rs b/crates/forge/tests/cli/test_cmd/mod.rs index c5dc47566daba..d0f7827dcb48e 100644 --- a/crates/forge/tests/cli/test_cmd/mod.rs +++ b/crates/forge/tests/cli/test_cmd/mod.rs @@ -55,6 +55,32 @@ fn setup_testdata_cmd(cmd: &mut TestCommand) { drop(dotenv); } +fn collect_debug_dump_internal_calls<'a>( + value: &'a serde_json::Value, + calls: &mut Vec<&'a serde_json::Value>, +) { + match value { + serde_json::Value::Array(values) => { + for value in values { + collect_debug_dump_internal_calls(value, calls); + } + } + serde_json::Value::Object(map) => { + if let Some(call) = map + .get("InternalCall") + .and_then(|value| value.as_array()) + .and_then(|values| values.first()) + { + calls.push(call); + } + for value in map.values() { + collect_debug_dump_internal_calls(value, calls); + } + } + _ => {} + } +} + /// Contracts excluded from the main `testdata` run because they depend on flaky external RPCs. /// These are run separately by the `flaky_testdata` test below. /// Format: pipe-separated regex alternation, e.g. `"Foo|Bar|Baz"`. @@ -1494,13 +1520,13 @@ Traces: ├─ [165406] → new SimpleContract@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f │ └─ ← [Return] 826 bytes of code ├─ [22630] SimpleContract::increment() - │ ├─ [20147] SimpleContract::_setNum(1) + │ ├─ [20147] SimpleContract::_setNum(uint256)(1) │ │ └─ ← 0 │ └─ ← [Stop] ├─ [23204] SimpleContract::setValues(100, 0x0000000000000000000000000000000000000123) - │ ├─ [247] SimpleContract::_setNum(100) + │ ├─ [247] SimpleContract::_setNum(uint256)(100) │ │ └─ ← 1 - │ ├─ [22336] SimpleContract::_setAddr(0x0000000000000000000000000000000000000123) + │ ├─ [22336] SimpleContract::_setAddr(address)(0x0000000000000000000000000000000000000123) │ │ └─ ← 0x0000000000000000000000000000000000000000 │ └─ ← [Stop] └─ ← [Stop] @@ -1550,7 +1576,7 @@ Traces: ├─ [..] → new SimpleContract@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f │ └─ ← [Return] [..] bytes of code ├─ [..] SimpleContract::setStr("new value") - │ ├─ [..] SimpleContract::_setStr("new value") + │ ├─ [..] SimpleContract::_setStr(string)("new value") │ │ └─ ← "initial value" │ └─ ← [Stop] └─ ← [Stop] @@ -2667,6 +2693,70 @@ contract Dummy { assert!(dump_path.exists()); }); +forgetest!(debug_dump_disambiguates_overloaded_internal_functions, |prj, cmd| { + prj.add_source( + "DebugVars", + r" +contract DebugVarsTest { + function testOverloadedInternalDebugVars() public { + uint256 amount = foo(uint256(42)); + address who = foo(address(0x000000000000000000000000000000000000bEEF)); + + require(amount == 43, 'bad amount'); + require(who == address(0x000000000000000000000000000000000000bEEF), 'bad address'); + } + + function foo(uint256 amount) internal pure returns (uint256 out) { + uint256 next = amount + 1; + return next; + } + + function foo(address who) internal pure returns (address out) { + address seen = who; + return seen; + } +} +", + ); + + let dump_path = prj.root().join("overloads_dump.json"); + + cmd.args([ + "test", + "--mt", + "testOverloadedInternalDebugVars", + "--debug", + "--dump", + dump_path.to_str().unwrap(), + ]); + cmd.assert_success(); + + let dump: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(dump_path).unwrap()).unwrap(); + let mut calls = Vec::new(); + collect_debug_dump_internal_calls(&dump, &mut calls); + + let uint_call = calls + .iter() + .find(|call| call["func_name"] == "DebugVarsTest::foo(uint256)") + .expect("missing uint256 overload in debugger dump"); + assert_eq!(uint_call["args"], serde_json::json!(["42"])); + assert_eq!(uint_call["return_data"], serde_json::json!(["43"])); + + let address_call = calls + .iter() + .find(|call| call["func_name"] == "DebugVarsTest::foo(address)") + .expect("missing address overload in debugger dump"); + assert_eq!( + address_call["args"], + serde_json::json!(["0x000000000000000000000000000000000000bEEF"]) + ); + assert_eq!( + address_call["return_data"], + serde_json::json!(["0x000000000000000000000000000000000000bEEF"]) + ); +}); + // forgetest!(test_debug_with_dump_setup_revert, |prj, cmd| { prj.add_test( diff --git a/crates/forge/tests/cli/test_optimizer.rs b/crates/forge/tests/cli/test_optimizer.rs index 4b16e4285a943..49aed2530777e 100644 --- a/crates/forge/tests/cli/test_optimizer.rs +++ b/crates/forge/tests/cli/test_optimizer.rs @@ -1427,7 +1427,7 @@ Traces: │ └─ ← [Stop] ├─ [..] Counter::number() [staticcall] │ └─ ← [Return] 1 - ├─ [..] StdAssertions::assertEq(1, 1) + ├─ [..] StdAssertions::assertEq(uint256,uint256)(1, 1) │ └─ ← └─ ← [Stop] From e389837d74335bbbf01e62126d16eac6ea32f478 Mon Sep 17 00:00:00 2001 From: figtracer Date: Wed, 17 Jun 2026 13:23:47 +0100 Subject: [PATCH 19/25] fix(anvil): support onlyTopLevelCall tracer config (#15199) --- crates/anvil/src/eth/backend/mem/mod.rs | 25 ++++++++--- crates/anvil/tests/it/traces.rs | 59 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 0abaad2c03710..2a6261a583c5b 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -75,9 +75,9 @@ use alloy_rpc_types::{ trace::{ filter::TraceFilter, geth::{ - FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, NoopFrame, - TraceResult, + CallConfig, FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, + GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, + NoopFrame, TraceResult, }, parity::{LocalizedTransactionTrace, TraceResultsWithTransactionHash, TraceType}, }, @@ -216,6 +216,20 @@ pub const MIN_TRANSACTION_GAS: u128 = 21000; // Gas per transaction creating a contract. pub const MIN_CREATE_GAS: u128 = 53000; +fn call_config_from_tracer_config( + tracer_config: GethDebugTracerConfig, +) -> Result { + let mut tracer_config = tracer_config.into_json(); + if let Some(config) = tracer_config.as_object_mut() + && !config.contains_key("onlyTopCall") + && let Some(only_top_level_call) = config.remove("onlyTopLevelCall") + { + config.insert("onlyTopCall".to_string(), only_top_level_call); + } + + GethDebugTracerConfig(tracer_config).into_call_config() +} + pub type State = foundry_evm::utils::StateChangeset; /// A block request, which includes the Pool Transactions if it's Pending @@ -2975,8 +2989,7 @@ where return match tracer { GethDebugTracerType::BuiltInTracer(tracer) => match tracer { GethDebugBuiltInTracerType::CallTracer => { - let call_config = tracer_config - .into_call_config() + let call_config = call_config_from_tracer_config(tracer_config) .map_err(|e| RpcError::invalid_params(e.to_string()))?; let mut inspector = self.build_inspector().with_tracing_config( @@ -3640,7 +3653,7 @@ where return Ok(res); } GethDebugBuiltInTracerType::CallTracer => { - return match tracer_config.into_call_config() { + return match call_config_from_tracer_config(tracer_config) { Ok(call_config) => { let inspector = TracingInspector::new( TracingInspectorConfig::from_geth_call_config(&call_config), diff --git a/crates/anvil/tests/it/traces.rs b/crates/anvil/tests/it/traces.rs index 2509a523c7cd5..bb338326a28a3 100644 --- a/crates/anvil/tests/it/traces.rs +++ b/crates/anvil/tests/it/traces.rs @@ -239,6 +239,65 @@ async fn test_call_tracer_debug_trace_call() { } } + let receipt = internal_call_tx_builder.send().await.unwrap().get_receipt().await.unwrap(); + let internal_call_tx_hash = receipt.transaction_hash; + let trace_provider = handle.http_provider(); + + let internal_call_tx_traces: GethTrace = trace_provider + .raw_request( + "debug_traceTransaction".into(), + ( + internal_call_tx_hash, + serde_json::json!({ + "tracer": "callTracer", + "tracerConfig": { + "withLog": true + } + }), + ), + ) + .await + .unwrap(); + + match internal_call_tx_traces { + GethTrace::CallTracer(call_frame) => { + assert_eq!(call_frame.calls.len(), 1); + assert_eq!( + call_frame.calls.first().unwrap().to.unwrap(), + *simple_storage_contract.address() + ); + } + _ => { + unreachable!() + } + } + + let internal_call_only_top_level_call_tx_traces: GethTrace = trace_provider + .raw_request( + "debug_traceTransaction".into(), + ( + internal_call_tx_hash, + serde_json::json!({ + "tracer": "callTracer", + "tracerConfig": { + "onlyTopLevelCall": true, + "withLog": true + } + }), + ), + ) + .await + .unwrap(); + + match internal_call_only_top_level_call_tx_traces { + GethTrace::CallTracer(call_frame) => { + assert!(call_frame.calls.is_empty()); + } + _ => { + unreachable!() + } + } + // directly calling the SimpleStorage contract should not result in any internal calls let direct_call_tx = TransactionRequest::default() .from(wallets[1].address()) From 7edb97e01378c938451d6321109706ad52d8ba9a Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:21:13 +0200 Subject: [PATCH 20/25] chore: pin Tempo main (#15209) --- Cargo.lock | 93 +++++++++++++------------ Cargo.toml | 20 +++--- crates/anvil/src/eth/backend/mem/mod.rs | 4 +- crates/anvil/src/eth/backend/tempo.rs | 6 ++ crates/evm/core/src/evm/tempo.rs | 48 ++++++++----- 5 files changed, 95 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9da950c7c619b..45610f03b00b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,7 +1183,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1207,7 +1207,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3055,7 +3055,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3214,7 +3214,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4276,7 +4276,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4560,7 +4560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6895,7 +6895,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7689,7 +7689,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9369,7 +9369,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-chains", "alloy-consensus", @@ -9449,7 +9449,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eip7928 0.4.3", @@ -9463,7 +9463,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9476,7 +9476,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -9500,7 +9500,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -9514,7 +9514,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9530,7 +9530,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -9543,7 +9543,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9557,7 +9557,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eip7928 0.4.3", @@ -9580,7 +9580,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9600,7 +9600,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-evm", "alloy-primitives", @@ -9613,7 +9613,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -9632,7 +9632,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9699,7 +9699,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-primitives", "derive_more", @@ -9714,7 +9714,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -9727,7 +9727,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-evm", @@ -9777,7 +9777,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-primitives", "bytes", @@ -9790,7 +9790,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-primitives", "derive_more", @@ -9804,7 +9804,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-eip7928 0.4.3", @@ -9829,7 +9829,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -9847,7 +9847,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "tokio", "tokio-stream", @@ -9857,7 +9857,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "2.3.0" -source = "git+https://github.com/paradigmxyz/reth?rev=fa3859e#fa3859e6ef44cb18178b9bc0148ea9a3a1280765" +source = "git+https://github.com/paradigmxyz/reth?rev=f955d2907c9b51f101ec93848bce36101a1ee7e3#f955d2907c9b51f101ec93848bce36101a1ee7e3" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -10383,7 +10383,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10442,7 +10442,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11167,7 +11167,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11716,13 +11716,13 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "tempo-alloy" version = "1.8.1" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-consensus", "alloy-contract", @@ -11755,7 +11755,7 @@ dependencies = [ [[package]] name = "tempo-chainspec" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-eips", "alloy-evm", @@ -11778,7 +11778,7 @@ dependencies = [ [[package]] name = "tempo-contracts" version = "1.8.1" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-contract", "alloy-primitives", @@ -11789,7 +11789,7 @@ dependencies = [ [[package]] name = "tempo-dkg-onchain-artifacts" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "bytes", "commonware-codec", @@ -11801,7 +11801,7 @@ dependencies = [ [[package]] name = "tempo-evm" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11820,6 +11820,7 @@ dependencies = [ "reth-revm", "tempo-chainspec", "tempo-contracts", + "tempo-precompiles", "tempo-primitives", "tempo-revm", "thiserror 2.0.18", @@ -11829,7 +11830,7 @@ dependencies = [ [[package]] name = "tempo-precompiles" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy", "alloy-evm", @@ -11850,7 +11851,7 @@ dependencies = [ [[package]] name = "tempo-precompiles-macros" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy", "proc-macro2", @@ -11861,7 +11862,7 @@ dependencies = [ [[package]] name = "tempo-primitives" version = "1.8.1" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -11892,7 +11893,7 @@ dependencies = [ [[package]] name = "tempo-revm" version = "1.8.2" -source = "git+https://github.com/tempoxyz/tempo?rev=563fc42578a9920067d129b0f191250e3a616a38#563fc42578a9920067d129b0f191250e3a616a38" +source = "git+https://github.com/tempoxyz/tempo?rev=2cf84002140367ff6de3268e5bf39972b8a0dc6c#2cf84002140367ff6de3268e5bf39972b8a0dc6c" dependencies = [ "alloy-consensus", "alloy-evm", @@ -11919,7 +11920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -13181,7 +13182,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -13319,7 +13320,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1a67d0b1dbe63..8a28be2d984ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -507,17 +507,17 @@ mpp = { git = "https://github.com/tempoxyz/mpp-rs", rev = "9de5344a961c13dd4b201 "reqwest-rustls-tls", "ws", ] } -tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38", default-features = false } -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38", default-features = false, features = [ +tempo-chainspec = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c", default-features = false } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c", default-features = false, features = [ "serde", ] } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38", default-features = false } -tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38", default-features = false } -tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38", default-features = false, features = [ +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c", default-features = false } +tempo-evm = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c", default-features = false } +tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c", default-features = false, features = [ "serde", ] } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38" } -tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c" } +tempo-precompiles = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c" } ## Pinned dependencies. Enabled for the workspace in crates/test-utils. @@ -594,9 +594,9 @@ alloy-op-hardforks = { git = "https://github.com/foundry-rs/optimism", branch = foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "ba6246789fc12cf8fe78aa74aadf31d663f08c1f" } ## tempo — unify crates.io versions (pulled by mpp/foundry-wallets) with git rev -tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38" } -tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38" } -tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "563fc42578a9920067d129b0f191250e3a616a38" } +tempo-primitives = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c" } +tempo-alloy = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c" } +tempo-contracts = { git = "https://github.com/tempoxyz/tempo", rev = "2cf84002140367ff6de3268e5bf39972b8a0dc6c" } # solar solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", rev = "da6b0c3a8c0edd23f8b1414c61418ba78d6c4670" } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 2a6261a583c5b..78e9b7638e5db 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -184,7 +184,7 @@ use tempo_chainspec::hardfork::TempoHardfork; use tempo_evm::evm::TempoEvmFactory; use tempo_precompiles::{ TIP_FEE_MANAGER_ADDRESS, extend_tempo_precompiles, - storage::StorageCtx, + storage::{StorageActions, StorageCtx}, tip_fee_manager::{IFeeManager, TipFeeManager}, tip20::{ISSUER_ROLE, ITIP20, TIP20Token}, }; @@ -1191,7 +1191,7 @@ impl Backend { cfg_env: &CfgEnv, ) { self.inject_precompiles(precompiles); - extend_tempo_precompiles(precompiles, cfg_env); + extend_tempo_precompiles(precompiles, cfg_env, StorageActions::disabled()); } /// Creates a concrete EVM, injects precompiles, transacts, and returns the result mapped diff --git a/crates/anvil/src/eth/backend/tempo.rs b/crates/anvil/src/eth/backend/tempo.rs index 80df57de93480..9a92882de6301 100644 --- a/crates/anvil/src/eth/backend/tempo.rs +++ b/crates/anvil/src/eth/backend/tempo.rs @@ -46,6 +46,7 @@ pub struct AnvilStorageProvider<'a> { gas_used: u64, gas_refunded: i64, reservoir: u64, + tip1060_storage_credits_enabled: bool, transient: HashMap<(Address, U256), U256>, hardfork: TempoHardfork, } @@ -66,6 +67,7 @@ impl<'a> AnvilStorageProvider<'a> { gas_used: 0, gas_refunded: 0, reservoir: 0, + tip1060_storage_credits_enabled: hardfork.is_t7(), transient: HashMap::new(), hardfork, } @@ -205,6 +207,10 @@ impl PrecompileStorageProvider for AnvilStorageProvider<'_> { fn amsterdam_eip8037_enabled(&self) -> bool { false } + + fn set_tip1060_storage_credits(&mut self, enabled: bool) { + self.tip1060_storage_credits_enabled = enabled && self.hardfork.is_t7(); + } } /// Initialize Tempo precompiles and fee tokens for Anvil. diff --git a/crates/evm/core/src/evm/tempo.rs b/crates/evm/core/src/evm/tempo.rs index c7797ed05380e..1aca7d80e4bc5 100644 --- a/crates/evm/core/src/evm/tempo.rs +++ b/crates/evm/core/src/evm/tempo.rs @@ -13,7 +13,10 @@ use revm::{ state::Bytecode, }; use tempo_evm::{TempoBlockEnv, TempoEvmFactory, TempoHaltReason, evm::TempoEvm}; -use tempo_precompiles::{extend_tempo_precompiles, storage::StorageCtx}; +use tempo_precompiles::{ + extend_tempo_precompiles, + storage::{StorageActions, StorageCtx}, +}; use tempo_revm::{ TempoInvalidTransaction, TempoTxEnv, evm::TempoContext, gas_params::tempo_gas_params, handler::TempoEvmHandler, @@ -46,23 +49,32 @@ pub(crate) fn initialize_tempo_evm< is_forked: bool, ) { let ctx = evm.ctx_mut(); - StorageCtx::enter_evm(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx, || { - if is_forked { - // In fork mode, warm up precompile accounts to avoid repeated RPC fetches. - let mut sctx = StorageCtx; - let sentinel = Bytecode::new_legacy(Bytes::from_static(&[0xef])); - for addr in - TEMPO_PRECOMPILE_ADDRESSES.iter().copied().chain(TEMPO_TIP20_TOKENS.iter().copied()) - { - sctx.set_code(addr, sentinel.clone()) - .expect("failed to warm tempo precompile address"); + StorageCtx::enter_evm( + &mut ctx.journaled_state, + &ctx.block, + &ctx.cfg, + &ctx.tx, + StorageActions::disabled(), + || { + if is_forked { + // In fork mode, warm up precompile accounts to avoid repeated RPC fetches. + let mut sctx = StorageCtx; + let sentinel = Bytecode::new_legacy(Bytes::from_static(&[0xef])); + for addr in TEMPO_PRECOMPILE_ADDRESSES + .iter() + .copied() + .chain(TEMPO_TIP20_TOKENS.iter().copied()) + { + sctx.set_code(addr, sentinel.clone()) + .expect("failed to warm tempo precompile address"); + } + } else { + // In non-fork mode, run full genesis initialization. + initialize_tempo_test_genesis_inner(TEST_CONTRACT_ADDRESS, CALLER) + .expect("tempo genesis initialization failed"); } - } else { - // In non-fork mode, run full genesis initialization. - initialize_tempo_test_genesis_inner(TEST_CONTRACT_ADDRESS, CALLER) - .expect("tempo genesis initialization failed"); - } - }); + }, + ); } impl FoundryEvmFactory for TempoEvmFactory { @@ -89,7 +101,7 @@ impl FoundryEvmFactory for TempoEvmFactory { let networks = tempo_evm.inspector().get_networks(); networks.inject_precompiles(tempo_evm.precompiles_mut()); let cfg = tempo_evm.cfg.clone(); - extend_tempo_precompiles(tempo_evm.precompiles_mut(), &cfg); + extend_tempo_precompiles(tempo_evm.precompiles_mut(), &cfg, StorageActions::disabled()); initialize_tempo_evm(&mut tempo_evm, is_forked); tempo_evm From 11c72aef1859349a66b3119435a902299a5bebbb Mon Sep 17 00:00:00 2001 From: figtracer Date: Wed, 17 Jun 2026 14:28:12 +0100 Subject: [PATCH 21/25] feat(debugger): add opcode search to TUI (#15211) Amp-Thread-ID: https://ampcode.com/threads/T-019ed581-c01b-702b-8a62-7dcf8df35fd5 Co-authored-by: Amp --- crates/debugger/src/tui/context.rs | 278 ++++++++++++++++++++++++++++- crates/debugger/src/tui/draw.rs | 21 ++- 2 files changed, 295 insertions(+), 4 deletions(-) diff --git a/crates/debugger/src/tui/context.rs b/crates/debugger/src/tui/context.rs index 9a8222a2c3043..786bd54defdfa 100644 --- a/crates/debugger/src/tui/context.rs +++ b/crates/debugger/src/tui/context.rs @@ -68,6 +68,10 @@ pub(crate) struct TUIContext<'a> { pub(crate) key_buffer: String, /// Current goto program counter prompt contents, if the prompt is active. pub(crate) pc_input: Option, + /// Current opcode search prompt contents, if the prompt is active. + pub(crate) opcode_search_input: Option, + /// Last opcode search term, used by repeat-search shortcuts. + pub(crate) last_opcode_search: Option, /// Last status or error message to show in the footer. pub(crate) status: Option, /// Current step in the debug steps. @@ -91,6 +95,8 @@ impl<'a> TUIContext<'a> { key_buffer: String::with_capacity(64), pc_input: None, + opcode_search_input: None, + last_opcode_search: None, status: None, current_step: 0, draw_memory: DrawMemory::default(), @@ -178,6 +184,11 @@ impl TUIContext<'_> { } fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow { + if self.opcode_search_input.is_some() { + self.handle_opcode_search_input_key_event(event); + return ControlFlow::Continue(()); + } + if self.pc_input.is_some() { self.handle_pc_input_key_event(event); return ControlFlow::Continue(()); @@ -314,6 +325,23 @@ impl TUIContext<'_> { self.pc_input = Some(String::new()); } + // Search opcodes in the current call + KeyCode::Char('/') => { + self.key_buffer.clear(); + self.status = None; + self.opcode_search_input = Some(String::new()); + } + + // Repeat opcode search forward + KeyCode::Char('n') => self.repeat(|this| { + this.repeat_opcode_search(SearchDirection::Forward); + }), + + // Repeat opcode search backward + KeyCode::Char('N') => self.repeat(|this| { + this.repeat_opcode_search(SearchDirection::Backward); + }), + // Toggle help notice KeyCode::Char('h') => self.show_shortcuts = !self.show_shortcuts, @@ -359,6 +387,65 @@ impl TUIContext<'_> { } } + fn handle_opcode_search_input_key_event(&mut self, event: KeyEvent) { + match event.code { + KeyCode::Esc => { + self.opcode_search_input = None; + } + KeyCode::Enter => { + let input = self.opcode_search_input.take().unwrap_or_default(); + self.search_opcode_from_input(&input); + } + KeyCode::Backspace => { + if let Some(input) = &mut self.opcode_search_input { + input.pop(); + } + } + KeyCode::Char(c) if !event.modifiers.contains(KeyModifiers::CONTROL) => { + if let Some(input) = &mut self.opcode_search_input { + input.push(c); + } + } + _ => {} + } + } + + fn search_opcode_from_input(&mut self, input: &str) { + let query = input.trim(); + if query.is_empty() { + self.set_error("Enter an opcode search term".to_string()); + return; + } + + self.last_opcode_search = Some(query.to_string()); + self.search_opcode(query, SearchDirection::Forward); + } + + fn repeat_opcode_search(&mut self, direction: SearchDirection) { + let Some(query) = self.last_opcode_search.clone() else { + self.set_error("No previous opcode search".to_string()); + return; + }; + + self.search_opcode(&query, direction); + } + + fn search_opcode(&mut self, query: &str, direction: SearchDirection) { + let Some(step_index) = + find_opcode_match(&self.opcode_list, self.current_step, query, direction) + else { + self.set_error(format!("No opcode matching `{query}` in current call")); + return; + }; + + self.current_step = step_index; + self.scroll_memory_to_current_write(); + + let pc = self.current_step().pc; + let opcode = self.opcode_list.get(step_index).map(String::as_str).unwrap_or_default(); + self.set_info(format!("Found `{query}` at PC 0x{pc:x} ({pc}): {opcode}")); + } + fn goto_pc_from_input(&mut self, input: &str) { let candidates = match parse_pc_candidates(input) { Ok(candidates) => candidates, @@ -460,7 +547,7 @@ impl TUIContext<'_> { } fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow { - if self.pc_input.is_some() { + if self.pc_input.is_some() || self.opcode_search_input.is_some() { return ControlFlow::Continue(()); } @@ -560,6 +647,12 @@ const fn is_pc_input_char(c: char) -> bool { c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':') } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SearchDirection { + Forward, + Backward, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PcBase { Hex, @@ -720,6 +813,34 @@ fn pc_exists_outside_code_context(arena: &[DebugNode], current: &DebugNode, pc: }) } +fn find_opcode_match( + opcodes: &[String], + current_step: usize, + query: &str, + direction: SearchDirection, +) -> Option { + if opcodes.is_empty() { + return None; + } + + let needle = query.trim().to_ascii_lowercase(); + if needle.is_empty() { + return None; + } + + let current = current_step.min(opcodes.len() - 1); + let matches = |i: usize| opcodes[i].to_ascii_lowercase().contains(&needle); + + match direction { + SearchDirection::Forward => { + ((current + 1)..opcodes.len()).chain(0..=current).find(|&i| matches(i)) + } + SearchDirection::Backward => { + (0..current).rev().chain((current..opcodes.len()).rev()).find(|&i| matches(i)) + } + } +} + fn pretty_opcode(step: &CallTraceStep) -> String { if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) { format!("{}(0x{})", step.op, hex::encode(immediate)) @@ -764,6 +885,13 @@ mod tests { step_with_stack(pc, OpCode::STOP, &[]) } + fn step_with_immediate(pc: usize, op: OpCode, immediate: &'static [u8]) -> CallTraceStep { + CallTraceStep { + immediate_bytes: Some(Bytes::from_static(immediate)), + ..step_with_stack(pc, op, &[]) + } + } + fn step_with_stack(pc: usize, op: OpCode, stack: &[usize]) -> CallTraceStep { CallTraceStep { pc, @@ -1041,6 +1169,154 @@ mod tests { assert_eq!(tui.status, None); } + #[test] + fn opcode_search_wraps_and_is_case_insensitive() { + let opcodes = + vec!["STOP".to_string(), "PUSH4(0x95d89b41)".to_string(), "MSTORE".to_string()]; + + assert_eq!(find_opcode_match(&opcodes, 0, "push4", SearchDirection::Forward), Some(1)); + assert_eq!(find_opcode_match(&opcodes, 0, "95D89B41", SearchDirection::Forward), Some(1)); + assert_eq!(find_opcode_match(&opcodes, 0, "mstore", SearchDirection::Backward), Some(2)); + assert_eq!(find_opcode_match(&opcodes, 0, "sload", SearchDirection::Forward), None); + } + + #[test] + fn opcode_search_input_mode_handles_keys_and_blocks_normal_commands() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![DebugNode::new( + address, + CallKind::Call, + vec![ + step(1), + step_with_immediate(2, OpCode::PUSH4, &[0x95, 0xd8, 0x9b, 0x41]), + step_with_stack(3, OpCode::MSTORE, &[]), + ], + Bytes::new(), + 0, + None, + )]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + assert!(matches!(tui.handle_key_event(key(KeyCode::Char('/'))), ControlFlow::Continue(()))); + assert_eq!(tui.opcode_search_input.as_deref(), Some("")); + + let _ = tui.handle_key_event(key(KeyCode::Char('q'))); + assert_eq!(tui.opcode_search_input.as_deref(), Some("q")); + assert_eq!(tui.current_step, 0); + + let _ = tui.handle_key_event(key(KeyCode::Backspace)); + let _ = tui.handle_key_event(key(KeyCode::Char('9'))); + let _ = tui.handle_key_event(key(KeyCode::Char('5'))); + let _ = tui.handle_key_event(key(KeyCode::Enter)); + + assert_eq!(tui.opcode_search_input, None); + assert_eq!(tui.last_opcode_search.as_deref(), Some("95")); + assert_eq!(tui.current_step, 1); + assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info); + } + + #[test] + fn opcode_search_repeats_forward_and_backward() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![DebugNode::new( + address, + CallKind::Call, + vec![ + step_with_stack(1, OpCode::MSTORE, &[]), + step(2), + step_with_stack(3, OpCode::MSTORE, &[]), + ], + Bytes::new(), + 0, + None, + )]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + let _ = tui.handle_key_event(key(KeyCode::Char('/'))); + for c in "mstore".chars() { + let _ = tui.handle_key_event(key(KeyCode::Char(c))); + } + let _ = tui.handle_key_event(key(KeyCode::Enter)); + assert_eq!(tui.current_step, 2); + + let _ = tui.handle_key_event(key(KeyCode::Char('n'))); + assert_eq!(tui.current_step, 0); + + let _ = tui.handle_key_event(key(KeyCode::Char('N'))); + assert_eq!(tui.current_step, 2); + } + + #[test] + fn opcode_search_escape_cancels_without_moving() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + let _ = tui.handle_key_event(key(KeyCode::Char('/'))); + let _ = tui.handle_key_event(key(KeyCode::Char('s'))); + let _ = tui.handle_key_event(key(KeyCode::Esc)); + + assert_eq!(tui.opcode_search_input, None); + assert_eq!(tui.last_opcode_search, None); + assert_eq!(tui.current_step, 0); + assert_eq!(tui.status, None); + } + + #[test] + fn opcode_search_reports_empty_input_without_remembering_search() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + let _ = tui.handle_key_event(key(KeyCode::Char('/'))); + let _ = tui.handle_key_event(key(KeyCode::Enter)); + + assert_eq!(tui.current_step, 0); + assert_eq!(tui.last_opcode_search, None); + let status = tui.status.as_ref().unwrap(); + assert_eq!(status.kind, StatusKind::Error); + assert_eq!(status.text, "Enter an opcode search term"); + } + + #[test] + fn opcode_search_reports_repeat_without_previous_search() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + let _ = tui.handle_key_event(key(KeyCode::Char('n'))); + + assert_eq!(tui.current_step, 0); + let status = tui.status.as_ref().unwrap(); + assert_eq!(status.kind, StatusKind::Error); + assert_eq!(status.text, "No previous opcode search"); + } + + #[test] + fn opcode_search_reports_no_match_without_moving() { + let address = Address::repeat_byte(1); + let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]); + let mut tui = TUIContext::new(&mut context); + tui.init(); + + let _ = tui.handle_key_event(key(KeyCode::Char('/'))); + for c in "sload".chars() { + let _ = tui.handle_key_event(key(KeyCode::Char(c))); + } + let _ = tui.handle_key_event(key(KeyCode::Enter)); + + assert_eq!(tui.current_step, 0); + assert_eq!(tui.last_opcode_search.as_deref(), Some("sload")); + let status = tui.status.as_ref().unwrap(); + assert_eq!(status.kind, StatusKind::Error); + assert_eq!(status.text, "No opcode matching `sload` in current call"); + } + #[test] fn memory_write_start_line_uses_write_offset() { assert_eq!(memory_write_start_line(&step_with_stack(0, OpCode::MSTORE, &[0, 96])), Some(3)); diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index 2a3bacc1ce703..52bb6448d77a7 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -195,7 +195,9 @@ impl TUIContext<'_> { } fn footer_height(&self) -> u16 { - let status_or_input = u16::from(self.pc_input.is_some() || self.status.is_some()); + let status_or_input = u16::from( + self.pc_input.is_some() || self.opcode_search_input.is_some() || self.status.is_some(), + ); let shortcuts = if self.show_shortcuts { 2 } else { 0 }; status_or_input + shortcuts } @@ -216,6 +218,19 @@ impl TUIContext<'_> { Style::new().add_modifier(Modifier::DIM), ), ])); + } else if let Some(input) = &self.opcode_search_input { + lines.push(Line::from(vec![ + Span::styled( + "Search opcodes: /", + Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + Span::raw(input.as_str()), + Span::styled("█", Style::new().fg(Color::Cyan)), + Span::styled( + " Enter: jump | Esc: cancel | after search: n/N repeat", + Style::new().add_modifier(Modifier::DIM), + ), + ])); } else if let Some(status) = &self.status { let style = match status.kind { StatusKind::Info => Style::new().fg(Color::Green), @@ -224,8 +239,8 @@ impl TUIContext<'_> { lines.push(Line::from(Span::styled(status.text.as_str(), style))); } - let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [p]: goto PC | [b]: cycle memory/calldata/returndata buffers"; - let l2 = "[l]: layout | [t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | [']: goto breakpoint | [h] toggle help"; + let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [p]: goto PC | [/]: search opcodes | [n/N]: next/prev search"; + let l2 = "[l]: layout | [b]: cycle buffer | [t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | [']: goto breakpoint | [h] toggle help"; let dimmed = Style::new().add_modifier(Modifier::DIM); if self.show_shortcuts { lines.push(Line::from(Span::styled(l1, dimmed))); From 3f449c3b9e2e58dd5f759e04e4e55d19abc469a1 Mon Sep 17 00:00:00 2001 From: Manuel Wedler Date: Wed, 17 Jun 2026 17:51:42 +0200 Subject: [PATCH 22/25] fix(verify): print Sourcify job UI URL instead of status API URL on submission (#15210) This is for consistency with the Etherscan submission which also shows a URL to a web interface. The Sourcify server provides a redirect URL for this purpose, because the UI lives on a different base URL. Co-authored-by: Mablr <59505383+mablr@users.noreply.github.com> --- crates/verify/src/sourcify.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/verify/src/sourcify.rs b/crates/verify/src/sourcify.rs index 19931da45b528..27ec7c5c19cbe 100644 --- a/crates/verify/src/sourcify.rs +++ b/crates/verify/src/sourcify.rs @@ -100,7 +100,7 @@ impl VerificationProvider for SourcifyVerificationProvider { .await?; if let Some(resp) = resp { - let job_url = Self::get_job_status_url( + let job_url = Self::get_job_ui_url( args.verifier.verifier_url.as_deref(), resp.verification_id.clone(), ); @@ -208,6 +208,11 @@ impl SourcifyVerificationProvider { format!("{base_url}v2/verify/{job_id}") } + fn get_job_ui_url(verifier_url: Option<&str>, job_id: String) -> String { + let base_url = Self::get_base_url(verifier_url); + format!("{base_url}verify-ui/jobs/{job_id}") + } + fn get_lookup_url( verifier_url: Option<&str>, chain_id: u64, From 0874730f108da0413469e1d1e7f0fa7a8769344b Mon Sep 17 00:00:00 2001 From: figtracer Date: Wed, 17 Jun 2026 20:20:37 +0100 Subject: [PATCH 23/25] feat(cast): add Tempo receive policy cast tooling (#15195) --- crates/cast/src/args.rs | 1 + crates/cast/src/cmd/mod.rs | 1 + crates/cast/src/cmd/receive_policy.rs | 784 ++++++++++++++++++++++++++ crates/cast/src/cmd/tip20/mod.rs | 4 +- crates/cast/src/opts.rs | 8 + 5 files changed, 796 insertions(+), 2 deletions(-) create mode 100644 crates/cast/src/cmd/receive_policy.rs diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 7eb97cf34e84e..9daef40bd01f9 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -963,6 +963,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::TxPool { command } => command.run().await?, CastSubcommand::Erc20Token { command } => command.run().await?, CastSubcommand::Tip20Token { command } => command.run().await?, + CastSubcommand::ReceivePolicy { command } => command.run().await?, CastSubcommand::Keychain { command } => command.run().await?, CastSubcommand::KeyAuthorization { command } => command.run().await?, CastSubcommand::Tempo { command } => command.run().await?, diff --git a/crates/cast/src/cmd/mod.rs b/crates/cast/src/cmd/mod.rs index 865a67af24882..04495599d0113 100644 --- a/crates/cast/src/cmd/mod.rs +++ b/crates/cast/src/cmd/mod.rs @@ -25,6 +25,7 @@ pub mod keychain; pub mod logs; pub(crate) mod miner; pub mod mktx; +pub mod receive_policy; pub mod rpc; pub mod run; pub mod send; diff --git a/crates/cast/src/cmd/receive_policy.rs b/crates/cast/src/cmd/receive_policy.rs new file mode 100644 index 0000000000000..e3c1e7025c65b --- /dev/null +++ b/crates/cast/src/cmd/receive_policy.rs @@ -0,0 +1,784 @@ +use crate::{ + cmd::tip20::{resolve_tip20_signer, send_tip20_transaction}, + tx::{SendTxOpts, TxParams}, +}; +use alloy_ens::NameOrAddress; +use alloy_primitives::{Address, Bytes, U256, keccak256}; +use alloy_sol_types::{SolCall, SolValue}; +use clap::{Parser, Subcommand}; +use eyre::{Result, WrapErr, ensure}; +use foundry_cli::{ + json::print_json_success, + opts::RpcOpts, + utils::{LoadConfig, get_provider}, +}; +use foundry_common::{provider::ProviderBuilder, shell}; +use foundry_evm_networks::TEMPO_PRECOMPILE_ADDRESSES; +use serde_json::{Value, json}; +use std::str::FromStr; +use tempo_alloy::TempoNetwork; +use tempo_contracts::precompiles::{ + ADDRESS_REGISTRY_ADDRESS, IAddressRegistry, IReceivePolicyGuard, ITIP403Registry, + RECEIVE_POLICY_GUARD_ADDRESS, TIP403_REGISTRY_ADDRESS, +}; +use tempo_primitives::TempoAddressExt; + +/// Account-level receive policy operations (Tempo). +#[derive(Debug, Parser, Clone)] +pub enum ReceivePolicySubcommand { + /// Set the caller's TIP-403 receive policy. + Set { + /// Sender policy ID to evaluate for inbound transfer originators. + sender_policy_id: u64, + + /// Token filter policy ID to evaluate for inbound TIP-20 tokens. + token_filter_id: u64, + + /// Address authorized to recover held receipts. Defaults to originator recovery. + #[arg(long, value_name = "ADDRESS", default_value_t = Address::ZERO)] + recovery_authority: Address, + + /// Print the calldata and receive-policy warning without sending a transaction. + #[arg(long, visible_alias = "dry-run")] + preview: bool, + + /// Suppress the originator-recovery/system-sender warning. + #[arg(long)] + force: bool, + + #[command(flatten)] + send_tx: SendTxOpts, + + #[command(flatten)] + tx: TxParams, + }, + + /// Get an account's configured receive policy. + Get { + /// Account whose receive policy should be queried. + #[arg(value_parser = NameOrAddress::from_str)] + account: NameOrAddress, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Validate whether an inbound TIP-20 transfer or mint would be credited or held. + Validate { + /// TIP-20 token address. + #[arg(value_parser = NameOrAddress::from_str)] + token: NameOrAddress, + + /// Inbound transfer sender or mint originator. + #[arg(value_parser = NameOrAddress::from_str)] + sender: NameOrAddress, + + /// Intended recipient. + #[arg(value_parser = NameOrAddress::from_str)] + receiver: NameOrAddress, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Blocked receive-policy receipt utilities. + Receipt { + #[command(subcommand)] + command: ReceivePolicyReceiptSubcommand, + }, + + /// Claim held TIP-20 funds using a blocked receive-policy receipt. + Claim { + /// Desired release target. The guard decides onchain whether to resume or reroute. + #[arg(value_parser = NameOrAddress::from_str)] + to: NameOrAddress, + + /// ABI-encoded ReceivePolicyGuard claim receipt. + receipt: Bytes, + + #[command(flatten)] + send_tx: SendTxOpts, + + #[command(flatten)] + tx: TxParams, + }, +} + +#[derive(Debug, Subcommand, Clone)] +pub enum ReceivePolicyReceiptSubcommand { + /// Decode an ABI-encoded ReceivePolicyGuard claim receipt. + Decode { + /// ABI-encoded ReceivePolicyGuard claim receipt. + receipt: Bytes, + }, + + /// Query the held TIP-20 balance for a claim receipt. + Balance { + /// ABI-encoded ReceivePolicyGuard claim receipt. + receipt: Bytes, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Burn held funds for a blocked receipt when authorized by the token. + Burn { + /// ABI-encoded ReceivePolicyGuard claim receipt. + receipt: Bytes, + + #[command(flatten)] + send_tx: Box, + + #[command(flatten)] + tx: Box, + }, +} + +impl ReceivePolicySubcommand { + pub async fn run(self) -> Result<()> { + match self { + Self::Set { + sender_policy_id, + token_filter_id, + recovery_authority, + preview, + force, + send_tx, + tx, + } => { + set( + sender_policy_id, + token_filter_id, + recovery_authority, + preview, + force, + send_tx, + tx, + ) + .await? + } + Self::Get { account, rpc } => get(account, rpc).await?, + Self::Validate { token, sender, receiver, rpc } => { + validate(token, sender, receiver, rpc).await? + } + Self::Receipt { command } => match command { + ReceivePolicyReceiptSubcommand::Decode { receipt } => decode_receipt(receipt)?, + ReceivePolicyReceiptSubcommand::Balance { receipt, rpc } => { + receipt_balance(receipt, rpc).await? + } + ReceivePolicyReceiptSubcommand::Burn { receipt, send_tx, tx } => { + burn_receipt(receipt, *send_tx, *tx).await? + } + }, + Self::Claim { to, receipt, send_tx, tx } => claim(to, receipt, send_tx, tx).await?, + } + + Ok(()) + } +} + +async fn set( + sender_policy_id: u64, + token_filter_id: u64, + recovery_authority: Address, + preview: bool, + force: bool, + send_tx: SendTxOpts, + tx: TxParams, +) -> Result<()> { + let warning = if force { + None + } else { + recovery_warning(sender_policy_id, recovery_authority, &send_tx.eth.rpc).await? + }; + + let call = ITIP403Registry::setReceivePolicyCall { + senderPolicyId: sender_policy_id, + tokenFilterId: token_filter_id, + recoveryAuthority: recovery_authority, + }; + let calldata = Bytes::from(call.abi_encode()); + + if preview { + let payload = json!({ + "action": "set_receive_policy", + "registry": format!("{TIP403_REGISTRY_ADDRESS}"), + "sender_policy_id": sender_policy_id, + "token_filter_id": token_filter_id, + "recovery_authority": format!("{recovery_authority}"), + "recovery_mode": recovery_mode(recovery_authority), + "calldata": format!("{calldata}"), + "warning": warning, + }); + if shell::is_json() { + print_json_success(payload)?; + } else { + sh_println!( + "Registry: {TIP403_REGISTRY_ADDRESS}\n\ + Sender policy ID: {sender_policy_id}\n\ + Token filter ID: {token_filter_id}\n\ + Recovery authority: {recovery_authority}\n\ + Recovery mode: {}\n\ + Calldata: {calldata}", + recovery_mode(recovery_authority) + )?; + if let Some(warning) = warning.as_deref() { + sh_warn!("{warning}")?; + } + } + return Ok(()); + } + + if let Some(warning) = warning.as_deref() { + sh_warn!("{warning}")?; + } + + let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?; + send_tip20_transaction( + NameOrAddress::Address(TIP403_REGISTRY_ADDRESS), + "setReceivePolicy(uint64,uint64,address)", + vec![ + sender_policy_id.to_string(), + token_filter_id.to_string(), + recovery_authority.to_string(), + ], + send_tx, + tx, + signer, + access_key, + ) + .await +} + +async fn get(account: NameOrAddress, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = get_provider(&config)?; + let account = account.resolve(&provider).await?; + let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider); + let policy = registry.receivePolicy(account).call().await?; + + let payload = json!({ + "account": format!("{account}"), + "has_receive_policy": policy.hasReceivePolicy, + "sender_policy_id": policy.senderPolicyId, + "sender_policy_type": policy_type(policy.senderPolicyType), + "token_filter_id": policy.tokenFilterId, + "token_filter_type": policy_type(policy.tokenFilterType), + "recovery_authority": format!("{}", policy.recoveryAuthority), + "recovery_mode": recovery_mode(policy.recoveryAuthority), + }); + print_payload(payload, |payload| { + sh_println!( + "Account: {}\n\ + Has receive policy: {}\n\ + Sender policy ID: {}\n\ + Sender policy type: {}\n\ + Token filter ID: {}\n\ + Token filter type: {}\n\ + Recovery authority: {}\n\ + Recovery mode: {}", + payload["account"].as_str().unwrap_or_default(), + payload["has_receive_policy"].as_bool().unwrap_or_default(), + payload["sender_policy_id"], + payload["sender_policy_type"].as_str().unwrap_or_default(), + payload["token_filter_id"], + payload["token_filter_type"].as_str().unwrap_or_default(), + payload["recovery_authority"].as_str().unwrap_or_default(), + payload["recovery_mode"].as_str().unwrap_or_default(), + ) + }) +} + +async fn validate( + token: NameOrAddress, + sender: NameOrAddress, + receiver: NameOrAddress, + rpc: RpcOpts, +) -> Result<()> { + let config = rpc.load_config()?; + let provider = get_provider(&config)?; + let token = token.resolve(&provider).await?; + let sender = sender.resolve(&provider).await?; + let receiver = receiver.resolve(&provider).await?; + let effective_receiver = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider) + .resolveRecipient(receiver) + .call() + .await?; + let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider); + let result = registry.validateReceivePolicy(token, sender, effective_receiver).call().await?; + let delivery_state = if result.authorized { "credited" } else { "held" }; + + let payload = validate_payload( + token, + sender, + receiver, + effective_receiver, + result.authorized, + result.blockedReason, + delivery_state, + ); + print_payload(payload, |payload| { + sh_println!( + "Token: {}\n\ + Sender: {}\n\ + Receiver: {}\n\ + Effective recv: {}\n\ + Authorized: {}\n\ + Blocked reason: {}\n\ + Delivery state: {}", + payload["token"].as_str().unwrap_or_default(), + payload["sender"].as_str().unwrap_or_default(), + payload["receiver"].as_str().unwrap_or_default(), + payload["effective_receiver"].as_str().unwrap_or_default(), + payload["authorized"].as_bool().unwrap_or_default(), + payload["blocked_reason"].as_str().unwrap_or_default(), + payload["delivery_state"].as_str().unwrap_or_default(), + ) + }) +} + +fn validate_payload( + token: Address, + sender: Address, + receiver: Address, + effective_receiver: Address, + authorized: bool, + blocked_reason_value: ITIP403Registry::BlockedReason, + delivery_state: &str, +) -> Value { + json!({ + "token": format!("{token}"), + "sender": format!("{sender}"), + "receiver": format!("{receiver}"), + "effective_receiver": format!("{effective_receiver}"), + "receiver_was_resolved": receiver != effective_receiver, + "authorized": authorized, + "blocked_reason": blocked_reason(blocked_reason_value), + "delivery_state": delivery_state, + }) +} + +fn decode_receipt(receipt: Bytes) -> Result<()> { + let decoded = decode_claim_receipt(&receipt)?; + let payload = receipt_payload(&receipt, &decoded, None); + print_payload(payload, |payload| { + print_decoded_receipt(payload)?; + print_claim_hint(payload) + }) +} + +async fn receipt_balance(receipt: Bytes, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = get_provider(&config)?; + let guard = IReceivePolicyGuard::new(RECEIVE_POLICY_GUARD_ADDRESS, provider); + let amount = guard.balanceOf(receipt.clone()).call().await?; + let decoded = decode_claim_receipt(&receipt)?; + let payload = receipt_payload(&receipt, &decoded, Some(amount)); + print_payload(payload, |payload| { + print_decoded_receipt(payload)?; + sh_println!("Held balance: {}", payload["held_balance"].as_str().unwrap_or_default()) + }) +} + +async fn burn_receipt(receipt: Bytes, send_tx: SendTxOpts, tx: TxParams) -> Result<()> { + decode_claim_receipt(&receipt)?; + let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?; + send_tip20_transaction( + NameOrAddress::Address(RECEIVE_POLICY_GUARD_ADDRESS), + "burnBlockedReceipt(bytes)", + vec![format!("{receipt}")], + send_tx, + tx, + signer, + access_key, + ) + .await +} + +async fn claim(to: NameOrAddress, receipt: Bytes, send_tx: SendTxOpts, tx: TxParams) -> Result<()> { + decode_claim_receipt(&receipt)?; + let config = send_tx.eth.rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let to = to.resolve(&provider).await?; + let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?; + send_tip20_transaction( + NameOrAddress::Address(RECEIVE_POLICY_GUARD_ADDRESS), + "claim(address,bytes)", + vec![to.to_string(), format!("{receipt}")], + send_tx, + tx, + signer, + access_key, + ) + .await +} + +async fn recovery_warning( + sender_policy_id: u64, + recovery_authority: Address, + rpc: &RpcOpts, +) -> Result> { + if recovery_authority != Address::ZERO { + return Ok(None); + } + + let config = rpc.load_config()?; + let provider = get_provider(&config)?; + let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider); + let mut blocked = Vec::new(); + for address in TEMPO_PRECOMPILE_ADDRESSES { + if !registry.isAuthorizedSender(sender_policy_id, *address).call().await.unwrap_or(true) { + blocked.push(*address); + } + } + + if blocked.is_empty() { + return Ok(None); + } + + Ok(Some(format!( + "originator recovery is enabled because recovery authority is 0x0, but sender policy \ + {sender_policy_id} blocks {} Tempo system/precompile sender(s): {}. Receipts created \ + for those senders may not be claimable by a user. Choose receiver or third-party \ + recovery authority when blocking system senders, or pass --force if this is intentional.", + blocked.len(), + blocked.iter().map(Address::to_string).collect::>().join(", ") + ))) +} + +fn decode_claim_receipt(receipt: &Bytes) -> Result { + let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(receipt) + .wrap_err("invalid ReceivePolicyGuard claim receipt")?; + + ensure!( + decoded.version == 1, + "unsupported ReceivePolicyGuard claim receipt version {}", + decoded.version + ); + ensure!(decoded.token != Address::ZERO, "ReceivePolicyGuard claim receipt token is zero"); + ensure!( + decoded.recipient != RECEIVE_POLICY_GUARD_ADDRESS, + "ReceivePolicyGuard claim receipt recipient cannot be the guard precompile" + ); + ensure!( + matches!( + decoded.blockedReason, + reason if reason == ITIP403Registry::BlockedReason::TOKEN_FILTER as u8 || + reason == ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8 + ), + "ReceivePolicyGuard claim receipt blocked reason is not claimable" + ); + ensure!( + matches!( + decoded.kind, + IReceivePolicyGuard::InboundKind::TRANSFER | IReceivePolicyGuard::InboundKind::MINT + ), + "ReceivePolicyGuard claim receipt inbound kind is unknown" + ); + + Ok(decoded) +} + +fn receipt_payload( + receipt: &Bytes, + decoded: &IReceivePolicyGuard::ClaimReceiptV1, + amount: Option, +) -> Value { + let receipt_key = keccak256(receipt); + let delivery_state = match amount { + Some(amount) if amount > U256::ZERO => "held", + Some(_) => "not_held", + None => "unknown", + }; + let mut payload = json!({ + "receipt": format!("{receipt}"), + "receipt_key": format!("{receipt_key}"), + "version": decoded.version, + "token": format!("{}", decoded.token), + "recovery_authority": format!("{}", decoded.recoveryAuthority), + "recovery_mode": recovery_mode(decoded.recoveryAuthority), + "originator": format!("{}", decoded.originator), + "recipient": format!("{}", decoded.recipient), + "recipient_is_virtual": decoded.recipient.is_virtual(), + "claim_target": if decoded.recipient.is_virtual() || decoded.recoveryAuthority == Address::ZERO { + Value::Null + } else { + json!(format!("{}", decoded.recipient)) + }, + "blocked_at": decoded.blockedAt, + "blocked_nonce": decoded.blockedNonce, + "blocked_reason": blocked_reason_u8(decoded.blockedReason), + "kind": inbound_kind(decoded.kind), + "memo": format!("{}", decoded.memo), + "delivery_state": delivery_state, + }); + if let Some(amount) = amount { + payload["held_balance"] = json!(amount.to_string()); + } + payload +} + +fn print_decoded_receipt(payload: &Value) -> Result<()> { + sh_println!( + "Receipt key: {}\n\ + Token: {}\n\ + Recovery authority: {}\n\ + Recovery mode: {}\n\ + Originator: {}\n\ + Recipient: {}\n\ + Blocked at: {}\n\ + Blocked nonce: {}\n\ + Blocked reason: {}\n\ + Kind: {}\n\ + Memo: {}\n\ + Delivery state: {}", + payload["receipt_key"].as_str().unwrap_or_default(), + payload["token"].as_str().unwrap_or_default(), + payload["recovery_authority"].as_str().unwrap_or_default(), + payload["recovery_mode"].as_str().unwrap_or_default(), + payload["originator"].as_str().unwrap_or_default(), + payload["recipient"].as_str().unwrap_or_default(), + payload["blocked_at"], + payload["blocked_nonce"], + payload["blocked_reason"].as_str().unwrap_or_default(), + payload["kind"].as_str().unwrap_or_default(), + payload["memo"].as_str().unwrap_or_default(), + payload["delivery_state"].as_str().unwrap_or_default(), + ) +} + +fn print_claim_hint(payload: &Value) -> Result<()> { + let recipient = payload["recipient"].as_str().unwrap_or_default(); + let receipt = payload["receipt"].as_str().unwrap_or_default(); + if payload["recovery_mode"].as_str() == Some("originator") { + sh_println!( + "\nClaim target: originator recovery reroutes funds, so do not default to the blocked recipient. Claim to an address that can receive the token:\n cast receive-policy claim {receipt}" + ) + } else if payload["recipient_is_virtual"].as_bool().unwrap_or_default() { + sh_println!( + "\nClaim target: recipient is a virtual address; resolve it first with:\n cast vaddr resolve {recipient}\nThen claim to the registered master address:\n cast receive-policy claim {receipt}" + ) + } else { + sh_println!("\nClaim path: cast receive-policy claim {recipient} {receipt}") + } +} + +fn print_payload(payload: Value, human: F) -> Result<()> +where + F: FnOnce(&Value) -> Result<()>, +{ + if shell::is_json() { + print_json_success(payload)?; + } else { + human(&payload)?; + } + Ok(()) +} + +fn recovery_mode(recovery_authority: Address) -> &'static str { + if recovery_authority == Address::ZERO { "originator" } else { "authority" } +} + +const fn policy_type(policy_type: ITIP403Registry::PolicyType) -> &'static str { + match policy_type { + ITIP403Registry::PolicyType::WHITELIST => "whitelist", + ITIP403Registry::PolicyType::BLACKLIST => "blacklist", + ITIP403Registry::PolicyType::COMPOUND => "compound", + _ => "unknown", + } +} + +const fn blocked_reason(reason: ITIP403Registry::BlockedReason) -> &'static str { + match reason { + ITIP403Registry::BlockedReason::NONE => "none", + ITIP403Registry::BlockedReason::TOKEN_FILTER => "token_filter", + ITIP403Registry::BlockedReason::RECEIVE_POLICY => "receive_policy", + _ => "unknown", + } +} + +const fn blocked_reason_u8(reason: u8) -> &'static str { + match reason { + 0 => "none", + 1 => "token_filter", + 2 => "receive_policy", + _ => "unknown", + } +} + +const fn inbound_kind(kind: IReceivePolicyGuard::InboundKind) -> &'static str { + match kind { + IReceivePolicyGuard::InboundKind::TRANSFER => "transfer", + IReceivePolicyGuard::InboundKind::MINT => "mint", + _ => "unknown", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, b256}; + use tempo_primitives::{MasterId, UserTag}; + + fn sample_receipt() -> Bytes { + IReceivePolicyGuard::ClaimReceiptV1::new( + address!("0000000000000000000000000000000000000010"), + address!("0000000000000000000000000000000000000020"), + address!("0000000000000000000000000000000000000030"), + address!("0000000000000000000000000000000000000040"), + 1_780_000_000, + 7, + ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8, + IReceivePolicyGuard::InboundKind::TRANSFER, + b256!("0000000000000000000000000000000000000000000000000000000000000042"), + ) + .abi_encode() + .into() + } + + #[test] + fn decodes_guard_claim_receipt() { + let receipt = sample_receipt(); + let decoded = decode_claim_receipt(&receipt).unwrap(); + assert_eq!(decoded.version, 1); + assert_eq!(decoded.token, address!("0000000000000000000000000000000000000010")); + assert_eq!(decoded.recoveryAuthority, address!("0000000000000000000000000000000000000020")); + assert_eq!(decoded.originator, address!("0000000000000000000000000000000000000030")); + assert_eq!(decoded.recipient, address!("0000000000000000000000000000000000000040")); + assert_eq!(decoded.blockedNonce, 7); + assert_eq!(decoded.kind, IReceivePolicyGuard::InboundKind::TRANSFER); + } + + #[test] + fn rejects_invalid_guard_claim_receipt() { + let err = decode_claim_receipt(&Bytes::from_static(&[0xde, 0xad])).unwrap_err(); + assert!(err.to_string().contains("invalid ReceivePolicyGuard claim receipt")); + } + + #[test] + fn rejects_semantically_invalid_guard_claim_receipts() { + let receipt = sample_receipt(); + let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(&receipt).unwrap(); + + let mut bad_version = decoded.clone(); + bad_version.version = 2; + let err = decode_claim_receipt(&bad_version.abi_encode().into()).unwrap_err(); + assert!(err.to_string().contains("unsupported ReceivePolicyGuard claim receipt version")); + + let mut bad_token = decoded.clone(); + bad_token.token = Address::ZERO; + let err = decode_claim_receipt(&bad_token.abi_encode().into()).unwrap_err(); + assert!(err.to_string().contains("token is zero")); + + let mut bad_recipient = decoded.clone(); + bad_recipient.recipient = RECEIVE_POLICY_GUARD_ADDRESS; + let err = decode_claim_receipt(&bad_recipient.abi_encode().into()).unwrap_err(); + assert!(err.to_string().contains("recipient cannot be the guard precompile")); + + let mut bad_reason = decoded; + bad_reason.blockedReason = ITIP403Registry::BlockedReason::NONE as u8; + let err = decode_claim_receipt(&bad_reason.abi_encode().into()).unwrap_err(); + assert!(err.to_string().contains("blocked reason is not claimable")); + } + + #[test] + fn validate_payload_records_effective_receiver() { + let receiver = Address::new_virtual( + MasterId::from([0x12, 0x34, 0x56, 0x78]), + UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]), + ); + let effective_receiver = address!("0000000000000000000000000000000000000040"); + + let payload = validate_payload( + address!("0000000000000000000000000000000000000010"), + address!("0000000000000000000000000000000000000030"), + receiver, + effective_receiver, + false, + ITIP403Registry::BlockedReason::RECEIVE_POLICY, + "held", + ); + + assert_eq!(payload["receiver"], format!("{receiver}")); + assert_eq!(payload["effective_receiver"], format!("{effective_receiver}")); + assert_eq!(payload["receiver_was_resolved"], true); + assert_eq!(payload["authorized"], false); + assert_eq!(payload["blocked_reason"], "receive_policy"); + assert_eq!(payload["delivery_state"], "held"); + } + + #[test] + fn receipt_payload_preserves_delivery_state_confidence() { + let receipt = sample_receipt(); + let decoded = decode_claim_receipt(&receipt).unwrap(); + + let unknown = receipt_payload(&receipt, &decoded, None); + assert_eq!(unknown["delivery_state"], "unknown"); + + let held = receipt_payload(&receipt, &decoded, Some(U256::from(1))); + assert_eq!(held["delivery_state"], "held"); + assert_eq!(held["blocked_reason"], "receive_policy"); + assert_eq!(held["kind"], "transfer"); + assert_eq!(held["held_balance"], "1"); + assert_eq!(held["recipient_is_virtual"], false); + assert_eq!(held["claim_target"], format!("{}", decoded.recipient)); + + let not_held = receipt_payload(&receipt, &decoded, Some(U256::ZERO)); + assert_eq!(not_held["delivery_state"], "not_held"); + assert_eq!(not_held["held_balance"], "0"); + } + + #[test] + fn virtual_receipt_recipient_requires_resolved_claim_target() { + let receipt = sample_receipt(); + let mut decoded = decode_claim_receipt(&receipt).unwrap(); + decoded.recipient = Address::new_virtual( + MasterId::from([0x12, 0x34, 0x56, 0x78]), + UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]), + ); + + let payload = receipt_payload(&receipt, &decoded, None); + assert_eq!(payload["recipient"], format!("{}", decoded.recipient)); + assert_eq!(payload["recipient_is_virtual"], true); + assert_eq!(payload["claim_target"], Value::Null); + } + + #[test] + fn originator_recovery_receipt_requires_explicit_claim_target() { + let receipt = sample_receipt(); + let mut decoded = decode_claim_receipt(&receipt).unwrap(); + decoded.recoveryAuthority = Address::ZERO; + + let payload = receipt_payload(&receipt, &decoded, None); + assert_eq!(payload["recovery_mode"], "originator"); + assert_eq!(payload["recipient_is_virtual"], false); + assert_eq!(payload["claim_target"], Value::Null); + } + + #[test] + fn originator_recovery_takes_precedence_over_virtual_recipient() { + let receipt = sample_receipt(); + let mut decoded = decode_claim_receipt(&receipt).unwrap(); + decoded.recoveryAuthority = Address::ZERO; + decoded.recipient = Address::new_virtual( + MasterId::from([0x12, 0x34, 0x56, 0x78]), + UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]), + ); + + let payload = receipt_payload(&receipt, &decoded, None); + assert_eq!(payload["recovery_mode"], "originator"); + assert_eq!(payload["recipient_is_virtual"], true); + assert_eq!(payload["claim_target"], Value::Null); + } + + #[test] + fn preview_calldata_uses_set_receive_policy_selector() { + let call = ITIP403Registry::setReceivePolicyCall { + senderPolicyId: 0, + tokenFilterId: 1, + recoveryAuthority: Address::ZERO, + }; + let calldata = call.abi_encode(); + assert_eq!(&calldata[..4], ITIP403Registry::setReceivePolicyCall::SELECTOR); + } +} diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index 269f5a73c5dd2..6a1705f932c56 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -180,7 +180,7 @@ impl Tip20Subcommand { } } -pub(super) async fn resolve_tip20_signer( +pub(crate) async fn resolve_tip20_signer( send_tx: &SendTxOpts, tx_params: &TxParams, ) -> eyre::Result<(Option, Option)> { @@ -196,7 +196,7 @@ pub(super) async fn resolve_tip20_signer( tempo::resolve_session_or_wallet_signer(&tx_params.tempo, &send_tx.eth.wallet, chain.id()).await } -pub(super) async fn send_tip20_transaction( +pub(crate) async fn send_tip20_transaction( to: NameOrAddress, sig: &'static str, args: Vec, diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index 1ada81ab5d052..f5f14292973c2 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -18,6 +18,7 @@ use crate::cmd::{ keychain::{KeyAuthorizationSubcommand, KeychainSubcommand}, logs::LogsArgs, mktx::MakeTxArgs, + receive_policy::ReceivePolicySubcommand, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, @@ -1257,6 +1258,13 @@ pub enum CastSubcommand { command: Tip20Subcommand, }, + /// Account-level receive policy operations (Tempo). + #[command(name = "receive-policy")] + ReceivePolicy { + #[command(subcommand)] + command: ReceivePolicySubcommand, + }, + /// Tempo keychain (access key) management. #[command(visible_alias = "kc")] Keychain { From 46ba762e19708afac7deaea0ff59b5926b9ced64 Mon Sep 17 00:00:00 2001 From: stevencartavia <112043913+stevencartavia@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:46:05 -0600 Subject: [PATCH 24/25] feat: add T6 admin access key authorization tooling (#15193) --- crates/cast/src/cmd/keychain.rs | 750 +++++++++++++++++++++++++++-- crates/cast/tests/cli/keychain.rs | 357 +++++++++++++- crates/common/src/tempo/auth.rs | 111 ++++- crates/common/src/tempo/session.rs | 73 +++ 4 files changed, 1248 insertions(+), 43 deletions(-) diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index 46588073c0bb1..85f2eb6e3981f 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -4,7 +4,7 @@ use foundry_wallets::BrowserWalletOpts; use std::time::Duration; use alloy_network::{EthereumWallet, TransactionBuilder}; -use alloy_primitives::{Address, B256, U256, hex}; +use alloy_primitives::{Address, B256, Bytes, U256, hex}; use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder}; use alloy_rlp::Encodable; use alloy_rpc_types::BlockId; @@ -38,8 +38,11 @@ use tempo_contracts::precompiles::{ self, CallScope, KeyInfo, KeyRestrictions, LegacyTokenLimit, SelectorRule, SignatureType, TokenLimit, }, - ITIP20, PATH_USD_ADDRESS, - account_keychain::{authorizeKeyCall, authorizeKeyWithWitnessCall, legacyAuthorizeKeyCall}, + ISignatureVerifier, ITIP20, PATH_USD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, + account_keychain::{ + authorizeAdminKeyCall, authorizeKeyCall, authorizeKeyWithWitnessCall, + legacyAuthorizeKeyCall, + }, }; use tempo_primitives::transaction::{ CallScope as AuthCallScope, KeyAuthorization, PrimitiveSignature, @@ -179,9 +182,19 @@ pub enum KeychainSubcommand { /// Optional TIP-1053 witness to bind to this on-chain authorization. /// /// `0x000...000` is a valid present witness and is distinct from omitting the flag. + /// + /// For `--admin`, the `authorizeAdminKey` precompile always takes a witness; omitting + /// this flag submits `bytes32(0)` (which fails if that witness is already burned). #[arg(long)] witness: Option, + /// Authorize a T6 admin access key via `authorizeAdminKey` (key-management only). + /// + /// Admin keys may authorize/revoke other keys but cannot carry an expiry, spending limits, + /// or call scopes. The account is the signing (precompile caller) account. + #[arg(long)] + admin: bool, + #[command(flatten)] tx: TransactionOpts, @@ -228,6 +241,58 @@ pub enum KeychainSubcommand { rpc: RpcOpts, }, + /// Check whether a key is the root key or an active admin key for an account (T6). + #[command(name = "is-admin")] + IsAdmin { + /// The account (root) address. + account: Address, + + /// The key address to check. + key_address: Address, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Verify a Tempo keychain signature against an account's active access key (T6). + /// + /// `signature` must be an encoded Tempo keychain signature (not a raw 65-byte secp256k1 + /// signature). Returns true only for an active access key, never the root key. The supplied + /// `hash` should already be domain-separated by the caller. + Verify { + /// The expected (root) account that embeds the key. + account: Address, + + /// The 32-byte message hash that was signed. + hash: B256, + + /// The encoded Tempo keychain signature. + signature: Bytes, + + #[command(flatten)] + rpc: RpcOpts, + }, + + /// Verify a Tempo keychain signature against an account's root or admin key (T6). + /// + /// `signature` must be an encoded Tempo keychain signature (not a raw 65-byte secp256k1 + /// signature). Returns true for the root key or an active admin key. `hash` should already be + /// domain-separated by the caller. + #[command(name = "verify-admin")] + VerifyAdmin { + /// The expected (root) account that embeds the key. + account: Address, + + /// The 32-byte message hash that was signed. + hash: B256, + + /// The encoded Tempo keychain signature. + signature: Bytes, + + #[command(flatten)] + rpc: RpcOpts, + }, + /// Query the remaining spending limit for a key on a specific token. #[command(name = "rl", visible_alias = "remaining-limit")] RemainingLimit { @@ -310,19 +375,49 @@ pub enum KeyAuthorizationSubcommand { Encode { #[command(flatten)] authorization: KeyAuthorizationArgs, + + /// Bind this authorization to a target account (T6). + /// + /// Required for `--admin` so the authorization cannot be replayed across accounts sharing + /// the same admin key. May also bind a plain authorization without `--admin`. + #[arg(long, value_name = "ADDRESS")] + account: Option
, }, /// Sign and RLP-encode a Tempo key authorization. + /// + /// With an admin access-key signer the bound account is the key's root and is derived + /// automatically; with a direct signer, `--bind-account` binds to another root. A root-signed + /// `--admin` authorization defaults the account to the signer. Sign { #[command(flatten)] authorization: KeyAuthorizationArgs, + /// Bind this authorization to a target (root) account (T6). + /// + /// Named `--bind-account` to avoid clashing with the wallet keystore `--account` selector. + #[arg(long = "bind-account", value_name = "ADDRESS")] + account: Option
, + #[command(flatten)] wallet: Box, #[command(flatten)] browser: BrowserWalletOpts, }, + + /// Decode and inspect a Tempo key authorization (signed or unsigned). + /// + /// Accepts the hex RLP from `encode` (unsigned) or `sign` (signed) and prints its fields, + /// including the T6 `is_admin`/`account` fields and the recovered signer for signed input. + Inspect { + /// Hex-encoded RLP key authorization (signed or unsigned). + authorization: String, + + /// Expected bound account; rejects a mismatched or replayed account-bound authorization. + #[arg(long, value_name = "ADDRESS")] + account: Option
, + }, } /// Common fields for `cast key-authorization encode` and `cast key-authorization sign`. @@ -366,6 +461,14 @@ pub struct KeyAuthorizationArgs { /// `0x000...000` is a valid present witness and is distinct from omitting the flag. #[arg(long)] witness: Option, + + /// Authorize a T6 admin access key (key-management only). + /// + /// Admin keys may authorize/revoke other keys but cannot carry an expiry, spending limits, or + /// call scopes, and require a bound account (`--account` for `encode`, signer-derived for + /// `sign`). + #[arg(long)] + admin: bool, } /// Higher-level access-key policy editing commands. @@ -669,9 +772,11 @@ impl KeychainSubcommand { scope, scopes_json, witness, + admin, tx, send_tx, } => { + let scopes_present = scopes_json.is_some() || !scope.is_empty(); let all_scopes = if let Some(ScopesJson(json_scopes)) = scopes_json { json_scopes } else { @@ -684,7 +789,9 @@ impl KeychainSubcommand { enforce_limits, limits, all_scopes, + scopes_present, witness, + admin, tx, send_tx, ) @@ -697,6 +804,15 @@ impl KeychainSubcommand { Self::IsWitnessBurned { account, witness, rpc } => { run_is_witness_burned(account, witness, rpc).await } + Self::IsAdmin { account, key_address, rpc } => { + run_is_admin(account, key_address, rpc).await + } + Self::Verify { account, hash, signature, rpc } => { + run_verify_keychain(account, hash, signature, rpc, false).await + } + Self::VerifyAdmin { account, hash, signature, rpc } => { + run_verify_keychain(account, hash, signature, rpc, true).await + } Self::RemainingLimit { wallet_address, key_address, token, rpc } => { run_remaining_limit(wallet_address, key_address, token, rpc).await } @@ -717,9 +833,12 @@ impl KeychainSubcommand { impl KeyAuthorizationSubcommand { pub async fn run(self) -> Result<()> { match self { - Self::Encode { authorization } => run_key_auth_encode(authorization), - Self::Sign { authorization, wallet, browser } => { - run_key_auth_sign(authorization, *wallet, browser).await + Self::Encode { authorization, account } => run_key_auth_encode(authorization, account), + Self::Sign { authorization, account, wallet, browser } => { + run_key_auth_sign(authorization, account, *wallet, browser).await + } + Self::Inspect { authorization, account } => { + run_key_auth_inspect(&authorization, account) } } } @@ -852,6 +971,21 @@ async fn run_inspect( let info: KeyInfo = provider.get_keychain_key(metadata.root_account, key_address).await?; let provisioned = info.keyId != Address::ZERO; let is_t3 = is_tempo_hardfork_active(&provider, TempoHardfork::T3).await?; + let is_t6 = is_tempo_hardfork_active(&provider, TempoHardfork::T6).await?; + + // On T6, `isAdminKey` is authoritative for the root/admin distinction. + let is_admin = if is_t6 { + provider.account_keychain().isAdminKey(metadata.root_account, key_address).call().await? + } else { + false + }; + let role = if key_address == metadata.root_account { + "root" + } else if is_admin { + "admin" + } else { + "limited" + }; let mut limits = Vec::new(); if info.enforceLimits { @@ -912,6 +1046,8 @@ async fn run_inspect( "key_id": key_address.to_string(), "provisioned": provisioned, "type": key_type, + "role": role, + "is_admin": is_admin, "expiry": provisioned.then_some(info.expiry), "expiry_human": provisioned.then(|| format_expiry_for_inspect(info.expiry)), "enforce_limits": info.enforceLimits, @@ -932,6 +1068,7 @@ async fn run_inspect( sh_println!("Root account: {}", metadata.root_account)?; sh_println!("Key id: {key_address}")?; sh_println!("Type: {key_type}")?; + sh_println!("Role: {role}")?; if info.isRevoked { sh_println!("Status: revoked")?; @@ -2651,7 +2788,9 @@ async fn run_authorize( enforce_limits: bool, limits: Vec, allowed_calls: Vec, + scopes_present: bool, witness: Option, + admin: bool, tx_opts: TransactionOpts, send_tx: SendTxOpts, ) -> Result<()> { @@ -2660,6 +2799,33 @@ async fn run_authorize( let config = send_tx.eth.load_config()?; let provider = ProviderBuilder::::from_config(&config)?.build()?; + // T6 admin keys are key-management only and use a dedicated precompile entrypoint. + if admin { + if !is_tempo_hardfork_active(&provider, TempoHardfork::T6).await? { + eyre::bail!("--admin requires a Tempo T6-capable AccountKeychain RPC"); + } + // u64::MAX is the no-expiry default; anything else is an explicit expiry admin keys reject. + eyre::ensure!(expiry == u64::MAX, "--admin cannot be combined with an explicit --expiry"); + eyre::ensure!( + !enforce, + "--admin cannot be combined with spending limits (--enforce-limits / --limit)" + ); + eyre::ensure!( + !scopes_present, + "--admin cannot be combined with call scopes (--scope / --scopes)" + ); + + // `authorizeAdminKey` requires a witness argument; omitting `--witness` submits bytes32(0). + let calldata = authorizeAdminKeyCall { + keyId: key_address, + signatureType: key_type, + witness: witness.unwrap_or(B256::ZERO), + } + .abi_encode(); + send_keychain_tx(calldata, tx_opts, &send_tx, None).await?; + return Ok(()); + } + let is_t3 = is_tempo_hardfork_active(&provider, TempoHardfork::T3).await?; if witness.is_some() && !is_tempo_hardfork_active(&provider, TempoHardfork::T5).await? { eyre::bail!("--witness requires a Tempo T5-capable AccountKeychain RPC"); @@ -2716,8 +2882,8 @@ async fn run_authorize( Ok(()) } -fn run_key_auth_encode(args: KeyAuthorizationArgs) -> Result<()> { - let authorization = args.into_authorization()?; +fn run_key_auth_encode(args: KeyAuthorizationArgs, account: Option
) -> Result<()> { + let authorization = args.into_authorization(account)?; let encoded = encode_key_authorization(&authorization); if shell::is_json() { @@ -2725,6 +2891,9 @@ fn run_key_auth_encode(args: KeyAuthorizationArgs) -> Result<()> { "key_authorization": hex::encode_prefixed(&encoded), "signature_hash": authorization.signature_hash().to_string(), "rlp_length": encoded.len(), + "is_admin": authorization.is_admin(), + "account": authorization.account.map(|account| account.to_string()), + "witness": authorization.witness().map(|witness| witness.to_string()), }); sh_println!("{}", serde_json::to_string_pretty(&json)?)?; } else { @@ -2736,22 +2905,27 @@ fn run_key_auth_encode(args: KeyAuthorizationArgs) -> Result<()> { async fn run_key_auth_sign( args: KeyAuthorizationArgs, + account: Option
, wallet: WalletOpts, browser: BrowserWalletOpts, ) -> Result<()> { - let authorization = args.into_authorization()?; + let is_admin = args.admin; - // TODO: remove this check once browser supports T5 KeyAuthorization fields - if browser.browser && authorization.witness().is_some() { - eyre::bail!("browser key authorization signing does not support T5 fields yet: witness"); + // TODO: remove this check once browser supports T5/T6 KeyAuthorization fields. Guard before + // `browser.run()` so the browser flow never starts for unsupported authorizations. + if browser.browser && (args.witness.is_some() || is_admin || account.is_some()) { + eyre::bail!( + "browser key authorization signing does not support T5/T6 fields yet: witness, admin, account" + ); } - let authorized_key_type = auth_signature_type_name(&authorization.key_type); - let signature_hash = authorization.signature_hash(); - if let Some(browser) = browser.run::().await? { let signer_address = browser.address(); ensure_key_authorization_root_sender(signer_address, wallet.from)?; + // The browser path rejects admin/witness/account above, so there is nothing to bind. + let authorization = args.into_authorization(None)?; + let authorized_key_type = auth_signature_type_name(&authorization.key_type); + let signature_hash = authorization.signature_hash(); let signed = browser.sign_key_authorization(authorization).await?; return print_signed_key_authorization( &signed, @@ -2762,19 +2936,37 @@ async fn run_key_auth_sign( } let (signer, tempo_access_key) = wallet.maybe_signer().await?; - if tempo_access_key.is_some() { - eyre::bail!( - "Tempo access keys cannot sign key authorizations; use a persistent root signer" - ); - } let signer = signer.ok_or_else(|| { eyre::eyre!( - "a persistent root signer is required to sign key authorizations; pass a signer with \ + "a signer is required to sign key authorizations; pass a signer with \ --browser, --private-key, --keystore, Ledger, Trezor, AWS, GCP, or Turnkey" ) })?; let signer_address = signer.address(); - ensure_key_authorization_root_sender(signer_address, wallet.from)?; + + // Resolve the account this authorization is bound to (T6 replay protection). + let bound_account = if let Some(access_key) = tempo_access_key.as_ref() { + // The access key (an admin key) signs for its root, so bind to the root, not the signer. + if let Some(explicit) = account { + eyre::ensure!( + explicit == access_key.wallet_address, + "--bind-account {explicit} does not match the selected Tempo access key's root account {}", + access_key.wallet_address, + ); + } + Some(access_key.wallet_address) + } else { + ensure_key_authorization_root_sender(signer_address, wallet.from)?; + match account { + Some(explicit) => Some(explicit), + None if is_admin => Some(signer_address), + None => None, + } + }; + + let authorization = args.into_authorization(bound_account)?; + let authorized_key_type = auth_signature_type_name(&authorization.key_type); + let signature_hash = authorization.signature_hash(); let signature = signer.sign_hash(&signature_hash).await?; let signed = authorization.into_signed(PrimitiveSignature::Secp256k1(signature)); print_signed_key_authorization(&signed, signature_hash, signer_address, authorized_key_type) @@ -2798,6 +2990,8 @@ fn print_signed_key_authorization( "authorized_key_type": authorized_key_type, "signature_type": signature_type, "witness": signed.authorization.witness().map(|witness| witness.to_string()), + "is_admin": signed.authorization.is_admin(), + "account": signed.authorization.account.map(|account| account.to_string()), }); sh_println!("{}", serde_json::to_string_pretty(&json)?)?; } else { @@ -2813,8 +3007,125 @@ fn encode_key_authorization(authorization: &T) -> Vec { out } +/// Decode a hex RLP key authorization (signed or unsigned) and validate its account binding. +/// +/// Tries the signed shape first, then the unsigned one. Returns the authorization, whether the +/// input was signed, and the best-effort recovered signer. When `expected_account` is set the +/// decoded authorization must be bound to exactly that account. +fn decode_and_validate_key_authorization( + authorization: &str, + expected_account: Option
, +) -> Result<(KeyAuthorization, bool, Option
)> { + let raw = authorization.trim(); + + let (auth, signed, signer) = + match tempo::decode_key_authorization::(raw) { + // Signer recovery is best-effort so `inspect` still surfaces fields for a corrupt sig. + Ok(signed) => { + let signer = signed.recover_signer().ok(); + (signed.authorization, true, signer) + } + Err(signed_err) => match tempo::decode_key_authorization::(raw) { + Ok(unsigned) => (unsigned, false, None), + Err(unsigned_err) => eyre::bail!( + "could not decode key authorization as signed ({signed_err}) or unsigned \ + ({unsigned_err})" + ), + }, + }; + + // Mirror the chain's T6 admin invariants so `inspect` rejects a malformed admin authorization. + if let Some(account) = auth.account { + eyre::ensure!( + account != Address::ZERO, + "key authorization account cannot be the zero address" + ); + } + if auth.is_admin() { + // A root-signed admin authorization may omit `account` (it is only required when the signer + // is not the target root), so `inspect` does not require it here. Binding is still enforced + // below when `--account` is supplied. + eyre::ensure!(auth.expiry.is_none(), "admin key authorization cannot carry an expiry"); + eyre::ensure!( + auth.limits.is_none(), + "admin key authorization cannot carry spending limits" + ); + eyre::ensure!( + auth.allowed_calls.is_none(), + "admin key authorization cannot carry call scopes" + ); + } + + // `--account` rejects a replayed or mismatched account-bound authorization. + if let Some(expected) = expected_account { + match auth.account { + Some(account) if account == expected => {} + Some(account) => eyre::bail!( + "key authorization is bound to account {account} but {expected} was expected" + ), + None => eyre::bail!( + "expected key authorization bound to account {expected} but it has no account field" + ), + } + } + + Ok((auth, signed, signer)) +} + +/// `cast key-authorization inspect` — decode a signed or unsigned key authorization and print its +/// fields, including the T6 `is_admin` / `account` fields. +fn run_key_auth_inspect(authorization: &str, expected_account: Option
) -> Result<()> { + let (auth, signed, signer) = + decode_and_validate_key_authorization(authorization, expected_account)?; + + if shell::is_json() { + let json = serde_json::json!({ + "signed": signed, + "signer": signer.map(|signer| signer.to_string()), + "chain_id": auth.chain_id, + "key_address": auth.key_id.to_string(), + "key_type": auth_signature_type_name(&auth.key_type), + "is_admin": auth.is_admin(), + "account": auth.account.map(|account| account.to_string()), + "expiry": auth.expiry.map(|expiry| expiry.get()), + "witness": auth.witness().map(|witness| witness.to_string()), + "enforce_limits": auth.limits.is_some(), + "scoped_calls": auth.allowed_calls.is_some(), + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + } else { + sh_println!("Signed: {signed}")?; + if let Some(signer) = signer { + sh_println!("Signer: {signer}")?; + } + sh_println!("Chain ID: {}", auth.chain_id)?; + sh_println!("Key Address: {}", auth.key_id)?; + sh_println!("Key Type: {}", auth_signature_type_name(&auth.key_type))?; + sh_println!("Admin: {}", auth.is_admin())?; + if let Some(account) = auth.account { + sh_println!("Account: {account}")?; + } + match auth.expiry { + Some(expiry) => sh_println!("Expiry: {}", expiry.get())?, + None => sh_println!("Expiry: none")?, + } + match auth.witness() { + Some(witness) => sh_println!("Witness: {witness}")?, + None => sh_println!("Witness: none")?, + } + sh_println!("Enforce Lim: {}", auth.limits.is_some())?; + sh_println!("Scoped Calls: {}", auth.allowed_calls.is_some())?; + } + + Ok(()) +} + impl KeyAuthorizationArgs { - fn into_authorization(self) -> Result { + /// Build a [`KeyAuthorization`] from these args, binding it to `account` when present. + /// + /// For `--admin`, `account` must be `Some` (passed via `--account` for `encode`, or derived + /// from the root signer for `sign`). + fn into_authorization(self, account: Option
) -> Result { let (scopes, explicit_scopes_json) = if let Some(AuthScopesJson(json_scopes)) = self.scopes_json { (json_scopes, true) @@ -2822,6 +3133,15 @@ impl KeyAuthorizationArgs { (self.scope, false) }; + let scopes_present = explicit_scopes_json || !scopes.is_empty(); + validate_admin_key_authorization( + self.admin, + account, + self.expiry.is_some(), + self.enforce_limits || !self.limits.is_empty(), + scopes_present, + )?; + let mut authorization = KeyAuthorization::unrestricted(self.chain_id, self.key_type, self.key_address); @@ -2836,7 +3156,7 @@ impl KeyAuthorizationArgs { authorization = authorization.with_limits(self.limits); } - if explicit_scopes_json || !scopes.is_empty() { + if scopes_present { authorization = authorization.with_allowed_calls(scopes); } @@ -2844,10 +3164,50 @@ impl KeyAuthorizationArgs { authorization = authorization.with_witness(witness); } + // Apply T6 admin / account binding last, after the restriction fields are validated above. + if self.admin { + // `validate_admin_key_authorization` guarantees `account` is `Some` here. + authorization = authorization.into_admin(account.expect("admin requires account")); + } else if let Some(account) = account { + authorization = authorization.with_account(account); + } + Ok(authorization) } } +/// Enforce the T6 admin access-key invariants when constructing a key authorization. +/// +/// Admin keys are key-management only: no expiry, spending limits, or call scopes, and they must be +/// bound to a target account (to prevent cross-account replay). A TIP-1053 witness is still +/// allowed. +fn validate_admin_key_authorization( + admin: bool, + account: Option
, + has_expiry: bool, + has_limits: bool, + has_scopes: bool, +) -> Result<()> { + if let Some(account) = account { + eyre::ensure!(account != Address::ZERO, "--account cannot be the zero address"); + } + + if admin { + eyre::ensure!(account.is_some(), "--admin requires --account"); + eyre::ensure!(!has_expiry, "--admin cannot be combined with --expiry"); + eyre::ensure!( + !has_limits, + "--admin cannot be combined with spending limits (--enforce-limits / --limit)" + ); + eyre::ensure!( + !has_scopes, + "--admin cannot be combined with call scopes (--scope / --scopes)" + ); + } + + Ok(()) +} + /// `cast keychain revoke` / `cast keychain rev` — revoke a key on-chain. async fn run_revoke( key_address: Address, @@ -2904,6 +3264,68 @@ async fn run_is_witness_burned(account: Address, witness: B256, rpc: RpcOpts) -> Ok(()) } +/// `cast keychain is-admin` — check whether a key is the root or an active admin key (T6). +async fn run_is_admin(account: Address, key_address: Address, rpc: RpcOpts) -> Result<()> { + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + if !is_tempo_hardfork_active(&provider, TempoHardfork::T6).await? { + eyre::bail!("is-admin requires a Tempo T6-capable AccountKeychain RPC"); + } + + let is_admin = provider.account_keychain().isAdminKey(account, key_address).call().await?; + + if shell::is_json() { + let json = serde_json::json!({ + "account": account.to_string(), + "key_address": key_address.to_string(), + "is_admin": is_admin, + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + } else { + sh_println!("{is_admin}")?; + } + + Ok(()) +} + +/// `cast keychain verify` / `verify-admin` — verify a Tempo keychain signature (T6). +async fn run_verify_keychain( + account: Address, + hash: B256, + signature: Bytes, + rpc: RpcOpts, + admin: bool, +) -> Result<()> { + let config = rpc.load_config()?; + let provider = ProviderBuilder::::from_config(&config)?.build()?; + let command = if admin { "verify-admin" } else { "verify" }; + if !is_tempo_hardfork_active(&provider, TempoHardfork::T6).await? { + eyre::bail!("{command} requires a Tempo T6-capable SignatureVerifier RPC"); + } + + let verifier = ISignatureVerifier::new(SIGNATURE_VERIFIER_ADDRESS, &provider); + let valid = if admin { + verifier.verifyKeychainAdmin(account, hash, signature.clone()).call().await? + } else { + verifier.verifyKeychain(account, hash, signature.clone()).call().await? + }; + + if shell::is_json() { + let json = serde_json::json!({ + "account": account.to_string(), + "hash": hash.to_string(), + "signature": signature.to_string(), + "admin": admin, + "valid": valid, + }); + sh_println!("{}", serde_json::to_string_pretty(&json)?)?; + } else { + sh_println!("{valid}")?; + } + + Ok(()) +} + /// `cast keychain rl` — query remaining spending limit. async fn run_remaining_limit( wallet_address: Address, @@ -3093,11 +3515,18 @@ pub(crate) async fn resolve_keychain_root_signer( return Ok(KeychainRootSigner::Browser(browser)); } + // The T6 spec allows an active admin access key to authorize/revoke other keys, but submitting + // these AccountKeychain mutators as access-key-signed precompile calldata reverts on-chain with + // `UnauthorizedCaller()` on the pinned Tempo build (gas estimation succeeds because it injects + // an override key id, while real execution recovers the signer). Reject before broadcasting + // rather than emitting a guaranteed-revert transaction; use a root signer for direct mutations. if tempo_access_key.is_some() { eyre::bail!( - "keychain policy changes must be signed by the root account; the selected `--from` \ - resolved to a Tempo access key. Use `--browser` for passkey roots, or pass a root \ - account signer with `--private-key`, `--keystore`, Ledger, Trezor, AWS, GCP, or Turnkey." + "submitting AccountKeychain admin mutators (authorize / revoke / policy) signed by a \ + Tempo access key currently reverts on-chain with UnauthorizedCaller() on the pinned \ + Tempo build, even for an active admin key. Use a root account signer (--browser for \ + passkey roots, or --private-key / --keystore / Ledger / Trezor / AWS / GCP / Turnkey) \ + for direct mutations." ); } @@ -3665,15 +4094,23 @@ fn print_key_entry(entry: &tempo::KeyEntry) -> Result<()> { sh_println!("Expiry: {}", format_expiry(expiry))?; } + let decoded = decoded_entry_key_authorization(entry); + let is_admin = decoded.as_ref().is_some_and(|signed| signed.authorization.is_admin()); + sh_println!("Role: {}", local_key_role(entry, is_admin))?; + sh_println!("Has Key: {}", entry.has_inline_key())?; sh_println!("Has Auth: {}", entry.key_authorization.is_some())?; - if let Some(signed) = decoded_entry_key_authorization(entry) { + if let Some(signed) = &decoded { let witness = signed .authorization .witness() .map(|witness| witness.to_string()) .unwrap_or_else(|| "(none)".to_string()); sh_println!("Auth Witness: {witness}")?; + sh_println!("Auth Admin: {}", signed.authorization.is_admin())?; + if let Some(account) = signed.authorization.account { + sh_println!("Auth Account: {account}")?; + } } if !entry.limits.is_empty() { @@ -3688,9 +4125,16 @@ fn print_key_entry(entry: &tempo::KeyEntry) -> Result<()> { fn key_entry_to_json(entry: &tempo::KeyEntry) -> serde_json::Value { let is_direct = entry.key_address.is_none_or(|key_address| key_address == entry.wallet_address); - let authorization_witness = decoded_entry_key_authorization(entry) - .and_then(|signed| signed.authorization.witness()) - .map(|witness| witness.to_string()); + let decoded = decoded_entry_key_authorization(entry); + let authorization_witness = + decoded.as_ref().and_then(|signed| signed.authorization.witness()).map(|w| w.to_string()); + let authorization_is_admin = + decoded.as_ref().is_some_and(|signed| signed.authorization.is_admin()); + let authorization_account = decoded + .as_ref() + .and_then(|signed| signed.authorization.account) + .map(|account| account.to_string()); + let role = local_key_role(entry, authorization_is_admin); let limits: Vec<_> = entry .limits @@ -3714,11 +4158,30 @@ fn key_entry_to_json(entry: &tempo::KeyEntry) -> serde_json::Value { "expiry_human": entry.expiry.map(format_expiry), "has_key": entry.has_inline_key(), "has_authorization": entry.key_authorization.is_some(), + "role": role, "authorization_witness": authorization_witness, + "authorization_is_admin": authorization_is_admin, + "authorization_account": authorization_account, "limits": limits, }) } +/// Classify a local key entry's role for display. +/// +/// `root` when the key is the account EOA itself, `admin` when a decoded local authorization marks +/// the key as a T6 admin key, otherwise `access`. This reflects only locally available data; use +/// `keychain is-admin` for the authoritative on-chain role. +fn local_key_role(entry: &tempo::KeyEntry, is_admin: bool) -> &'static str { + let is_direct = entry.key_address.is_none_or(|key_address| key_address == entry.wallet_address); + if is_direct { + "root" + } else if is_admin { + "admin" + } else { + "limited" + } +} + fn decoded_entry_key_authorization(entry: &tempo::KeyEntry) -> Option { let raw = entry.key_authorization.as_deref()?.trim(); if raw.is_empty() { @@ -3885,6 +4348,7 @@ mod tests { witness, .. }, + .. } => { assert_eq!(chain_id, 4217); assert_eq!(key_address, Address::from_str(key).unwrap()); @@ -3943,6 +4407,7 @@ mod tests { scope: vec![], scopes_json: None, witness: None, + admin: false, } } @@ -3968,10 +4433,10 @@ mod tests { #[test] fn test_key_auth_encode_preserves_zero_witness_presence() { - let absent = key_auth_args().into_authorization().unwrap(); + let absent = key_auth_args().into_authorization(None).unwrap(); let mut args = key_auth_args(); args.witness = Some(B256::ZERO); - let zero_witness = args.into_authorization().unwrap(); + let zero_witness = args.into_authorization(None).unwrap(); assert_eq!(absent.witness(), None); assert_eq!(zero_witness.witness(), Some(B256::ZERO)); @@ -3979,12 +4444,225 @@ mod tests { assert_ne!(encode_key_authorization(&absent), encode_key_authorization(&zero_witness)); } + #[test] + fn test_admin_key_auth_roundtrip_preserves_admin_and_account() { + use alloy_rlp::Decodable; + + let account = target_addr(0xAB); + let mut args = key_auth_args(); + args.admin = true; + let authorization = args.into_authorization(Some(account)).unwrap(); + assert!(authorization.is_admin()); + assert_eq!(authorization.account, Some(account)); + + let signed = authorization.into_signed(PrimitiveSignature::from_bytes(&[0u8; 65]).unwrap()); + let encoded = encode_key_authorization(&signed); + let decoded = SignedKeyAuthorization::decode(&mut encoded.as_slice()).unwrap(); + assert!(decoded.authorization.is_admin()); + assert_eq!(decoded.authorization.account, Some(account)); + } + + #[test] + fn test_account_bound_non_admin_roundtrip() { + use alloy_rlp::Decodable; + + let account = target_addr(0xCD); + let authorization = key_auth_args().into_authorization(Some(account)).unwrap(); + assert!(!authorization.is_admin()); + assert_eq!(authorization.account, Some(account)); + + let signed = authorization.into_signed(PrimitiveSignature::from_bytes(&[0u8; 65]).unwrap()); + let encoded = encode_key_authorization(&signed); + let decoded = SignedKeyAuthorization::decode(&mut encoded.as_slice()).unwrap(); + assert!(!decoded.authorization.is_admin()); + assert_eq!(decoded.authorization.account, Some(account)); + } + + #[test] + fn test_non_admin_authorization_omits_t6_fields() { + // Backward compatibility: a plain authorization must not carry admin/account. + let authorization = key_auth_args().into_authorization(None).unwrap(); + assert!(!authorization.is_admin()); + assert_eq!(authorization.account, None); + assert!(authorization.is_legacy_compatible()); + } + + #[test] + fn test_admin_account_binding_changes_signature_hash() { + let mut admin_a = key_auth_args(); + admin_a.admin = true; + let auth_a = admin_a.into_authorization(Some(target_addr(0x01))).unwrap(); + + let mut admin_b = key_auth_args(); + admin_b.admin = true; + let auth_b = admin_b.into_authorization(Some(target_addr(0x02))).unwrap(); + + // Account binding feeds the signing hash so a signature cannot be replayed across accounts. + assert_ne!(auth_a.signature_hash(), auth_b.signature_hash()); + } + + #[test] + fn test_admin_requires_account() { + let mut args = key_auth_args(); + args.admin = true; + let err = args.into_authorization(None).unwrap_err().to_string(); + assert!(err.contains("--admin requires --account"), "got: {err}"); + } + + #[test] + fn test_admin_rejects_expiry_limits_and_scopes() { + let account = target_addr(0xAB); + + let mut expiry = key_auth_args(); + expiry.admin = true; + expiry.expiry = Some(1_782_647_677); + assert!( + expiry.into_authorization(Some(account)).unwrap_err().to_string().contains("--expiry"), + "expiry must be rejected for admin keys" + ); + + let mut limits = key_auth_args(); + limits.admin = true; + limits.enforce_limits = true; + assert!( + limits + .into_authorization(Some(account)) + .unwrap_err() + .to_string() + .contains("spending limits"), + "spending limits must be rejected for admin keys" + ); + + let mut scopes = key_auth_args(); + scopes.admin = true; + scopes.scopes_json = Some(AuthScopesJson(vec![])); + assert!( + scopes + .into_authorization(Some(account)) + .unwrap_err() + .to_string() + .contains("call scopes"), + "call scopes must be rejected for admin keys" + ); + } + + #[test] + fn test_account_zero_is_rejected() { + let err = key_auth_args().into_authorization(Some(Address::ZERO)).unwrap_err().to_string(); + assert!(err.contains("--account cannot be the zero address"), "got: {err}"); + } + + /// Hex-encode a signed admin authorization bound to `account` for inspect tests. + fn signed_admin_auth_hex(account: Address) -> String { + let mut args = key_auth_args(); + args.admin = true; + let authorization = args.into_authorization(Some(account)).unwrap(); + let signed = authorization.into_signed(PrimitiveSignature::from_bytes(&[0u8; 65]).unwrap()); + hex::encode_prefixed(encode_key_authorization(&signed)) + } + + #[test] + fn test_inspect_decodes_signed_and_unsigned_shapes() { + // Signed admin authorization: reported as signed with its T6 fields exposed. + let account = target_addr(0xAB); + let (auth, signed, _signer) = + decode_and_validate_key_authorization(&signed_admin_auth_hex(account), None).unwrap(); + assert!(signed, "signed input must be reported as signed"); + assert!(auth.is_admin()); + assert_eq!(auth.account, Some(account)); + + // Unsigned authorization: the other RLP shape decodes with no signer. + let unsigned = key_auth_args().into_authorization(None).unwrap(); + let hex = hex::encode_prefixed(encode_key_authorization(&unsigned)); + let (auth, signed, signer) = decode_and_validate_key_authorization(&hex, None).unwrap(); + assert!(!signed, "unsigned input must be reported as unsigned"); + assert!(!auth.is_admin()); + assert_eq!(auth.account, None); + assert!(signer.is_none(), "unsigned input must not recover a signer"); + } + + #[test] + fn test_inspect_account_mismatch_is_rejected() { + let account = target_addr(0xAB); + let hex = signed_admin_auth_hex(account); + let err = decode_and_validate_key_authorization(&hex, Some(target_addr(0xCD))) + .unwrap_err() + .to_string(); + assert!(err.contains("is bound to account") && err.contains("was expected"), "got: {err}"); + } + + #[test] + fn test_inspect_rejects_admin_auth_carrying_restrictions() { + // Build an admin authorization that carries an expiry directly (bypassing the CLI + // constructor's guard) to prove `inspect` mirrors the chain's admin invariants. + let account = target_addr(0xAB); + let authorization = + KeyAuthorization::unrestricted(31337, AuthSignatureType::Secp256k1, target_addr(0x42)) + .with_expiry(1_782_647_677) + .into_admin(account); + let hex = hex::encode_prefixed(encode_key_authorization(&authorization)); + + let err = decode_and_validate_key_authorization(&hex, None).unwrap_err().to_string(); + assert!(err.contains("cannot carry an expiry"), "got: {err}"); + } + + #[test] + fn test_inspect_accepts_root_signed_admin_auth_without_account() { + // T6 allows a root-signed admin authorization to omit `account` (account is only required + // when the signer is not the target root). `inspect` is a decoder and must not reject it. + let mut authorization = + KeyAuthorization::unrestricted(31337, AuthSignatureType::Secp256k1, target_addr(0x42)); + authorization.is_admin = true; + let hex = hex::encode_prefixed(encode_key_authorization(&authorization)); + + let (auth, _signed, _signer) = + decode_and_validate_key_authorization(&hex, None).expect("admin auth may omit account"); + assert!(auth.is_admin()); + assert_eq!(auth.account, None); + } + + #[test] + fn test_inspect_admin_auth_without_account_rejected_when_account_expected() { + // When the caller supplies `--account`, an admin authorization that omits `account` must + // still be rejected: the binding cannot be verified. + let mut authorization = + KeyAuthorization::unrestricted(31337, AuthSignatureType::Secp256k1, target_addr(0x42)); + authorization.is_admin = true; + let hex = hex::encode_prefixed(encode_key_authorization(&authorization)); + + let err = decode_and_validate_key_authorization(&hex, Some(target_addr(0xAB))) + .unwrap_err() + .to_string(); + assert!(err.contains("no account field"), "got: {err}"); + } + + #[test] + fn test_local_key_role_classification() { + let wallet = target_addr(0x01); + let key = target_addr(0x02); + + let root = tempo::KeyEntry { + wallet_address: wallet, + key_address: Some(wallet), + ..Default::default() + }; + assert_eq!(local_key_role(&root, false), "root"); + + let access = tempo::KeyEntry { + wallet_address: wallet, + key_address: Some(key), + ..Default::default() + }; + assert_eq!(local_key_role(&access, false), "limited"); + assert_eq!(local_key_role(&access, true), "admin"); + } + #[test] fn test_key_auth_encode_preserves_explicit_empty_scopes_json() { - let absent = key_auth_args().into_authorization().unwrap(); + let absent = key_auth_args().into_authorization(None).unwrap(); let mut args = key_auth_args(); args.scopes_json = Some(AuthScopesJson(vec![])); - let deny_all = args.into_authorization().unwrap(); + let deny_all = args.into_authorization(None).unwrap(); assert_eq!(absent.allowed_calls, None); assert_eq!(deny_all.allowed_calls, Some(vec![])); @@ -3996,7 +4674,7 @@ mod tests { fn test_key_auth_encode_rejects_zero_expiry() { let mut args = key_auth_args(); args.expiry = Some(0); - let err = args.into_authorization().unwrap_err(); + let err = args.into_authorization(None).unwrap_err(); assert!( err.to_string().contains("--expiry must be greater than zero"), "unexpected error: {err}" diff --git a/crates/cast/tests/cli/keychain.rs b/crates/cast/tests/cli/keychain.rs index 770963dbd1a6d..4ff3c0021d68c 100644 --- a/crates/cast/tests/cli/keychain.rs +++ b/crates/cast/tests/cli/keychain.rs @@ -213,7 +213,7 @@ casttest!(keychain_authorize_sponsor_hash_json_is_object, async |_prj, cmd| { assert_eq!(hash.len(), 66, "sponsor_hash should be 32-byte hex (66 chars), got: {hash}"); }); -// TODO: remove this check once browser supports T5 KeyAuthorization fields +// TODO: remove this check once browser supports T5/T6 KeyAuthorization fields casttest!(key_authorization_sign_rejects_browser_witness_before_browser_run, |_prj, cmd| { let stderr = cmd .args([ @@ -232,8 +232,9 @@ casttest!(key_authorization_sign_rejects_browser_witness_before_browser_run, |_p .stderr_lossy(); assert!( - stderr - .contains("browser key authorization signing does not support T5 fields yet: witness"), + stderr.contains( + "browser key authorization signing does not support T5/T6 fields yet: witness, admin, account" + ), "unexpected stderr:\n{stderr}" ); assert!( @@ -242,6 +243,356 @@ casttest!(key_authorization_sign_rejects_browser_witness_before_browser_run, |_p ); }); +// Offline: signing an admin key authorization round-trips `is_admin`/`account` into the JSON. +casttest!(key_authorization_sign_admin_emits_admin_json, |_prj, cmd| { + // `sign --admin` derives the bound account from the signer (it has no auth-level --account), + // so signing with PK1 binds the authorization to ADDR1. + let output = cmd + .args([ + "key-authorization", + "sign", + accounts::ADDR2, + "--chain-id", + "31337", + "--admin", + "--private-key", + accounts::PK1, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast key-authorization sign --admin --json should emit valid JSON"); + assert_eq!(parsed["is_admin"], serde_json::Value::Bool(true), "got: {output}"); + // The bound account equals the signer (ADDR1), not the authorized key (ADDR2). + assert_eq!( + parsed["account"].as_str().map(str::to_lowercase), + Some(accounts::ADDR1.to_lowercase()), + "got: {output}" + ); + assert!( + parsed["signed_key_authorization"].as_str().is_some_and(|s| s.starts_with("0x")), + "got: {output}" + ); +}); + +// Offline: `--admin` without `--account` is rejected before any signing happens. +casttest!(key_authorization_encode_admin_requires_account, |_prj, cmd| { + let stderr = cmd + .args(["key-authorization", "encode", accounts::ADDR2, "--chain-id", "31337", "--admin"]) + .assert_failure() + .get_output() + .stderr_lossy(); + + assert!(stderr.contains("--admin requires --account"), "unexpected stderr:\n{stderr}"); +}); + +// Offline: an admin key authorization cannot carry an expiry. +casttest!(key_authorization_encode_admin_rejects_expiry, |_prj, cmd| { + let stderr = cmd + .args([ + "key-authorization", + "encode", + accounts::ADDR2, + "--chain-id", + "31337", + "--admin", + "--account", + accounts::ADDR1, + "--expiry", + "1782647677", + ]) + .assert_failure() + .get_output() + .stderr_lossy(); + + assert!(stderr.contains("--expiry"), "unexpected stderr:\n{stderr}"); +}); + +// Offline: `key-authorization inspect` decodes a signed admin authorization and exposes T6 fields. +casttest!(key_authorization_inspect_signed_admin_json, |_prj, cmd| { + let signed = cmd + .args([ + "key-authorization", + "sign", + accounts::ADDR2, + "--chain-id", + "31337", + "--admin", + "--private-key", + accounts::PK1, // root account ADDR1; the bound account is derived from the signer + ]) + .assert_success() + .get_output() + .stdout_lossy(); + let signed = signed.trim().to_string(); + + let output = cmd + .cast_fuse() + .args(["key-authorization", "inspect", &signed, "--json"]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast key-authorization inspect --json should emit valid JSON"); + assert_eq!(parsed["signed"], serde_json::Value::Bool(true), "got: {output}"); + assert_eq!(parsed["is_admin"], serde_json::Value::Bool(true), "got: {output}"); + assert_eq!( + parsed["account"].as_str().map(str::to_lowercase), + Some(accounts::ADDR1.to_lowercase()), + "got: {output}" + ); + assert_eq!( + parsed["signer"].as_str().map(str::to_lowercase), + Some(accounts::ADDR1.to_lowercase()), + "got: {output}" + ); +}); + +// Offline: inspecting with a mismatched `--account` rejects a replayed admin authorization. +casttest!(key_authorization_inspect_account_mismatch_rejected, |_prj, cmd| { + let signed = cmd + .args([ + "key-authorization", + "sign", + accounts::ADDR2, + "--chain-id", + "31337", + "--admin", + "--private-key", + accounts::PK1, // bound to ADDR1 + ]) + .assert_success() + .get_output() + .stdout_lossy(); + let signed = signed.trim().to_string(); + + let stderr = cmd + .cast_fuse() + .args(["key-authorization", "inspect", &signed, "--account", accounts::ADDR2]) + .assert_failure() + .get_output() + .stderr_lossy(); + + assert!(stderr.contains("is bound to account"), "unexpected stderr:\n{stderr}"); +}); + +// On-chain (T6): authorize an admin key, then confirm it via `keychain is-admin --json`. +casttest!(keychain_authorize_admin_then_is_admin, async |_prj, cmd| { + use tempo_chainspec::hardfork::TempoHardfork; + let (_, handle) = + anvil::spawn(NodeConfig::test_tempo().with_hardfork(Some(TempoHardfork::T6.into()))).await; + let rpc = handle.http_endpoint(); + + cmd.cast_fuse() + .args([ + "keychain", + "authorize", + accounts::ADDR2, // admin key to authorize + "--admin", + "--private-key", + accounts::PK1, // root account ADDR1 + "--rpc-url", + &rpc, + ]) + .assert_success(); + + let output = cmd + .cast_fuse() + .args([ + "keychain", + "is-admin", + accounts::ADDR1, // root account + accounts::ADDR2, // admin key + "--rpc-url", + &rpc, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain is-admin --json should emit valid JSON"); + assert!(parsed.is_object(), "expected JSON object, got: {output}"); + assert_eq!(parsed["is_admin"], serde_json::Value::Bool(true), "got: {output}"); +}); + +// On-chain (T6): a keychain signature from an authorized admin key passes `verify-admin`. +casttest!(keychain_verify_admin_accepts_admin_signature, async |_prj, cmd| { + use alloy_primitives::{Address, B256, hex}; + use alloy_signer::SignerSync; + use alloy_signer_local::PrivateKeySigner; + use tempo_chainspec::hardfork::TempoHardfork; + use tempo_primitives::transaction::{KeychainSignature, PrimitiveSignature, TempoSignature}; + + let (_, handle) = + anvil::spawn(NodeConfig::test_tempo().with_hardfork(Some(TempoHardfork::T6.into()))).await; + let rpc = handle.http_endpoint(); + + // Authorize ADDR2 as an admin key for ADDR1 (PK1). + cmd.cast_fuse() + .args([ + "keychain", + "authorize", + accounts::ADDR2, + "--admin", + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + ]) + .assert_success(); + + // Build a keychain signature over an arbitrary hash, signed by the admin key (PK2) for ADDR1. + let account: Address = accounts::ADDR1.parse().unwrap(); + let hash = B256::repeat_byte(0x42); + let key: PrivateKeySigner = accounts::PK2.parse().unwrap(); + let signing_hash = KeychainSignature::signing_hash(hash, account); + let inner = key.sign_hash_sync(&signing_hash).unwrap(); + let signature = TempoSignature::Keychain(KeychainSignature::new( + account, + PrimitiveSignature::Secp256k1(inner), + )) + .to_bytes(); + let signature_hex = hex::encode_prefixed(signature); + + let output = cmd + .cast_fuse() + .args([ + "keychain", + "verify-admin", + accounts::ADDR1, + &hash.to_string(), + &signature_hex, + "--rpc-url", + &rpc, + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast keychain verify-admin --json should emit valid JSON"); + assert_eq!(parsed["valid"], serde_json::Value::Bool(true), "got: {output}"); +}); + +// On-chain (T6): `keychain authorize --admin` rejects spending limits before submitting. +casttest!(keychain_authorize_admin_rejects_limits, async |_prj, cmd| { + use tempo_chainspec::hardfork::TempoHardfork; + let (_, handle) = + anvil::spawn(NodeConfig::test_tempo().with_hardfork(Some(TempoHardfork::T6.into()))).await; + let rpc = handle.http_endpoint(); + + let stderr = cmd + .args([ + "keychain", + "authorize", + accounts::ADDR2, + "--admin", + "--enforce-limits", + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + ]) + .assert_failure() + .get_output() + .stderr_lossy(); + + assert!(stderr.contains("spending limits"), "unexpected stderr:\n{stderr}"); +}); + +// An access-key signer is rejected for admin-gated keychain mutators even when it is an active +// admin key, because submitting the mutator as access-key-signed calldata reverts on-chain with +// `UnauthorizedCaller()` on the pinned Tempo build. +casttest!(keychain_access_key_cannot_submit_admin_mutator, async |_prj, cmd| { + use tempo_chainspec::hardfork::TempoHardfork; + let (_, handle) = + anvil::spawn(NodeConfig::test_tempo().with_hardfork(Some(TempoHardfork::T6.into()))).await; + let rpc = handle.http_endpoint(); + + // Root ADDR1 (PK1) authorizes ADDR2 (PK2) as an admin key. + cmd.cast_fuse() + .args([ + "keychain", + "authorize", + accounts::ADDR2, + "--admin", + "--private-key", + accounts::PK1, + "--rpc-url", + &rpc, + ]) + .assert_success(); + + // ADDR2 is an active admin key, but the access-key mutator is still rejected locally. + let stderr = cmd + .cast_fuse() + .args([ + "keychain", + "authorize", + accounts::ADDR3, + "--tempo.access-key", + accounts::PK2, + "--tempo.root-account", + accounts::ADDR1, + "--rpc-url", + &rpc, + ]) + .assert_failure() + .get_output() + .stderr_lossy(); + + assert!( + stderr.contains("currently reverts on-chain with UnauthorizedCaller()"), + "unexpected stderr:\n{stderr}" + ); +}); + +// Offline (T6): an admin access key signing a child authorization binds the authorization to the +// root account it manages, not to the signing admin key. This covers the delegated admin-signing +// path in `run_key_auth_sign`. +casttest!(key_authorization_sign_admin_access_key_binds_root_account, |_prj, cmd| { + let output = cmd + .args([ + "key-authorization", + "sign", + accounts::ADDR3, // the child key being authorized + "--chain-id", + "31337", + "--admin", + "--tempo.access-key", + accounts::PK2, // admin key ADDR2 signs + "--tempo.root-account", + accounts::ADDR1, // on behalf of root ADDR1 + "--json", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let parsed: serde_json::Value = serde_json::from_str(output.trim()) + .expect("cast key-authorization sign --json should emit valid JSON"); + assert_eq!(parsed["is_admin"], serde_json::Value::Bool(true), "got: {output}"); + // The signer is the admin key (ADDR2)... + assert_eq!( + parsed["signer"].as_str().map(str::to_lowercase), + Some(accounts::ADDR2.to_lowercase()), + "got: {output}" + ); + // ...but the bound account is the root account being modified (ADDR1), not the signer. + assert_eq!( + parsed["account"].as_str().map(str::to_lowercase), + Some(accounts::ADDR1.to_lowercase()), + "got: {output}" + ); +}); + casttest!(keychain_doctor_json_keeps_report_schema_version, async |_prj, cmd| { let output = cmd .args([ diff --git a/crates/common/src/tempo/auth.rs b/crates/common/src/tempo/auth.rs index d79306cfb74f2..c2c70855f48a1 100644 --- a/crates/common/src/tempo/auth.rs +++ b/crates/common/src/tempo/auth.rs @@ -198,6 +198,20 @@ pub async fn ensure_access_key(cfg: EnsureAccessKeyConfig) -> Result, + } + #[derive(Clone)] struct MockState { wallet: Arc>>, @@ -360,6 +383,8 @@ mod tests { key_id: Arc>>, /// Chain ID the mock `/poll` returns in `keyAuthorization`. poll_chain_id: u64, + /// Shape of the returned authorization, for exercising rejection paths. + shape: MockAuthShape, } async fn create_code_handler( @@ -386,11 +411,25 @@ mod tests { /// Build the RLP-hex `SignedKeyAuthorization` blob the live server returns /// in the `key_authorization` field. - fn signed_key_auth_hex(chain_id: u64, key_id: Address, expiry: u64) -> String { + fn signed_key_auth_hex( + chain_id: u64, + key_id: Address, + expiry: u64, + shape: MockAuthShape, + ) -> String { use alloy_rlp::Encodable; use tempo_primitives::transaction::{KeyAuthorization, PrimitiveSignature}; - let auth = KeyAuthorization::unrestricted(chain_id, SignatureType::Secp256k1, key_id) - .with_expiry(expiry); + let mut auth = KeyAuthorization::unrestricted(chain_id, SignatureType::Secp256k1, key_id); + if shape.admin { + // An admin authorization carries no expiry; bind it to its account (or zero, which the + // `is_admin` check rejects before the account is even inspected). + auth = auth.into_admin(shape.account.unwrap_or(Address::ZERO)); + } else { + auth = auth.with_expiry(expiry); + if let Some(account) = shape.account { + auth = auth.with_account(account); + } + } let sig: PrimitiveSignature = serde_json::from_value(serde_json::json!({ "type": "secp256k1", "r": "0x0", "s": "0x0", "yParity": 0 })) @@ -407,12 +446,21 @@ mod tests { Json(serde_json::json!({ "status": "authorized", "account_address": wallet, - "key_authorization": signed_key_auth_hex(state.poll_chain_id, key_id, 9_999_999_999), + "key_authorization": + signed_key_auth_hex(state.poll_chain_id, key_id, 9_999_999_999, state.shape), })) } /// Spawn a mock wallet.tempo server whose `/poll` echoes `poll_chain_id`. async fn spawn_mock_wallet(poll_chain_id: u64) -> (String, tokio::task::JoinHandle<()>) { + spawn_mock_wallet_with(poll_chain_id, MockAuthShape::default()).await + } + + /// Spawn a mock wallet.tempo server with a custom authorization shape. + async fn spawn_mock_wallet_with( + poll_chain_id: u64, + shape: MockAuthShape, + ) -> (String, tokio::task::JoinHandle<()>) { let app = Router::new() .route("/code", post(create_code_handler)) .route("/poll/{code}", post(poll_handler)) @@ -420,6 +468,7 @@ mod tests { wallet: Arc::default(), key_id: Arc::default(), poll_chain_id, + shape, }); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -491,4 +540,58 @@ mod tests { server.abort(); unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_rejects_admin_authorization() { + // An admin key authorization from the wallet must be rejected before keys.toml is written. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + // Bind the admin auth to the mock wallet account (0x..42) so it is rejected purely for + // being an admin key, not for an account mismatch. + let account: Address = "0x0000000000000000000000000000000000000042".parse().unwrap(); + let shape = MockAuthShape { admin: true, account: Some(account) }; + let (service_url, server) = spawn_mock_wallet_with(4217, shape).await; + + let err = ensure_access_key(test_cfg(service_url)).await.unwrap_err(); + assert!( + err.to_string().contains("admin key authorization"), + "expected admin-key rejection, got: {err}" + ); + assert!( + read_tempo_keys_file().is_none_or(|f| f.keys.is_empty()), + "an admin authorization must not be persisted to keys.toml" + ); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } + + #[tokio::test(flavor = "multi_thread")] + async fn ensure_access_key_rejects_cross_account_binding() { + // An authorization bound to an account other than the authorizing one must be rejected + // before keys.toml is written. + let _g = test_env_mutex().lock().await; + let tmp = tempfile::tempdir().unwrap(); + unsafe { std::env::set_var(TEMPO_HOME_ENV, tmp.path()) }; + + // The mock authorizes account 0x..42 but binds the authorization to 0x..dead. + let other: Address = "0x000000000000000000000000000000000000dead".parse().unwrap(); + let shape = MockAuthShape { admin: false, account: Some(other) }; + let (service_url, server) = spawn_mock_wallet_with(4217, shape).await; + + let err = ensure_access_key(test_cfg(service_url)).await.unwrap_err(); + assert!( + err.to_string().contains("wallet authorized account"), + "expected cross-account rejection, got: {err}" + ); + assert!( + read_tempo_keys_file().is_none_or(|f| f.keys.is_empty()), + "a cross-account authorization must not be persisted to keys.toml" + ); + + server.abort(); + unsafe { std::env::remove_var(TEMPO_HOME_ENV) }; + } } diff --git a/crates/common/src/tempo/session.rs b/crates/common/src/tempo/session.rs index 309524e4d8a29..51ab9431c8a4a 100644 --- a/crates/common/src/tempo/session.rs +++ b/crates/common/src/tempo/session.rs @@ -364,6 +364,23 @@ pub(crate) fn validate_signed_session_authorization( auth.key_type, expected_key_type ); + // A session uses a limited access key; T6 admin keys must never be used as a session key. + ensure!( + !auth.is_admin(), + "session {} key_authorization is an admin key, expected a limited access key", + session.session_id + ); + // A T6 account-bound authorization must target this session's root account (no cross-account + // replay). + if let Some(account) = auth.account { + ensure!( + account == session.root_account, + "session {} key_authorization is bound to account {}, expected {}", + session.session_id, + account, + session.root_account + ); + } // `session_id` is local metadata; the signed binding lives in the authorization witness. ensure!( auth.witness == Some(session.session_id), @@ -1120,6 +1137,62 @@ mod tests { }); } + #[test] + fn resolve_rejects_admin_key_authorization() { + with_tempo_home(|| { + let session_id = B256::from([0x17; 32]); + let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active); + // A session must use a limited access key, never a T6 admin key. + entry.key.as_mut().unwrap().key_authorization = + Some(signed_key_authorization_hex_with(&entry, |mut auth| { + auth.is_admin = true; + auth + })); + upsert_session_entry(entry).unwrap(); + + let error = resolve_live_session_signer(session_id, 100).unwrap_err(); + + assert!(error.to_string().contains("admin key"), "got: {error}"); + }); + } + + #[test] + fn resolve_rejects_account_bound_to_other_account() { + with_tempo_home(|| { + let session_id = B256::from([0x18; 32]); + let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active); + // An account-bound authorization minted for another account must not be replayable. + entry.key.as_mut().unwrap().key_authorization = + Some(signed_key_authorization_hex_with(&entry, |auth| { + auth.with_account( + Address::from_str("0x000000000000000000000000000000000000dead").unwrap(), + ) + })); + upsert_session_entry(entry).unwrap(); + + let error = resolve_live_session_signer(session_id, 100).unwrap_err(); + + assert!(error.to_string().contains("bound to account"), "got: {error}"); + }); + } + + #[test] + fn resolve_accepts_account_bound_to_root() { + with_tempo_home(|| { + let session_id = B256::from([0x19; 32]); + let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active); + let root_account = entry.root_account; + // An account binding that targets the session root is valid (backward compatible). + entry.key.as_mut().unwrap().key_authorization = + Some(signed_key_authorization_hex_with(&entry, |auth| { + auth.with_account(root_account) + })); + upsert_session_entry(entry).unwrap(); + + assert!(resolve_live_session_signer(session_id, 100).is_ok()); + }); + } + #[test] fn resolve_live_session_signer_rejects_authorization_with_wider_session_limit() { with_tempo_home(|| { From 10e906c0d930b54531bee192ec2807aec8bb3879 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:43:52 +0200 Subject: [PATCH 25/25] feat(common): resolve fee token on chain (#15161) * feat(tempo): resolve fee token symbols on-chain * fix: fmt * fix: avoid fee token symbol lookup in curl mode * feat(common): resolve fee token on chain * fix(tempo): resolve sponsor fee token before signing * fix(tempo): propagate fee token lookup errors * fix(tempo): stamp fee token before sponsor hash print * fix(tempo): finalize fee token consistently * refactor: simplify `maybe_print_fee_token` * refactor(tempo): simplify fee token resolution * fix(tempo): avoid fee token resolution for sponsored txs * fix(tempo): resolve sponsor fee token before digest * fix(tempo): leave default fee token unset * fix(tempo): avoid duplicate fee token resolution * fix(tempo): preserve resolved fee token display --- crates/cast/src/cmd/batch_mktx.rs | 36 +++-- crates/cast/src/cmd/batch_send.rs | 3 + crates/cast/src/cmd/erc20.rs | 80 +++++++++-- crates/cast/src/cmd/keychain.rs | 62 +++++++-- crates/cast/src/cmd/mktx.rs | 84 +++++++++--- crates/cast/src/cmd/send.rs | 96 ++++++++++---- crates/cast/src/cmd/tip20/mine.rs | 2 + crates/cast/src/cmd/tip20/mod.rs | 65 +++++++-- crates/cast/src/cmd/vaddr/create.rs | 16 ++- crates/cli/src/opts/tempo.rs | 20 ++- crates/common/src/tempo/mod.rs | 98 ++++++++++---- crates/common/src/tempo/tests.rs | 198 ++++++++++++++++++++++++---- crates/forge/src/cmd/create.rs | 25 +++- crates/script/src/broadcast.rs | 131 ++++++++++++------ crates/script/src/lib.rs | 7 +- 15 files changed, 721 insertions(+), 202 deletions(-) diff --git a/crates/cast/src/cmd/batch_mktx.rs b/crates/cast/src/cmd/batch_mktx.rs index 5a1325c2e7b11..235244b289dbc 100644 --- a/crates/cast/src/cmd/batch_mktx.rs +++ b/crates/cast/src/cmd/batch_mktx.rs @@ -21,7 +21,9 @@ use foundry_cli::{ utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{ - FoundryTransactionBuilder, provider::ProviderBuilder, tempo::maybe_print_resolved_fee_token, + FoundryTransactionBuilder, + provider::ProviderBuilder, + tempo::{maybe_print_fee_token, resolve_and_set_fee_token}, }; use foundry_wallets::{TempoAccessKeyConfig, WalletOpts, WalletSigner}; use tempo_alloy::TempoNetwork; @@ -133,14 +135,16 @@ impl BatchMakeTxArgs { } let from = eth.wallet.from.unwrap_or(Address::ZERO); - let (tx, _) = tx_builder.build(from).await?; + let (mut tx, _) = tx_builder.build(from).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(from), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let raw_tx = alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); sh_println!("{raw_tx}")?; @@ -148,14 +152,16 @@ impl BatchMakeTxArgs { } if ethsign { - let (tx, _) = tx_builder.build(config.sender).await?; + let (mut tx, _) = tx_builder.build(config.sender).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(config.sender), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let signed_tx = provider.sign_transaction(tx).await?; sh_println!("{signed_tx}")?; return Ok(()); @@ -168,15 +174,17 @@ impl BatchMakeTxArgs { }; let signed_tx = if let Some(ref access_key) = tempo_access_key { - let (tx, _) = + let (mut tx, _) = tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(access_key.wallet_address), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let raw_tx = tx .sign_with_access_key( &provider, @@ -189,14 +197,16 @@ impl BatchMakeTxArgs { alloy_primitives::hex::encode(raw_tx) } else { tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?; - let (tx, _) = tx_builder.build(&signer).await?; + let (mut tx, _) = tx_builder.build(&signer).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(Signer::address(&signer)), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let envelope = tx.build(&EthereumWallet::new(signer)).await?; alloy_primitives::hex::encode(envelope.encoded_2718()) }; diff --git a/crates/cast/src/cmd/batch_send.rs b/crates/cast/src/cmd/batch_send.rs index 2001841922c25..af9d359a52721 100644 --- a/crates/cast/src/cmd/batch_send.rs +++ b/crates/cast/src/cmd/batch_send.rs @@ -147,6 +147,7 @@ impl BatchSendArgs { provider, tx, Some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, @@ -174,6 +175,7 @@ impl BatchSendArgs { &signer, access_key, Some(chain), + None, send_tx.cast_async, send_tx.confirmations, timeout, @@ -196,6 +198,7 @@ impl BatchSendArgs { provider, tx_request, Some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, diff --git a/crates/cast/src/cmd/erc20.rs b/crates/cast/src/cmd/erc20.rs index 2ffbb428ccc26..6ece5699b8bc2 100644 --- a/crates/cast/src/cmd/erc20.rs +++ b/crates/cast/src/cmd/erc20.rs @@ -24,7 +24,7 @@ use foundry_common::{ fmt::{UIfmt, UIfmtReceiptExt}, provider::{ProviderBuilder, RetryProviderWithSigner}, shell, - tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_resolved_fee_token}, + tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token}, }; #[doc(hidden)] pub use foundry_config::{Chain, utils::*}; @@ -389,6 +389,7 @@ impl Erc20Subcommand { (pre_resolved_signer, tempo_keychain) }; let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; + let sponsor_fee_payer = tx_opts.tempo.sponsor; let expires_at = tx_opts.tempo.resolve_expires(); let tempo_sponsor = if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? }; @@ -412,6 +413,15 @@ impl Erc20Subcommand { .await?; if needs_sponsor_payload { if print_sponsor_hash { + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx .compute_sponsor_hash(access_key.wallet_address) .ok_or_else(|| { @@ -423,6 +433,13 @@ impl Erc20Subcommand { return Ok(()); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + ) + .await?; sponsor .attach_and_print::( &mut tx, @@ -436,11 +453,12 @@ impl Erc20Subcommand { tx, signer, access_key, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, $send_tx.cast_async, $send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } else if let Some(browser) = $send_tx.browser.run::().await? { @@ -454,6 +472,15 @@ impl Erc20Subcommand { tx_opts.apply::(&mut tx, chain.is_legacy()); fill_tx(&$provider, &mut tx, browser.address(), chain, true).await?; if print_sponsor_hash { + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx.compute_sponsor_hash(browser.address()).ok_or_else(|| { eyre::eyre!("This network does not support sponsored transactions") })?; @@ -461,14 +488,28 @@ impl Erc20Subcommand { return Ok(()); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + Some(browser.address()), + ) + .await?; + maybe_print_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + fee_token, + ) + .await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&$provider), - Some(chain), - tx.fee_token(), - ) - .await?; let tx_hash = browser.send_transaction_via_browser(tx).await?; CastTxSender::new(&$provider) .print_tx_result( @@ -489,6 +530,15 @@ impl Erc20Subcommand { if needs_sponsor_payload { fill_tx(&$provider, &mut tx, from, chain, false).await?; if print_sponsor_hash { + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx.compute_sponsor_hash(from).ok_or_else(|| { eyre::eyre!("This network does not support sponsored transactions") })?; @@ -496,18 +546,26 @@ impl Erc20Subcommand { return Ok(()); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&$provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, from).await?; } } cast_send( $provider, tx, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, $send_tx.cast_async, $send_tx.sync, $send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index 85f2eb6e3981f..5da4f4209fd0e 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -24,8 +24,8 @@ use foundry_common::{ provider::ProviderBuilder, sh_warn, shell, tempo::{ - self, KeyType, KeysFile, TEMPO_BROWSER_GAS_BUFFER, WalletType, - maybe_print_resolved_fee_token, read_tempo_keys_file, tempo_keys_path, + self, KeyType, KeysFile, TEMPO_BROWSER_GAS_BUFFER, WalletType, maybe_print_fee_token, + read_tempo_keys_file, resolve_and_set_fee_token, tempo_keys_path, }, }; use foundry_evm::hardfork::TempoHardfork; @@ -3566,6 +3566,7 @@ pub(crate) async fn send_keychain_tx_with_root_signer( before_submit: impl FnOnce() -> Result<()>, ) -> Result { let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; + let sponsor_fee_payer = tx_opts.tempo.sponsor; let expires_at = tx_opts.tempo.resolve_expires(); let tempo_sponsor = if print_sponsor_hash { None } else { tx_opts.tempo.sponsor_config().await? }; @@ -3591,7 +3592,17 @@ pub(crate) async fn send_keychain_tx_with_root_signer( if print_sponsor_hash { let from = root_signer.address(); - let (tx, _) = builder.build(from).await?; + let chain = builder.chain(); + let (mut tx, _) = builder.build(from).await?; + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?; @@ -3615,14 +3626,25 @@ pub(crate) async fn send_keychain_tx_with_root_signer( tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(browser.address()), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token) + .await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx.fee_token(), - ) - .await?; before_submit()?; let tx_hash = browser.send_transaction_via_browser(tx).await?; @@ -3636,7 +3658,24 @@ pub(crate) async fn send_keychain_tx_with_root_signer( let (mut tx, _) = builder.build(from).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, from).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(from), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token) + .await?; } before_submit()?; @@ -3648,12 +3687,13 @@ pub(crate) async fn send_keychain_tx_with_root_signer( cast_send( provider, tx, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index 29743a1a1bcf5..b2af61d8da3da 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -16,7 +16,9 @@ use foundry_cli::{ utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane}, }; use foundry_common::{ - FoundryTransactionBuilder, provider::ProviderBuilder, tempo::maybe_print_resolved_fee_token, + FoundryTransactionBuilder, + provider::ProviderBuilder, + tempo::{maybe_print_fee_token, resolve_and_set_fee_token}, }; use std::{path::PathBuf, str::FromStr}; use tempo_alloy::TempoNetwork; @@ -103,6 +105,7 @@ impl MakeTxArgs { self; let print_sponsor_hash = tx.tempo.print_sponsor_hash; + let sponsor_fee_payer = tx.tempo.sponsor; let expires_at = tx.tempo.resolve_expires(); let tempo_sponsor = if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? }; @@ -146,7 +149,16 @@ impl MakeTxArgs { // sponsor hash commits to the sender. let signer = eth.wallet.signer().await?; let from = signer.address(); - let (tx, _) = tx_builder.build(from).await?; + let (mut tx, _) = tx_builder.build(from).await?; + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx.compute_sponsor_hash(from).ok_or_else(|| { eyre::eyre!("This network does not support sponsored transactions") })?; @@ -179,14 +191,25 @@ impl MakeTxArgs { let (mut tx, _) = tx_builder.build(from).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, from).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(from), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token) + .await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx.fee_token(), - ) - .await?; let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()); print_scalar(raw_tx)?; @@ -199,14 +222,25 @@ impl MakeTxArgs { let (mut tx, _) = tx_builder.build(config.sender).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, config.sender).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(config.sender), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token) + .await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx.fee_token(), - ) - .await?; let signed_tx = provider.sign_transaction(tx).await?; print_scalar(signed_tx)?; @@ -223,14 +257,24 @@ impl MakeTxArgs { let (mut tx, _) = tx_builder.build(&signer).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, from).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(from), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx.fee_token(), - ) - .await?; let tx = tx.build(&EthereumWallet::new(signer)).await?; diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index f7ef22dfcda0d..4da3798b80d8e 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -18,7 +18,7 @@ use foundry_common::{ FoundryTransactionBuilder, fmt::{UIfmt, UIfmtReceiptExt}, provider::ProviderBuilder, - tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_resolved_fee_token}, + tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token}, }; use foundry_config::Chain; use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; @@ -142,6 +142,7 @@ impl SendTxArgs { let print_sponsor_hash = tx.tempo.print_sponsor_hash; let sponsor_url = tx.tempo.sponsor_url.clone(); + let sponsor_fee_payer = tx.tempo.sponsor; let expires_at = tx.tempo.resolve_expires(); let tempo_sponsor = if print_sponsor_hash || sponsor_url.is_some() { None @@ -242,7 +243,8 @@ impl SendTxArgs { // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit. if print_sponsor_hash { - let (tx, from) = if let Some(ref ak) = access_key { + let chain = builder.chain(); + let (mut tx, from) = if let Some(ref ak) = access_key { let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?; (tx, ak.wallet_address) } else { @@ -255,6 +257,15 @@ impl SendTxArgs { let (tx, _) = builder.build(from).await?; (tx, from) }; + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?; @@ -317,18 +328,26 @@ impl SendTxArgs { tx_request.nonce().unwrap_or_default(), )?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx_request, + ) + .await?; sponsor.attach_and_print::(&mut tx_request, config.sender).await?; } cast_send( provider, tx_request, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; // Case 2: @@ -351,14 +370,25 @@ impl SendTxArgs { tx_request.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx_request, + ) + .await?; sponsor.attach_and_print::(&mut tx_request, browser.address()).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx_request, + Some(browser.address()), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token) + .await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx_request.fee_token(), - ) - .await?; let tx_hash = browser.send_transaction_via_browser(tx_request).await?; @@ -380,6 +410,13 @@ impl SendTxArgs { tx_request.nonce().unwrap_or_default(), )?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx_request, + ) + .await?; sponsor.attach_and_print::(&mut tx_request, ak.wallet_address).await?; } cast_send_with_access_key( @@ -387,11 +424,12 @@ impl SendTxArgs { tx_request, &signer, &ak, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; // Case 4: @@ -406,7 +444,6 @@ impl SendTxArgs { tx::validate_from_address(send_tx.eth.wallet.from, from)?; - let chain = builder.chain(); let (mut tx_request, _) = builder.build(&signer).await?; maybe_print_resolved_lane( resolved_lane.as_ref(), @@ -429,12 +466,13 @@ impl SendTxArgs { cast_send( provider, tx_request, - Some(chain), + None, + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + false, ) .await?; // Case 5: @@ -458,6 +496,13 @@ impl SendTxArgs { )?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx_request, + ) + .await?; sponsor.attach_and_print::(&mut tx_request, from).await?; } @@ -469,12 +514,13 @@ impl SendTxArgs { cast_send( provider, tx_request, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } @@ -486,8 +532,9 @@ impl SendTxArgs { #[allow(clippy::too_many_arguments)] pub(crate) async fn cast_send>( provider: P, - tx: N::TransactionRequest, + mut tx: N::TransactionRequest, chain: Option, + fee_payer: Option
, cast_async: bool, sync: bool, confs: u64, @@ -498,12 +545,14 @@ where N::TransactionRequest: Default + FoundryTransactionBuilder, N::ReceiptResponse: UIfmt + UIfmtReceiptExt, { - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( resolve_unknown_fee_token_symbol.then_some(&provider), chain, - tx.fee_token(), + &mut tx, + fee_payer, ) .await?; + maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token).await?; let cast = CastTxSender::new(provider); if sync { @@ -533,6 +582,7 @@ pub(crate) async fn cast_send_with_access_key>( signer: &WalletSigner, access_key: &TempoAccessKeyConfig, chain: Option, + fee_payer: Option
, cast_async: bool, confirmations: u64, timeout: u64, @@ -544,12 +594,14 @@ where { tx.set_from(access_key.wallet_address); tx.set_key_id(access_key.key_address); - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( resolve_unknown_fee_token_symbol.then_some(provider), chain, - tx.fee_token(), + &mut tx, + fee_payer, ) .await?; + maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(provider), fee_token).await?; let raw_tx = tx .sign_with_access_key( provider, diff --git a/crates/cast/src/cmd/tip20/mine.rs b/crates/cast/src/cmd/tip20/mine.rs index 7d38c4c8ef74d..f8039f330d47f 100644 --- a/crates/cast/src/cmd/tip20/mine.rs +++ b/crates/cast/src/cmd/tip20/mine.rs @@ -121,6 +121,7 @@ pub(super) async fn register( &signer, access_key, Some(chain), + None, send_tx.cast_async, send_tx.confirmations, timeout, @@ -133,6 +134,7 @@ pub(super) async fn register( provider, tx, Some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index 6a1705f932c56..3a20f294c8514 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -17,7 +17,7 @@ use foundry_cli::{ use foundry_common::{ FoundryTransactionBuilder, provider::ProviderBuilder, - tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_resolved_fee_token}, + tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token}, }; use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; use std::{str::FromStr, time::Duration}; @@ -208,6 +208,7 @@ pub(crate) async fn send_tip20_transaction( let mut tx_opts = tx_params.into_transaction_opts(); let print_sponsor_hash = tx_opts.tempo.print_sponsor_hash; let sponsor_url = tx_opts.tempo.sponsor_url.clone(); + let sponsor_fee_payer = tx_opts.tempo.sponsor; let expires_at = tx_opts.tempo.resolve_expires(); let tempo_sponsor = if print_sponsor_hash || sponsor_url.is_some() { None @@ -245,7 +246,7 @@ pub(crate) async fn send_tip20_transaction( let chain = builder.chain(); if print_sponsor_hash { - let (tx, from) = if let Some(ref ak) = access_key { + let (mut tx, from) = if let Some(ref ak) = access_key { let (tx, _) = builder.build_with_access_key(ak.wallet_address, ak).await?; (tx, ak.wallet_address) } else { @@ -256,6 +257,15 @@ pub(crate) async fn send_tip20_transaction( let (tx, _) = builder.build(signer).await?; (tx, from) }; + if let Some(fee_payer) = sponsor_fee_payer { + resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(fee_payer), + ) + .await?; + } let hash = tx .compute_sponsor_hash(from) .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))?; @@ -275,14 +285,24 @@ pub(crate) async fn send_tip20_transaction( tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, browser.address()).await?; + } else { + let fee_token = resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + Some(browser.address()), + ) + .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; } - maybe_print_resolved_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - tx.fee_token(), - ) - .await?; let tx_hash = browser.send_transaction_via_browser(tx).await?; CastTxSender::new(&provider) .print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout) @@ -294,6 +314,13 @@ pub(crate) async fn send_tip20_transaction( let (mut tx, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, ak.wallet_address).await?; } cast_send_with_access_key( @@ -301,11 +328,12 @@ pub(crate) async fn send_tip20_transaction( tx, signer, &ak, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } else if let Some(sponsor_url) = sponsor_url { @@ -333,12 +361,13 @@ pub(crate) async fn send_tip20_transaction( cast_send( provider, tx, - Some(chain), + None, + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + false, ) .await?; } else { @@ -352,6 +381,13 @@ pub(crate) async fn send_tip20_transaction( let (mut tx, _) = builder.build(&signer).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + (!config.eth_rpc_curl).then_some(&provider), + Some(chain), + &mut tx, + ) + .await?; sponsor.attach_and_print::(&mut tx, from).await?; } @@ -362,12 +398,13 @@ pub(crate) async fn send_tip20_transaction( cast_send( provider, tx, - Some(chain), + tempo_sponsor.is_none().then_some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, timeout, - !config.eth_rpc_curl, + tempo_sponsor.is_none() && !config.eth_rpc_curl, ) .await?; } diff --git a/crates/cast/src/cmd/vaddr/create.rs b/crates/cast/src/cmd/vaddr/create.rs index fb077efa86c75..58ba37aaf02c8 100644 --- a/crates/cast/src/cmd/vaddr/create.rs +++ b/crates/cast/src/cmd/vaddr/create.rs @@ -21,7 +21,7 @@ use foundry_common::{ fmt::{UIfmt, UIfmtReceiptExt}, provider::ProviderBuilder, shell, - tempo::maybe_print_resolved_fee_token, + tempo::{maybe_print_fee_token, resolve_and_set_fee_token}, }; use rand::{RngCore, SeedableRng, rngs::StdRng}; use serde_json::json; @@ -189,12 +189,14 @@ async fn register( tempo::fill_access_key_transaction(&provider, &mut tx, access_key, chain).await?; if shell::is_json() { // JSON mode bypasses `cast_send_with_access_key`, so report the selection here. - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(access_key.wallet_address), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let raw_tx = tx .sign_with_access_key( &provider, @@ -221,6 +223,7 @@ async fn register( &signer, access_key, Some(chain), + None, send_tx.cast_async, send_tx.confirmations, timeout, @@ -232,12 +235,14 @@ async fn register( let provider = build_provider_with_signer::(&send_tx, signer)?; if shell::is_json() { // JSON mode bypasses `cast_send`, so report the selection here. - maybe_print_resolved_fee_token( + let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), Some(chain), - tx.fee_token(), + &mut tx, + Some(sender), ) .await?; + maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?; let cast = CastTxSender::new(&provider); if send_tx.sync { cast.send_sync(tx).await.map(|(tx_hash, _)| tx_hash) @@ -259,6 +264,7 @@ async fn register( provider, tx, Some(chain), + None, send_tx.cast_async, send_tx.sync, send_tx.confirmations, diff --git a/crates/cli/src/opts/tempo.rs b/crates/cli/src/opts/tempo.rs index b2fde07e456ad..fff691a2e6cad 100644 --- a/crates/cli/src/opts/tempo.rs +++ b/crates/cli/src/opts/tempo.rs @@ -138,7 +138,7 @@ pub struct TempoOpts { /// knows what hash to sign. The transaction is not sent. #[arg( long = "tempo.print-sponsor-hash", - conflicts_with_all = &["sponsor", "sponsor_signer", "sponsor_sig", "sponsor_url"] + conflicts_with_all = &["sponsor_signer", "sponsor_sig", "sponsor_url"] )] pub print_sponsor_hash: bool, @@ -453,13 +453,29 @@ mod tests { } #[test] - fn print_sponsor_hash_conflicts_with_sponsor_submission() { + fn print_sponsor_hash_allows_sponsor_address() { + let opts = TempoOpts::try_parse_from([ + "", + "--tempo.print-sponsor-hash", + "--tempo.sponsor", + "0x1111111111111111111111111111111111111111", + ]) + .unwrap(); + + assert!(opts.print_sponsor_hash); + assert_eq!(opts.sponsor, Some(address!("0x1111111111111111111111111111111111111111"))); + } + + #[test] + fn print_sponsor_hash_conflicts_with_sponsor_signature() { assert!( TempoOpts::try_parse_from([ "", "--tempo.print-sponsor-hash", "--tempo.sponsor", "0x1111111111111111111111111111111111111111", + "--tempo.sponsor-signature", + "0x0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b", ]) .is_err() ); diff --git a/crates/common/src/tempo/mod.rs b/crates/common/src/tempo/mod.rs index abf1884b15471..d8f4a95023b75 100644 --- a/crates/common/src/tempo/mod.rs +++ b/crates/common/src/tempo/mod.rs @@ -13,7 +13,7 @@ use eyre::{Context, Result}; use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner}; use std::sync::Arc; pub use tempo_alloy::contracts::precompiles::PATH_USD_ADDRESS; -use tempo_alloy::contracts::precompiles::{DEFAULT_FEE_TOKEN, ITIP20}; +use tempo_alloy::contracts::precompiles::{IFeeManager, ITIP20, TIP_FEE_MANAGER_ADDRESS}; mod keystore; mod registry; @@ -67,18 +67,66 @@ pub const ALPHA_USD_ADDRESS: Address = address!("0x20C00000000000000000000000000 pub const BETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000002"); pub const THETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000003"); -/// Resolves an explicit Tempo fee token or the canonical default for a known Tempo network. +/// Resolves and applies the Tempo fee token selected by the network. /// -/// TODO: fee token resolution is incomplete, must use FeeTokenManager. -pub fn resolve_fee_token( +/// This must happen before computing a sponsor digest, because Tempo sponsor signatures commit to +/// the fee token. +pub async fn resolve_and_set_fee_token( + provider: Option<&dyn Provider>, chain: Option, - explicit_fee_token: Option
, -) -> Option
{ - explicit_fee_token.or_else(|| chain.is_some_and(Chain::is_tempo).then_some(DEFAULT_FEE_TOKEN)) + tx: &mut N::TransactionRequest, + fee_payer: Option
, +) -> Result> +where + N: Network, + N::TransactionRequest: Default + FoundryTransactionBuilder, +{ + if let Some(fee_token) = tx.fee_token() { + return Ok(Some(fee_token)); + } + if !chain.is_some_and(Chain::is_tempo) { + return Ok(None); + } + let Some(provider) = provider else { + return Ok(None); + }; + + let fee_payer = fee_payer.or_else(|| tx.from()); + let fee_token = if let Some(fee_payer) = fee_payer { + stored_user_fee_token(provider, fee_payer).await? + } else { + None + }; + let Some(fee_token) = fee_token else { + return Ok(None); + }; + tx.set_fee_token(fee_token); + Ok(Some(fee_token)) +} + +async fn stored_user_fee_token( + provider: &dyn Provider, + fee_payer: Address, +) -> Result> +where + N: Network, + N::TransactionRequest: Default + NetworkTransactionBuilder, +{ + let call = IFeeManager::userTokensCall { user: fee_payer }; + let tx = N::TransactionRequest::default() + .with_to(TIP_FEE_MANAGER_ADDRESS) + .with_input(call.abi_encode()); + let output = provider + .call(tx) + .await + .wrap_err_with(|| format!("failed to resolve Tempo fee token for {fee_payer}"))?; + let fee_token = IFeeManager::userTokensCall::abi_decode_returns(&output) + .wrap_err("failed to decode Tempo fee token lookup")?; + Ok((!fee_token.is_zero()).then_some(fee_token)) } /// Returns the known symbol for a Tempo fee token without making an RPC call. -pub const fn known_fee_token_symbol(fee_token: Address) -> Option<&'static str> { +const fn known_fee_token_symbol(fee_token: Address) -> Option<&'static str> { match fee_token { PATH_USD_ADDRESS => Some("PathUSD"), ALPHA_USD_ADDRESS => Some("AlphaUSD"), @@ -106,7 +154,7 @@ where (!symbol.is_empty()).then_some(symbol) } -/// Prints the selected Tempo fee token when one is set. +/// Prints the fee token selected for display. /// /// Unknown symbols are resolved on-chain only when a provider is supplied, because some provider /// modes such as `--curl` must preserve the first RPC request for the user's intended action. @@ -135,21 +183,6 @@ where Ok(()) } -/// Prints the fee token selected for display, resolving the chain default and unknown symbols -/// without mutating a transaction request. -pub async fn maybe_print_resolved_fee_token( - provider: Option<&P>, - chain: Option, - fee_token: Option
, -) -> Result<()> -where - N: Network, - N::TransactionRequest: Default + NetworkTransactionBuilder, - P: Provider, -{ - maybe_print_fee_token(provider, resolve_fee_token(chain, fee_token)).await -} - /// Gas sponsor configuration for Tempo fee-payer signatures. #[derive(Clone, Debug)] pub struct TempoSponsor { @@ -171,6 +204,23 @@ impl TempoSponsor { self.sponsor } + /// Resolves the fee token paid by this sponsor and applies it to the transaction request. + /// + /// This must happen before computing a sponsor digest, because Tempo sponsor signatures commit + /// to the fee token. + pub async fn resolve_and_set_fee_token( + &self, + provider: Option<&dyn Provider>, + chain: Option, + tx: &mut N::TransactionRequest, + ) -> Result> + where + N: Network, + N::TransactionRequest: Default + FoundryTransactionBuilder, + { + resolve_and_set_fee_token(provider, chain, tx, Some(self.sponsor)).await + } + pub async fn attach_and_print( &self, tx: &mut N::TransactionRequest, diff --git a/crates/common/src/tempo/tests.rs b/crates/common/src/tempo/tests.rs index 8843d46a01cdd..57b33f852bc08 100644 --- a/crates/common/src/tempo/tests.rs +++ b/crates/common/src/tempo/tests.rs @@ -3,15 +3,18 @@ use eyre::WrapErr; use foundry_evm_hardforks::TempoHardfork; use serde::Deserialize; use std::env; -use tempo_alloy::contracts::precompiles::DEFAULT_FEE_TOKEN; use alloy_chains::{Chain, NamedChain}; +use alloy_network::TransactionBuilder; use alloy_primitives::{Address, address}; -use tempo_alloy::TempoNetwork; +use alloy_provider::mock::Asserter; +use alloy_rpc_types::TransactionRequest; +use alloy_sol_types::SolValue; +use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest}; use super::{ - ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, PATH_USD_ADDRESS, THETA_USD_ADDRESS, - known_fee_token_symbol, resolve_fee_token, resolve_fee_token_symbol, + ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, PATH_USD_ADDRESS, THETA_USD_ADDRESS, TempoSponsor, + known_fee_token_symbol, resolve_and_set_fee_token, resolve_fee_token_symbol, }; #[derive(Debug, Deserialize)] @@ -64,36 +67,183 @@ async fn test_fork_schedule_parses_configured_rpcs() -> eyre::Result<()> { Ok(()) } -#[test] -fn resolves_canonical_fee_token_for_tempo_chains() { - for chain in [ - NamedChain::Tempo, - NamedChain::TempoModerato, - NamedChain::TempoTestnet, - NamedChain::TempoDevnet, - ] { - assert_eq!(resolve_fee_token(Some(chain.into()), None), Some(DEFAULT_FEE_TOKEN)); - } +#[tokio::test] +async fn explicit_fee_token_overrides_stored_user_token_when_applied() -> eyre::Result<()> { + let asserter = Asserter::new(); + let provider = + ProviderBuilder::new_with_network::().connect_mocked_client(asserter); + let explicit = Address::repeat_byte(0x42); + let fee_payer = Address::repeat_byte(0x11); + let mut tx = TempoTransactionRequest { + inner: TransactionRequest::default().with_from(fee_payer), + fee_token: Some(explicit), + ..Default::default() + }; + + assert_eq!( + resolve_and_set_fee_token::( + Some(&provider), + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + Some(fee_payer), + ) + .await?, + Some(explicit) + ); + assert_eq!(tx.fee_token, Some(explicit)); + Ok(()) } -#[test] -fn leaves_non_tempo_chains_without_a_default() { - assert_eq!(resolve_fee_token(Some(NamedChain::Mainnet.into()), None), None); +#[tokio::test] +async fn default_fee_token_resolution_leaves_transaction_fee_token_unset() -> eyre::Result<()> { + let explicit = Address::repeat_byte(0x42); + let mut tx = TempoTransactionRequest { fee_token: Some(explicit), ..Default::default() }; + + let resolved = resolve_and_set_fee_token::( + None, + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + None, + ) + .await?; + assert_eq!(resolved, Some(explicit)); + assert_eq!(tx.fee_token, Some(explicit)); + + let mut tx = TempoTransactionRequest::default(); + let resolved = resolve_and_set_fee_token::( + None, + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + None, + ) + .await?; + assert_eq!(resolved, None); + assert_eq!(tx.fee_token, None); + + let mut tx = TempoTransactionRequest::default(); + let resolved = resolve_and_set_fee_token::( + None, + Some(Chain::from_named(NamedChain::Mainnet)), + &mut tx, + None, + ) + .await?; + assert_eq!(resolved, None); + assert_eq!(tx.fee_token, None); + Ok(()) } -#[test] -fn leaves_unknown_chain_without_a_default() { - assert_eq!(resolve_fee_token(None, None), None); +#[tokio::test] +async fn send_fee_token_resolution_can_skip_lookup_for_curl_mode() -> eyre::Result<()> { + let asserter = Asserter::new(); + let provider = + ProviderBuilder::new_with_network::().connect_mocked_client(asserter.clone()); + let fee_payer = Address::repeat_byte(0x11); + let mut tx = TempoTransactionRequest::default(); + + asserter.push_success(&BETA_USD_ADDRESS.abi_encode()); + assert_eq!( + resolve_and_set_fee_token::( + Some(&provider), + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + Some(fee_payer), + ) + .await?, + Some(BETA_USD_ADDRESS) + ); + assert_eq!(tx.fee_token, Some(BETA_USD_ADDRESS)); + + let mut tx = TempoTransactionRequest::default(); + assert_eq!( + resolve_and_set_fee_token::( + None, + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + Some(fee_payer), + ) + .await?, + None + ); + assert_eq!(tx.fee_token, None); + + Ok(()) } -#[test] -fn explicit_fee_token_overrides_chain_default() { +#[tokio::test] +async fn unset_user_token_does_not_stamp_default_fee_token() -> eyre::Result<()> { + let asserter = Asserter::new(); + let provider = + ProviderBuilder::new_with_network::().connect_mocked_client(asserter.clone()); + let fee_payer = Address::repeat_byte(0x11); + let mut tx = TempoTransactionRequest::default(); + + asserter.push_success(&Address::ZERO.abi_encode()); + + assert_eq!( + resolve_and_set_fee_token::( + Some(&provider), + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + Some(fee_payer), + ) + .await?, + None + ); + assert_eq!(tx.fee_token, None); + + Ok(()) +} + +#[tokio::test] +async fn sponsor_fee_token_resolution_uses_sponsor_address() -> eyre::Result<()> { + let asserter = Asserter::new(); + let provider = + ProviderBuilder::new_with_network::().connect_mocked_client(asserter.clone()); + let sponsor = TempoSponsor::new(Address::repeat_byte(0x22), None, None); + let sender = Address::repeat_byte(0x11); + let mut tx = TempoTransactionRequest { + inner: TransactionRequest::default().with_from(sender), + ..Default::default() + }; + + asserter.push_success(&BETA_USD_ADDRESS.abi_encode()); + + assert_eq!( + sponsor + .resolve_and_set_fee_token::( + Some(&provider), + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + ) + .await?, + Some(BETA_USD_ADDRESS) + ); + assert_eq!(tx.fee_token, Some(BETA_USD_ADDRESS)); + Ok(()) +} + +#[tokio::test] +async fn sponsor_fee_token_resolution_preserves_explicit_token() -> eyre::Result<()> { + let asserter = Asserter::new(); + let provider = + ProviderBuilder::new_with_network::().connect_mocked_client(asserter); let explicit = Address::repeat_byte(0x42); + let sponsor = TempoSponsor::new(Address::repeat_byte(0x22), None, None); + let mut tx = TempoTransactionRequest { fee_token: Some(explicit), ..Default::default() }; + assert_eq!( - resolve_fee_token(Some(Chain::from_named(NamedChain::Tempo)), Some(explicit)), + sponsor + .resolve_and_set_fee_token::( + Some(&provider), + Some(Chain::from_named(NamedChain::Tempo)), + &mut tx, + ) + .await?, Some(explicit) ); - assert_eq!(resolve_fee_token(None, Some(explicit)), Some(explicit)); + assert_eq!(tx.fee_token, Some(explicit)); + Ok(()) } #[test] diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 06a502affeded..52bfbce7a8d66 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -24,7 +24,7 @@ use foundry_common::{ fmt::parse_tokens, provider::ProviderBuilder, shell, - tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_resolved_fee_token}, + tempo::{TEMPO_BROWSER_GAS_BUFFER, maybe_print_fee_token, resolve_and_set_fee_token}, }; use foundry_compilers::{ ArtifactId, artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, @@ -575,14 +575,25 @@ impl CreateArgs { let tempo_sponsor = self.tx.tempo.sponsor_config().await?; if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + resolve_unknown_fee_token_symbol.then_some(&provider), + Some(chain), + &mut deployer.tx, + ) + .await?; sponsor.attach_and_print::(&mut deployer.tx, deployer_address).await?; + } else { + let fee_token = resolve_and_set_fee_token( + resolve_unknown_fee_token_symbol.then_some(&provider), + Some(chain), + &mut deployer.tx, + Some(deployer_address), + ) + .await?; + maybe_print_fee_token(resolve_unknown_fee_token_symbol.then_some(&provider), fee_token) + .await?; } - maybe_print_resolved_fee_token( - resolve_unknown_fee_token_symbol.then_some(&provider), - Some(chain), - deployer.tx.fee_token(), - ) - .await?; // Deploy the actual contract let (deployed_contract, receipt) = if let Some(browser) = browser_signer { diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index dc1aa5f166672..0bdb4ff8e7cd5 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -36,7 +36,7 @@ use foundry_common::{ shell, tempo::{ KeyEntry, KeysFile, TempoSponsor, WALLET_KEYS_PATH, decode_key_authorization, - maybe_print_fee_token, tempo_home, + maybe_print_fee_token, resolve_and_set_fee_token, tempo_home, }, }; use foundry_config::Config; @@ -107,6 +107,7 @@ where /// 1. Nonce synchronization: Waits for the provider's nonce to catch up to the expected /// transaction nonce when doing sequential broadcast /// 2. Gas estimation: Re-estimates gas right before broadcasting for chains that require it + #[allow(clippy::too_many_arguments)] pub async fn prepare( &mut self, provider: &RootProvider, @@ -115,6 +116,7 @@ where estimate_via_rpc: bool, estimate_multiplier: u64, tempo_sponsor: Option<&TempoSponsor>, + chain: Option, ) -> Result<()> { let (tx, access_key_authorization) = match self { Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) => (tx, None), @@ -173,6 +175,13 @@ where .await?; } + let fee_token = if let Some(sponsor) = tempo_sponsor { + sponsor.resolve_and_set_fee_token(Some(provider), chain, tx).await?; + None + } else { + resolve_and_set_fee_token(Some(provider), chain, tx, tx.from()).await? + }; + // Chains which use `eth_estimateGas` are being sent sequentially and require their // gas to be re-estimated right before broadcasting. if !is_fixed_gas_limit && estimate_via_rpc { @@ -182,8 +191,9 @@ where if let Some(sponsor) = tempo_sponsor { let from = tx.from().expect("no sender"); sponsor.attach_and_print::(tx, from).await?; + } else { + maybe_print_fee_token(Some(provider), fee_token).await?; } - maybe_print_fee_token(Some(provider), tx.fee_token()).await?; Ok(()) } @@ -245,6 +255,7 @@ where /// /// This is a convenience method that combines [`prepare`](Self::prepare) and /// [`send`](Self::send) into a single call. + #[allow(clippy::too_many_arguments)] pub async fn prepare_and_send( mut self, provider: Arc>, @@ -253,6 +264,7 @@ where estimate_via_rpc: bool, estimate_multiplier: u64, tempo_sponsor: Option<&TempoSponsor>, + chain: Option, ) -> Result { self.prepare( &provider, @@ -261,6 +273,7 @@ where estimate_via_rpc, estimate_multiplier, tempo_sponsor, + chain, ) .await?; @@ -630,52 +643,51 @@ impl BundledState { // Iterate through transactions, matching the `from` field with the associated // wallet. Then send the transaction. Panics if we find a unknown `from` - let transactions = sequence - .transactions - .iter() - .skip(already_broadcasted) - .map(|tx_with_metadata| { - let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; - - let kind = match tx_with_metadata.tx().clone() { - TransactionMaybeSigned::Signed { tx, .. } => { - if tempo_sponsor.is_some() { - eyre::bail!( - "cannot attach Tempo sponsor signature to an already signed script transaction" - ); - } - SendTransactionKind::Signed(tx) + let sequence_chain = sequence.chain; + let mut transactions = Vec::with_capacity( + sequence.transactions.len().saturating_sub(already_broadcasted), + ); + for tx_with_metadata in sequence.transactions.iter().skip(already_broadcasted) { + let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; + + let kind = match tx_with_metadata.tx().clone() { + TransactionMaybeSigned::Signed { tx, .. } => { + if tempo_sponsor.is_some() { + eyre::bail!( + "cannot attach Tempo sponsor signature to an already signed script transaction" + ); } - TransactionMaybeSigned::Unsigned(mut tx) => { - let from = tx.from().expect("No sender for onchain transaction!"); + SendTransactionKind::Signed(tx) + } + TransactionMaybeSigned::Unsigned(mut tx) => { + let from = tx.from().expect("No sender for onchain transaction!"); - tx.set_chain_id(sequence.chain); + tx.set_chain_id(sequence_chain); - // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in - // alloy - if tx.kind().is_none() { - tx.set_create(); - } + // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in + // alloy + if tx.kind().is_none() { + tx.set_create(); + } - if let Some(gas_price) = gas_price { - tx.set_gas_price(gas_price); - } else { - let eip1559_fees = eip1559_fees.expect("was set above"); - tx.set_max_priority_fee_per_gas( - eip1559_fees.max_priority_fee_per_gas, - ); - tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); - } + if let Some(gas_price) = gas_price { + tx.set_gas_price(gas_price); + } else { + let eip1559_fees = eip1559_fees.expect("was set above"); + tx.set_max_priority_fee_per_gas( + eip1559_fees.max_priority_fee_per_gas, + ); + tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas); + } - self.script_config.tempo.apply::(&mut tx, None); + self.script_config.tempo.apply::(&mut tx, None); - send_kind.for_sender(sequence.chain, &from, tx)? - } - }; + send_kind.for_sender(sequence_chain, &from, tx)? + } + }; - Ok((kind, is_fixed_gas_limit)) - }) - .collect::>>()?; + transactions.push((kind, is_fixed_gas_limit)); + } let estimate_via_rpc = has_different_gas_calc(sequence.chain) || self.script_config.evm_opts.networks.is_tempo() @@ -695,6 +707,7 @@ impl BundledState { // cannot handle more than that. let batch_size = if sequential_broadcast { 1 } else { 100 }; let mut index = already_broadcasted; + let sequence_chain = sequence.chain; for (batch_number, batch) in transactions.chunks(batch_size).enumerate() { seq_progress.inner.write().set_status(&format!( @@ -718,6 +731,7 @@ impl BundledState { estimate_via_rpc, self.args.gas_estimate_multiplier, tempo_sponsor.as_deref(), + Some(sequence_chain.into()), ) .await; (res, kind, *is_fixed_gas_limit, 0, None) @@ -763,6 +777,7 @@ impl BundledState { estimate_via_rpc, self.args.gas_estimate_multiplier, tempo_sponsor.as_deref(), + Some(sequence_chain.into()), ) .await; ( @@ -1238,6 +1253,24 @@ impl BundledState { ..Default::default() }; self.script_config.tempo.apply::(&mut batch_tx, None); + let fee_token = if let Some(sponsor) = &tempo_sponsor { + sponsor + .resolve_and_set_fee_token( + Some(provider.as_ref()), + Some(Chain::from_named(NamedChain::Tempo)), + &mut batch_tx, + ) + .await?; + None + } else { + resolve_and_set_fee_token( + Some(provider.as_ref()), + Some(Chain::from_named(NamedChain::Tempo)), + &mut batch_tx, + Some(sender), + ) + .await? + }; if let BatchSigner::TempoKeychain(_, ak) = &batch_signer { batch_tx.key_id = Some(ak.key_address); @@ -1258,8 +1291,9 @@ impl BundledState { if let Some(sponsor) = &tempo_sponsor { sponsor.attach_and_print::(&mut batch_tx, sender).await?; + } else { + maybe_print_fee_token(Some(provider.as_ref()), fee_token).await?; } - maybe_print_fee_token(Some(provider.as_ref()), batch_tx.fee_token()).await?; // Sign and send. let tx_hash = match batch_signer { @@ -1576,7 +1610,18 @@ mod tests { let provider = RootProvider::::new_http("http://localhost:8545".parse().unwrap()); - sender.prepare(&provider, false, true, false, 100, None).await.unwrap(); + sender + .prepare( + &provider, + false, + true, + false, + 100, + None, + Some(Chain::from_named(NamedChain::Mainnet)), + ) + .await + .unwrap(); match sender { SendTransactionKind::AccessKey(tx, _, _) => { diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index b591a6210d75c..8a306c1b730d1 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -13,7 +13,6 @@ extern crate foundry_common; extern crate tracing; use crate::{broadcast::BundledState, runner::ScriptRunner}; -use alloy_chains::{Chain, NamedChain}; use alloy_json_abi::{Function, JsonAbi}; use alloy_network::Network; use alloy_primitives::{ @@ -37,7 +36,6 @@ use foundry_common::{ abi::{encode_function_args, get_func}, compile::ContractSizeLimits, shell, - tempo::resolve_fee_token, }; use foundry_compilers::ArtifactId; use foundry_config::{ @@ -314,10 +312,6 @@ impl ScriptArgs { tempo.resolve_expires(); - // Resolve the fee token: default only when the active EVM network is Tempo. - let chain = evm_opts.networks.is_tempo().then(|| Chain::from_named(NamedChain::Tempo)); - tempo.fee_token = resolve_fee_token(chain, tempo.fee_token); - let script_config = ScriptConfig::new(config, evm_opts, args.batch, tempo).await?; Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet }) } @@ -888,6 +882,7 @@ impl ScriptConfig { #[cfg(test)] mod tests { use super::*; + use alloy_chains::NamedChain; use alloy_network::Ethereum; use alloy_primitives::{B256, address}; use foundry_cli::opts::TEMPO_SESSION_ID_ENV;