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 0f077d824be74..9aae06cea2fd1 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,54 @@ forgetest!(can_execute_script_command_with_tempo, |prj, cmd| { .assert_success(); }); +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", + r#" +import "forge-std/Script.sol"; + +contract TempoScript is Script { + uint256 public value; + + constructor() { + value = 1; + } + + function setUp() external { + require(value == 1); + value = 2; + } + + function run() external { + require(value == 2); + value = 3; + } +} +"#, + ); + let (api, handle) = spawn(NodeConfig::test_tempo()).await; + let fee_token = address!("0x20c0000000000000000000000000000000000000"); + 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", + "--tempo.fee-token", + "0x20c0000000000000000000000000000000000000", + "--with-gas-price", + "600000000", + "--block-gas-limit", + "18446744073709551615", + ]); + cmd.assert_success(); +}); + forgetest_async!(tempo_aa_script_broadcast_deploys_with_fee_token, |prj, cmd| { foundry_test_utils::util::initialize(prj.root()); prj.add_source( diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 199598c84db7b..4aba1566821d1 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( @@ -1146,6 +1156,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}; @@ -1935,6 +1946,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([