From 6b8bd8cdda9659fe95d76a3efc192e0ca745bd6a Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:57:06 +0200 Subject: [PATCH 1/4] fix(script): skip synthetic Tempo fees Tempo script deployment, setUp, and run execute inside a synthetic runner whose oversized gas allowance must not debit TIP-20 fees. Disable fee charging only for that cheatcode-enabled runner while retaining normal fee behavior for RPC simulation of collected broadcast transactions. --- .changelog/fix-tempo-script-setup-fees.md | 5 ++ crates/forge/tests/cli/script.rs | 39 +++++++++++++++ crates/script/src/lib.rs | 58 ++++++++++++++++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 .changelog/fix-tempo-script-setup-fees.md diff --git a/.changelog/fix-tempo-script-setup-fees.md b/.changelog/fix-tempo-script-setup-fees.md new file mode 100644 index 0000000000000..d50c399ea97e0 --- /dev/null +++ b/.changelog/fix-tempo-script-setup-fees.md @@ -0,0 +1,5 @@ +--- +forge: patch +--- + +Prevented synthetic Tempo script execution from charging TIP-20 gas fees. diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 4a7cedf15d54f..67d07b42ae6c6 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -11,6 +11,7 @@ use anvil::{NodeConfig, spawn}; use axum::{Router, body::Bytes as BodyBytes}; use forge_script_sequence::ScriptSequence; use foundry_compilers::artifacts::EvmVersion; +use foundry_evm::constants::CALLER; use foundry_test_utils::{ ScriptOutcome, ScriptTester, rpc::{self, next_http_archive_rpc_url}, @@ -4805,6 +4806,44 @@ forgetest!(can_execute_script_command_with_tempo, |prj, cmd| { .assert_success(); }); +forgetest_async!(tempo_script_setup_does_not_charge_fees, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + let script = prj.add_script( + "TempoScript.s.sol", + r#" +import "forge-std/Script.sol"; + +contract TempoScript is Script { + function run() external {} +} +"#, + ); + prj.update_config(|config| { + config.gas_limit = u64::MAX.into(); + config.gas_price = Some(600_000_000); + }); + + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + api.anvil_deal_tip20( + CALLER, + address!("0x20c0000000000000000000000000000000000000"), + U256::ZERO, + ) + .await + .unwrap(); + cmd.arg("script").arg(script).args([ + "--rpc-url", + &handle.http_endpoint(), + "--network", + "tempo", + "--gas-price", + "600000000", + "--tempo.fee-token", + "0x20c0000000000000000000000000000000000000", + ]); + cmd.assert_success(); +}); + forgetest_async!(tempo_aa_script_broadcast_deploys_with_fee_token, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); let script = prj.add_script( diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 21af4efaceb13..c0e94dc08548d 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -1010,7 +1010,17 @@ impl ScriptConfig { target: ArtifactId, restricted: bool, ) -> Result> { - self._get_runner(Some((known_contracts, script_wallets, target)), debug, restricted).await + let mut runner = self + ._get_runner(Some((known_contracts, script_wallets, target)), debug, restricted) + .await?; + + // Script execution is synthetic. Keep the Tempo transaction context, but do not charge + // protocol fees for deploying or calling the local script contract. + if self.evm_opts.networks.is_tempo() { + runner.executor.evm_env_mut().cfg_env.disable_fee_charge = true; + } + + Ok(runner) } async fn _get_runner( @@ -1147,6 +1157,7 @@ mod tests { CallKind, CallTrace, CallTraceArena, CallTraceNode, SparsedTraceArena, TraceKind, }, }; + use semver::Version; use std::{fs, num::NonZeroU64, sync::LazyLock}; use tempfile::tempdir; use tokio::sync::{Mutex, MutexGuard}; @@ -1936,6 +1947,51 @@ mod tests { assert_eq!(script.hardfork, Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))); } + #[tokio::test(flavor = "multi_thread")] + async fn tempo_runner_fee_charge_matches_execution_context() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let networks = NetworkConfigs::with_tempo(); + let evm_opts = EvmOpts { + fork_url: Some(handle.http_endpoint()), + sender: handle.dev_accounts().next().unwrap(), + networks, + ..Default::default() + }; + let config = Config { networks, ..Default::default() }; + let mut script = ScriptConfig::::new( + config, + evm_opts, + false, + TempoOpts::default(), + None, + ) + .await + .unwrap(); + + let rpc_runner = script._get_runner(None, false, false).await.unwrap(); + assert!(!rpc_runner.executor.evm_env().cfg_env.disable_fee_charge); + + let target = ArtifactId { + path: PathBuf::from("Script.json"), + name: "Script".to_string(), + source: PathBuf::from("Script.sol"), + version: Version::new(0, 8, 30), + build_id: String::new(), + profile: "default".to_string(), + }; + let synthetic_runner = script + .get_runner_with_cheatcodes( + ContractsByArtifact::default(), + Wallets::new(Default::default(), None), + false, + target, + false, + ) + .await + .unwrap(); + assert!(synthetic_runner.executor.evm_env().cfg_env.disable_fee_charge); + } + #[test] fn can_parse_shared_tempo_opts() { let args = ScriptArgs::parse_from([ From 8e7d8c33ffa02f52183c3d9dcc25c8a9d8c1ee47 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:14:38 +0200 Subject: [PATCH 2/4] test(script): clarify Tempo fee coverage --- crates/forge/tests/cli/script.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 67d07b42ae6c6..6fab4ec922f58 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -4806,7 +4806,7 @@ forgetest!(can_execute_script_command_with_tempo, |prj, cmd| { .assert_success(); }); -forgetest_async!(tempo_script_setup_does_not_charge_fees, |prj, cmd| { +forgetest_async!(tempo_script_runs_with_zero_fee_token_balance, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); let script = prj.add_script( "TempoScript.s.sol", @@ -4824,13 +4824,9 @@ contract TempoScript is Script { }); let (api, handle) = spawn(NodeConfig::test_tempo()).await; - api.anvil_deal_tip20( - CALLER, - address!("0x20c0000000000000000000000000000000000000"), - U256::ZERO, - ) - .await - .unwrap(); + let fee_token = address!("0x20c0000000000000000000000000000000000000"); + // Synthetic script execution should not require the caller to fund protocol fees. + api.anvil_deal_tip20(CALLER, fee_token, U256::ZERO).await.unwrap(); cmd.arg("script").arg(script).args([ "--rpc-url", &handle.http_endpoint(), From 835eae81b1398316bf8cdb12efd7f8d00a7322d3 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:00:06 +0200 Subject: [PATCH 3/4] test(script): cover Tempo synthetic phases --- crates/forge/tests/cli/script.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 6fab4ec922f58..74babcb515516 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -4814,7 +4814,21 @@ forgetest_async!(tempo_script_runs_with_zero_fee_token_balance, |prj, cmd| { import "forge-std/Script.sol"; contract TempoScript is Script { - function run() external {} + uint256 public value; + + constructor() { + value = 1; + } + + function setUp() external { + require(value == 1); + value = 2; + } + + function run() external { + require(value == 2); + value = 3; + } } "#, ); From 59f491753047060a60c8d1076f2a2c1cdc0c4cb6 Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:33:54 +0200 Subject: [PATCH 4/4] test(script): strengthen Tempo fee regression --- crates/forge/tests/cli/script.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 74babcb515516..7200e301d9dd9 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -4832,24 +4832,24 @@ contract TempoScript is Script { } "#, ); - prj.update_config(|config| { - config.gas_limit = u64::MAX.into(); - config.gas_price = Some(600_000_000); - }); - let (api, handle) = spawn(NodeConfig::test_tempo()).await; let fee_token = address!("0x20c0000000000000000000000000000000000000"); - // Synthetic script execution should not require the caller to fund protocol fees. + let fee_manager = address!("0xfeec000000000000000000000000000000000000"); + // Clear both balances so synthetic execution cannot accidentally succeed through fee + // accounting. api.anvil_deal_tip20(CALLER, fee_token, U256::ZERO).await.unwrap(); + api.anvil_deal_tip20(fee_manager, fee_token, U256::ZERO).await.unwrap(); cmd.arg("script").arg(script).args([ "--rpc-url", &handle.http_endpoint(), "--network", "tempo", - "--gas-price", - "600000000", "--tempo.fee-token", "0x20c0000000000000000000000000000000000000", + "--with-gas-price", + "600000000", + "--block-gas-limit", + "18446744073709551615", ]); cmd.assert_success(); });