diff --git a/Cargo.lock b/Cargo.lock index 0b03f6a1dd307..2b9d1ae1babba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1869,6 +1869,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "ethers-flashbots" +version = "0.13.1" +source = "git+https://github.com/ape-dev-cs/ethers-flashbots.git#7888d7f1e80a261cbb1a7ec6745767834ab27be5" +dependencies = [ + "async-trait", + "chrono", + "ethers", + "futures-core", + "futures-util", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror", + "url", +] + [[package]] name = "ethers-middleware" version = "2.0.8" @@ -2296,6 +2314,7 @@ dependencies = [ "dotenvy", "dunce", "ethers", + "ethers-flashbots", "eyre", "forge", "forge-doc", diff --git a/abi/abi/HEVM.sol b/abi/abi/HEVM.sol index 937785c064849..f771e2233d132 100644 --- a/abi/abi/HEVM.sol +++ b/abi/abi/HEVM.sol @@ -118,6 +118,9 @@ startBroadcast(address) startBroadcast(uint256) stopBroadcast() +startBundle(uint64,uint256) +stopBundle() + projectRoot()(string) readFile(string)(string) readFileBinary(string)(bytes) diff --git a/abi/src/bindings/hevm.rs b/abi/src/bindings/hevm.rs index 19235b396b539..06246b4a6b62a 100644 --- a/abi/src/bindings/hevm.rs +++ b/abi/src/bindings/hevm.rs @@ -4313,6 +4313,29 @@ pub mod hevm { }, ], ), + ( + ::std::borrow::ToOwned::to_owned("startBundle"), + ::std::vec![ + ::ethers_core::abi::ethabi::Function { + name: ::std::borrow::ToOwned::to_owned("startBundle"), + inputs: ::std::vec![ + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::Uint(64usize), + internal_type: ::core::option::Option::None, + }, + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::Uint(256usize), + internal_type: ::core::option::Option::None, + }, + ], + outputs: ::std::vec![], + constant: ::core::option::Option::None, + state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, + }, + ], + ), ( ::std::borrow::ToOwned::to_owned("startPrank"), ::std::vec![ @@ -4361,6 +4384,18 @@ pub mod hevm { }, ], ), + ( + ::std::borrow::ToOwned::to_owned("stopBundle"), + ::std::vec![ + ::ethers_core::abi::ethabi::Function { + name: ::std::borrow::ToOwned::to_owned("stopBundle"), + inputs: ::std::vec![], + outputs: ::std::vec![], + constant: ::core::option::Option::None, + state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, + }, + ], + ), ( ::std::borrow::ToOwned::to_owned("stopPrank"), ::std::vec![ @@ -6497,6 +6532,16 @@ pub mod hevm { .method_hash([206, 129, 125, 71], p0) .expect("method not found (this should never happen)") } + ///Calls the contract's `startBundle` (0x13580fe5) function + pub fn start_bundle( + &self, + p0: u64, + p1: ::ethers_core::types::U256, + ) -> ::ethers_contract::builders::ContractCall { + self.0 + .method_hash([19, 88, 15, 229], (p0, p1)) + .expect("method not found (this should never happen)") + } ///Calls the contract's `startPrank` (0x06447d56) function pub fn start_prank_0( &self, @@ -6524,6 +6569,12 @@ pub mod hevm { .method_hash([118, 234, 221, 54], ()) .expect("method not found (this should never happen)") } + ///Calls the contract's `stopBundle` (0x433ac3ce) function + pub fn stop_bundle(&self) -> ::ethers_contract::builders::ContractCall { + self.0 + .method_hash([67, 58, 195, 206], ()) + .expect("method not found (this should never happen)") + } ///Calls the contract's `stopPrank` (0x90c5013b) function pub fn stop_prank(&self) -> ::ethers_contract::builders::ContractCall { self.0 @@ -9160,6 +9211,19 @@ pub mod hevm { )] #[ethcall(name = "startBroadcast", abi = "startBroadcast(uint256)")] pub struct StartBroadcast2Call(pub ::ethers_core::types::U256); + ///Container type for all input parameters for the `startBundle` function with signature `startBundle(uint64,uint256)` and selector `0x13580fe5` + #[derive( + Clone, + ::ethers_contract::EthCall, + ::ethers_contract::EthDisplay, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + #[ethcall(name = "startBundle", abi = "startBundle(uint64,uint256)")] + pub struct StartBundleCall(pub u64, pub ::ethers_core::types::U256); ///Container type for all input parameters for the `startPrank` function with signature `startPrank(address)` and selector `0x06447d56` #[derive( Clone, @@ -9202,6 +9266,19 @@ pub mod hevm { )] #[ethcall(name = "stopBroadcast", abi = "stopBroadcast()")] pub struct StopBroadcastCall; + ///Container type for all input parameters for the `stopBundle` function with signature `stopBundle()` and selector `0x433ac3ce` + #[derive( + Clone, + ::ethers_contract::EthCall, + ::ethers_contract::EthDisplay, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + #[ethcall(name = "stopBundle", abi = "stopBundle()")] + pub struct StopBundleCall; ///Container type for all input parameters for the `stopPrank` function with signature `stopPrank()` and selector `0x90c5013b` #[derive( Clone, @@ -9604,9 +9681,11 @@ pub mod hevm { StartBroadcast0(StartBroadcast0Call), StartBroadcast1(StartBroadcast1Call), StartBroadcast2(StartBroadcast2Call), + StartBundle(StartBundleCall), StartPrank0(StartPrank0Call), StartPrank1(StartPrank1Call), StopBroadcast(StopBroadcastCall), + StopBundle(StopBundleCall), StopPrank(StopPrankCall), Store(StoreCall), ToString0(ToString0Call), @@ -10354,6 +10433,10 @@ pub mod hevm { = ::decode(data) { return Ok(Self::StartBroadcast2(decoded)); } + if let Ok(decoded) + = ::decode(data) { + return Ok(Self::StartBundle(decoded)); + } if let Ok(decoded) = ::decode(data) { return Ok(Self::StartPrank0(decoded)); @@ -10366,6 +10449,10 @@ pub mod hevm { = ::decode(data) { return Ok(Self::StopBroadcast(decoded)); } + if let Ok(decoded) + = ::decode(data) { + return Ok(Self::StopBundle(decoded)); + } if let Ok(decoded) = ::decode(data) { return Ok(Self::StopPrank(decoded)); @@ -10845,6 +10932,9 @@ pub mod hevm { Self::StartBroadcast2(element) => { ::ethers_core::abi::AbiEncode::encode(element) } + Self::StartBundle(element) => { + ::ethers_core::abi::AbiEncode::encode(element) + } Self::StartPrank0(element) => { ::ethers_core::abi::AbiEncode::encode(element) } @@ -10854,6 +10944,9 @@ pub mod hevm { Self::StopBroadcast(element) => { ::ethers_core::abi::AbiEncode::encode(element) } + Self::StopBundle(element) => { + ::ethers_core::abi::AbiEncode::encode(element) + } Self::StopPrank(element) => { ::ethers_core::abi::AbiEncode::encode(element) } @@ -11092,9 +11185,11 @@ pub mod hevm { Self::StartBroadcast0(element) => ::core::fmt::Display::fmt(element, f), Self::StartBroadcast1(element) => ::core::fmt::Display::fmt(element, f), Self::StartBroadcast2(element) => ::core::fmt::Display::fmt(element, f), + Self::StartBundle(element) => ::core::fmt::Display::fmt(element, f), Self::StartPrank0(element) => ::core::fmt::Display::fmt(element, f), Self::StartPrank1(element) => ::core::fmt::Display::fmt(element, f), Self::StopBroadcast(element) => ::core::fmt::Display::fmt(element, f), + Self::StopBundle(element) => ::core::fmt::Display::fmt(element, f), Self::StopPrank(element) => ::core::fmt::Display::fmt(element, f), Self::Store(element) => ::core::fmt::Display::fmt(element, f), Self::ToString0(element) => ::core::fmt::Display::fmt(element, f), @@ -11970,6 +12065,11 @@ pub mod hevm { Self::StartBroadcast2(value) } } + impl ::core::convert::From for HEVMCalls { + fn from(value: StartBundleCall) -> Self { + Self::StartBundle(value) + } + } impl ::core::convert::From for HEVMCalls { fn from(value: StartPrank0Call) -> Self { Self::StartPrank0(value) @@ -11985,6 +12085,11 @@ pub mod hevm { Self::StopBroadcast(value) } } + impl ::core::convert::From for HEVMCalls { + fn from(value: StopBundleCall) -> Self { + Self::StopBundle(value) + } + } impl ::core::convert::From for HEVMCalls { fn from(value: StopPrankCall) -> Self { Self::StopPrank(value) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index df86e92081d5f..8329bab34cac0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,6 +25,8 @@ ui = { path = "../ui" } # eth ethers = { workspace = true, features = ["rustls"] } solang-parser.workspace = true +ethers-flashbots = { git = "https://github.com/ape-dev-cs/ethers-flashbots.git" } + # cli clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } diff --git a/cli/src/cmd/forge/script/broadcast.rs b/cli/src/cmd/forge/script/broadcast.rs index 320eeb308ec94..9d3e715d8cdb6 100644 --- a/cli/src/cmd/forge/script/broadcast.rs +++ b/cli/src/cmd/forge/script/broadcast.rs @@ -11,13 +11,15 @@ use crate::{ update_progress, }; use ethers::{ - prelude::{Provider, Signer, TxHash}, + prelude::{Provider, Signer, TxHash, rand::thread_rng, SignerMiddleware, k256::ecdsa::SigningKey}, providers::{JsonRpcClient, Middleware}, - utils::format_units, + utils::format_units, signers::Wallet, types::{U64, Signature}, }; +use ethers_flashbots::{FlashbotsMiddleware, BundleRequest, PendingBundleError}; use eyre::{bail, ContextCompat, Result, WrapErr}; use foundry_common::{estimate_eip1559_fees, shell, try_get_http_provider, RetryProvider}; use futures::StreamExt; +use reqwest::Url; use std::{cmp::min, collections::HashSet, ops::Mul, sync::Arc}; use tracing::trace; @@ -85,12 +87,15 @@ impl ScriptArgs { } }; + trace!(target: "script", "transactions: {:#?}", &deployment_sequence.transactions); + // Iterate through transactions, matching the `from` field with the associated // wallet. Then send the transaction. Panics if we find a unknown `from` let sequence = deployment_sequence .transactions .iter() .skip(already_broadcasted) + .filter(|tx| tx.bundle_block.is_none() && tx.bundle_gas.is_none()) .map(|tx_with_metadata| { let tx = tx_with_metadata.typed_tx(); let from = *tx.from().expect("No sender for onchain transaction!"); @@ -123,6 +128,46 @@ impl ScriptArgs { }) .collect::>>()?; + let flashbots_sequence = deployment_sequence + .transactions + .iter() + .skip(already_broadcasted) + .filter(|tx| tx.bundle_block.is_some() && tx.bundle_gas.is_some()) + .map(|tx_with_metadata| { + let tx = tx_with_metadata.typed_tx(); + let from = *tx.from().expect("No sender for onchain transaction!"); + + let kind = send_kind.for_sender(&from)?; + let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit; + + let mut tx = tx.clone(); + + tx.set_chain_id(chain); + + if let Some(gas_price) = self.with_gas_price { + tx.set_gas_price(gas_price); + } else { + // fill gas price + match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => { + tx.set_gas_price(gas_price.expect("Could not get gas_price.")); + } + TypedTransaction::Eip1559(ref mut inner) => { + let eip1559_fees = + eip1559_fees.expect("Could not get eip1559 fee estimation."); + let prio_fee = tx_with_metadata.bundle_gas.expect("Max priority fee per gas must be set"); + inner.max_fee_per_gas = Some(eip1559_fees.0.overflowing_add(prio_fee).0); + inner.max_priority_fee_per_gas = tx_with_metadata.bundle_gas; + } + } + } + + Ok((tx, tx_with_metadata.bundle_block, kind, is_fixed_gas_limit)) + }) + .collect::>>()?; + + trace!(target: "script", "sequence: {:#?}", &sequence); + let pb = init_progress!(deployment_sequence.transactions, "txes"); // We send transactions and wait for receipts in batches of 100, since some networks @@ -130,6 +175,64 @@ impl ScriptArgs { let batch_size = 100; let mut index = 0; + let bundle_map: HashMap> = flashbots_sequence.into_iter().fold( + HashMap::new(), + |mut acc, (tx, block, kind, _)| { + acc.entry(block.expect("we should have a target block")) + .or_insert_with(Vec::new) + .push((tx, kind)); + acc + }, + ); + + for (target_block, transactions) in bundle_map.iter() { + let bundle_signer = LocalWallet::new(&mut thread_rng()); + + let mut bundle = BundleRequest::new(); + + let client = FlashbotsMiddleware::new( + provider.clone(), + Url::parse("https://relay.flashbots.net")?, + bundle_signer.clone(), + ); + + for (tx, kind) in transactions.into_iter() { + let signature = self.sign_flashbots( + provider.clone(), + &tx, + kind, + bundle_signer.clone() + ).await; + + trace!(target: "script", "tx: {:#?}", &tx); + + bundle = bundle.push_transaction(tx.rlp_signed(&signature.unwrap())); + } + + bundle = bundle.set_block(target_block.clone()) + .set_simulation_block(provider.get_block_number().await?) + .set_simulation_timestamp(0); + + trace!(target: "script", "bundle: {:#?}", &bundle); + + let simulated_bundle = client.simulate_bundle(&bundle).await?; + trace!(target: "script","Simulated bundle: {:?}", simulated_bundle); + + // Send it + let pending_bundle = client.send_bundle(&bundle).await?; + match pending_bundle.await { + Ok(bundle_hash) => trace!( + target: "script", + "Bundle with hash {:?} was included in target block", + bundle_hash + ), + Err(PendingBundleError::BundleNotIncluded) => { + trace!(target: "script", "Bundle was not included in target block.") + } + Err(e) => trace!(target: "script", "An error occured: {}", e), + } + } + for (batch_number, batch) in sequence.chunks(batch_size).map(|f| f.to_vec()).enumerate() { let mut pending_transactions = vec![]; @@ -211,6 +314,31 @@ impl ScriptArgs { Ok(()) } + + async fn sign_flashbots( + &self, + provider: Arc, + tx: &TypedTransaction, + kind: &SendTransactionKind<'_>, + bundle_signer: Wallet + ) -> Result { + match kind { + SendTransactionKind::Unlocked(_) => { + panic!("Need a local signer to send a flashbots bundle.") + } + SendTransactionKind::Raw(signer) => { + let client = SignerMiddleware::new( + FlashbotsMiddleware::new( + provider.clone(), + Url::parse("https://relay.flashbots.net")?, + bundle_signer, + ), + signer.clone(), + ); + Ok(client.signer().sign_transaction(&tx).await?) + } + } + } async fn send_transaction( &self, @@ -241,9 +369,9 @@ impl ScriptArgs { // 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 && - (has_different_gas_calc(provider.get_chainid().await?.as_u64()) || - self.skip_simulation) + if !is_fixed_gas_limit + && (has_different_gas_calc(provider.get_chainid().await?.as_u64()) + || self.skip_simulation) { self.estimate_gas(&mut tx, &provider).await?; } @@ -377,7 +505,7 @@ impl ScriptArgs { &mut script_config.config, returns, ) - .await + .await; } else if self.broadcast { eyre::bail!("No onchain transactions generated in script"); } @@ -401,6 +529,8 @@ impl ScriptArgs { .map(|btx| { let mut tx = TransactionWithMetadata::from_typed_transaction(btx.transaction); tx.rpc = btx.rpc; + tx.bundle_block = btx.bundle_block; + tx.bundle_gas = btx.bundle_gas; tx }) .collect() @@ -614,7 +744,7 @@ impl ScriptArgs { } /// How to send a single transaction -#[derive(Clone)] +#[derive(Debug, Clone)] enum SendTransactionKind<'a> { Unlocked(Address), Raw(&'a WalletSigner), diff --git a/cli/src/cmd/forge/script/cmd.rs b/cli/src/cmd/forge/script/cmd.rs index 2b5dd924b77ce..e23044a16d52c 100644 --- a/cli/src/cmd/forge/script/cmd.rs +++ b/cli/src/cmd/forge/script/cmd.rs @@ -177,6 +177,8 @@ impl ScriptArgs { for tx in txs.iter() { lib_deploy.push_back(BroadcastableTransaction { rpc: tx.rpc.clone(), + bundle_block: tx.bundle_block.clone(), + bundle_gas: tx.bundle_gas.clone(), transaction: TypedTransaction::Legacy(tx.transaction.clone().into()), }); } @@ -338,6 +340,8 @@ impl ScriptArgs { for new_tx in new_txs.iter() { txs.push_back(BroadcastableTransaction { rpc: new_tx.rpc.clone(), + bundle_block: new_tx.bundle_block.clone(), + bundle_gas: new_tx.bundle_gas.clone(), transaction: TypedTransaction::Legacy(new_tx.transaction.clone().into()), }); } diff --git a/cli/src/cmd/forge/script/executor.rs b/cli/src/cmd/forge/script/executor.rs index 2ad610482ff2c..ae8d7ff50dfa8 100644 --- a/cli/src/cmd/forge/script/executor.rs +++ b/cli/src/cmd/forge/script/executor.rs @@ -145,6 +145,9 @@ impl ScriptArgs { .expect("to have been built.") .write(); + let bundle_block = transaction.bundle_block; + let bundle_gas = transaction.clone().bundle_gas; + if let TypedTransaction::Legacy(mut tx) = transaction.transaction { let result = runner .simulate( @@ -201,6 +204,8 @@ impl ScriptArgs { decoder, created_contracts, is_fixed_gas_limit, + bundle_block, + bundle_gas )?; Ok((Some(tx), result.traces)) diff --git a/cli/src/cmd/forge/script/mod.rs b/cli/src/cmd/forge/script/mod.rs index a780e36b7658e..0a6ce9b322aeb 100644 --- a/cli/src/cmd/forge/script/mod.rs +++ b/cli/src/cmd/forge/script/mod.rs @@ -427,6 +427,8 @@ impl ScriptArgs { .enumerate() .map(|(i, bytes)| BroadcastableTransaction { rpc: fork_url.clone(), + bundle_block: None, + bundle_gas: None, transaction: TypedTransaction::Legacy(TransactionRequest { from: Some(from), data: Some(bytes.clone()), diff --git a/cli/src/cmd/forge/script/multi.rs b/cli/src/cmd/forge/script/multi.rs index 566c31e875e9c..5f4b6f719f07e 100644 --- a/cli/src/cmd/forge/script/multi.rs +++ b/cli/src/cmd/forge/script/multi.rs @@ -142,7 +142,7 @@ impl ScriptArgs { .iter_mut() .map(|sequence| async move { let provider = Arc::new(get_http_provider( - sequence.typed_transactions().first().unwrap().0.clone(), + sequence.typed_transactions().first().unwrap().0.rpc.clone(), )); receipts::wait_for_pending(provider, sequence).await }) @@ -165,7 +165,7 @@ impl ScriptArgs { match self .send_transactions( sequence, - &sequence.typed_transactions().first().unwrap().0.clone(), + &sequence.typed_transactions().first().unwrap().0.rpc.clone(), &script_wallets, ) .await diff --git a/cli/src/cmd/forge/script/sequence.rs b/cli/src/cmd/forge/script/sequence.rs index c55c712f31e0c..507b25d65c367 100644 --- a/cli/src/cmd/forge/script/sequence.rs +++ b/cli/src/cmd/forge/script/sequence.rs @@ -10,7 +10,7 @@ use crate::cmd::forge::{ use ethers::{ abi::Address, prelude::{artifacts::Libraries, ArtifactId, TransactionReceipt, TxHash}, - types::transaction::eip2718::TypedTransaction, + types::{transaction::eip2718::TypedTransaction, U256, U64}, }; use eyre::{ContextCompat, WrapErr}; use foundry_common::{fs, shell, SELECTOR_LEN}; @@ -48,6 +48,12 @@ pub struct ScriptSequence { pub commit: Option, } +pub struct TransactionMetadata { + pub rpc: String, + pub bundle_block: Option, + pub bundle_gas: Option, +} + /// Sensitive values from the transactions in a script sequence #[derive(Deserialize, Serialize, Clone, Default)] pub struct SensitiveTransactionMetadata { @@ -271,8 +277,8 @@ impl ScriptSequence { verify.set_chain(config, self.chain.into()); - if verify.etherscan.key.is_some() || - verify.verifier.verifier != VerificationProviderType::Etherscan + if verify.etherscan.key.is_some() + || verify.verifier.verifier != VerificationProviderType::Etherscan { trace!(target: "script", "prepare future verifications"); @@ -355,11 +361,18 @@ impl ScriptSequence { } /// Returns the list of the transactions without the metadata. - pub fn typed_transactions(&self) -> Vec<(String, &TypedTransaction)> { + pub fn typed_transactions(&self) -> Vec<(TransactionMetadata, &TypedTransaction)> { self.transactions .iter() .map(|tx| { - (tx.rpc.clone().expect("to have been filled with a proper rpc"), tx.typed_tx()) + ( + TransactionMetadata { + rpc: tx.rpc.clone().expect("to have been filled with a proper rpc"), + bundle_block: tx.bundle_block, + bundle_gas: tx.bundle_gas, + }, + tx.typed_tx(), + ) }) .collect() } @@ -379,12 +392,12 @@ impl Drop for ScriptSequence { fn sig_to_file_name(sig: &str) -> String { if let Some((name, _)) = sig.split_once('(') { // strip until call argument parenthesis - return name.to_string() + return name.to_string(); } // assume calldata if `sig` is hex if let Ok(calldata) = hex::decode(sig) { // in which case we return the function signature - return hex::encode(&calldata[..SELECTOR_LEN]) + return hex::encode(&calldata[..SELECTOR_LEN]); } // return sig as is diff --git a/cli/src/cmd/forge/script/transaction.rs b/cli/src/cmd/forge/script/transaction.rs index f1ad9cabc26d1..e72798ed5cd34 100644 --- a/cli/src/cmd/forge/script/transaction.rs +++ b/cli/src/cmd/forge/script/transaction.rs @@ -4,7 +4,7 @@ use ethers::{ abi, abi::Address, prelude::{NameOrAddress, H256 as TxHash}, - types::transaction::eip2718::TypedTransaction, + types::{transaction::eip2718::TypedTransaction, U256, U64}, }; use eyre::{ContextCompat, WrapErr}; use foundry_common::{abi::format_token_raw, RpcUrl, SELECTOR_LEN}; @@ -42,6 +42,8 @@ pub struct TransactionWithMetadata { pub transaction: TypedTransaction, pub additional_contracts: Vec, pub is_fixed_gas_limit: bool, + pub bundle_block: Option, + pub bundle_gas: Option, } fn default_string() -> Option { @@ -69,8 +71,10 @@ impl TransactionWithMetadata { decoder: &CallTraceDecoder, additional_contracts: Vec, is_fixed_gas_limit: bool, + bundle_block: Option, + bundle_gas: Option, ) -> eyre::Result { - let mut metadata = Self { transaction, rpc, is_fixed_gas_limit, ..Default::default() }; + let mut metadata = Self { transaction, rpc, is_fixed_gas_limit, bundle_block, bundle_gas, ..Default::default() }; // Specify if any contract was directly created with this transaction if let Some(NameOrAddress::Address(to)) = metadata.transaction.to().cloned() { @@ -109,6 +113,7 @@ impl TransactionWithMetadata { }) .collect(); } + Ok(metadata) } diff --git a/cli/test-utils/src/script.rs b/cli/test-utils/src/script.rs index 8411532ed757a..78a77fb0b8c51 100644 --- a/cli/test-utils/src/script.rs +++ b/cli/test-utils/src/script.rs @@ -222,6 +222,8 @@ impl ScriptTester { if !output.contains(expected.as_str()) { panic!("OUTPUT: {output}\n\nEXPECTED: {}", expected.as_str()); } + // println!("{output}"); + self } diff --git a/cli/tests/it/script.rs b/cli/tests/it/script.rs index b21f505cce32d..f460f946df4e4 100644 --- a/cli/tests/it/script.rs +++ b/cli/tests/it/script.rs @@ -451,6 +451,23 @@ forgetest_async!(can_deploy_script_with_lib, |prj: TestProject, cmd: TestCommand .await; }); +forgetest_async!(can_run_script_with_flashbots, |prj: TestProject, cmd: TestCommand| async move { + let node_config = NodeConfig::test() + .with_eth_rpc_url(Some(rpc::next_http_archive_rpc_endpoint())) + .silent(); +let (_api, handle) = spawn(node_config).await; + let mut tester = ScriptTester::new_broadcast(cmd, &handle.http_endpoint(), prj.root()); + + tester + .load_private_keys(vec![1]) + .await + .add_sig("BroadcastTestNoLinking", "deployFlashbots()") + .simulate(ScriptOutcome::OkSimulation) + .broadcast(ScriptOutcome::OkBroadcast) + .assert_nonce_increment(vec![(0, 2)]) + .await; +}); + forgetest_async!( #[serial_test::serial] can_deploy_script_private_key, diff --git a/evm/src/executor/inspector/cheatcodes/env.rs b/evm/src/executor/inspector/cheatcodes/env.rs index 00501c2574a82..998b5df60639f 100644 --- a/evm/src/executor/inspector/cheatcodes/env.rs +++ b/evm/src/executor/inspector/cheatcodes/env.rs @@ -13,7 +13,7 @@ use crate::{ use ethers::{ abi::{self, AbiEncode, RawLog, Token, Tokenizable, Tokenize}, signers::{LocalWallet, Signer}, - types::{Address, Bytes, U256}, + types::{Address, Bytes, U256, U64}, }; use foundry_config::Config; use revm::{ @@ -577,6 +577,25 @@ pub fn apply( true, )? } + HEVMCalls::StartBundle(inner) => { + ensure!(state.broadcast.is_some(), "You must have already started broadcasting."); + ensure!( + state.bundle_enabled == false, + "You must not have already started a bundle." + ); + state.bundle_enabled = true; + state.bundle_block = U64::from(inner.0); + state.bundle_gas = inner.1; + + + Bytes::new() + } + HEVMCalls::StopBundle(_) => { + ensure!(state.broadcast.is_some(), "You must not have stopped broadcasting."); + ensure!(state.bundle_enabled == true, "You must start a bundle first."); + state.bundle_enabled = false; + Bytes::new() + } HEVMCalls::StartBroadcast0(_) => { correct_sender_nonce( b160_to_h160(data.env.tx.caller), diff --git a/evm/src/executor/inspector/cheatcodes/mod.rs b/evm/src/executor/inspector/cheatcodes/mod.rs index 70a4b9b1ecab6..ff8396b36245a 100644 --- a/evm/src/executor/inspector/cheatcodes/mod.rs +++ b/evm/src/executor/inspector/cheatcodes/mod.rs @@ -16,7 +16,7 @@ use ethers::{ signers::LocalWallet, types::{ transaction::eip2718::TypedTransaction, Address, Bytes, NameOrAddress, TransactionRequest, - U256, + U256, U64, }, }; use foundry_common::evm::Breakpoints; @@ -187,6 +187,10 @@ pub struct Cheatcodes { /// Breakpoints supplied by the `vm.breakpoint("")` cheatcode /// char -> pc pub breakpoints: Breakpoints, + + pub bundle_block: U64, + pub bundle_gas: U256, + pub bundle_enabled: bool, } impl Cheatcodes { @@ -696,6 +700,8 @@ where self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: data.db.active_fork_url(), + bundle_block: if self.bundle_enabled {Some(self.bundle_block)} else { None }, + bundle_gas: if self.bundle_enabled {Some(self.bundle_gas)} else { None }, transaction: TypedTransaction::Legacy(TransactionRequest { from: Some(broadcast.new_origin), to: Some(NameOrAddress::Address(b160_to_h160(call.contract))), @@ -1010,6 +1016,8 @@ where self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: data.db.active_fork_url(), + bundle_block: if self.bundle_enabled {Some(self.bundle_block)} else { None }, + bundle_gas: if self.bundle_enabled {Some(self.bundle_gas)} else { None }, transaction: TypedTransaction::Legacy(TransactionRequest { from: Some(broadcast.new_origin), to, diff --git a/evm/src/executor/inspector/cheatcodes/util.rs b/evm/src/executor/inspector/cheatcodes/util.rs index abb9aad05aba6..b16b21ae8fb75 100644 --- a/evm/src/executor/inspector/cheatcodes/util.rs +++ b/evm/src/executor/inspector/cheatcodes/util.rs @@ -46,6 +46,8 @@ pub const MAGIC_SKIP_BYTES: &[u8] = b"FOUNDRY::SKIP"; #[derive(Debug, Clone, Default)] pub struct BroadcastableTransaction { pub rpc: Option, + pub bundle_block: Option, + pub bundle_gas: Option, pub transaction: TypedTransaction, } diff --git a/testdata/cheats/Broadcast.t.sol b/testdata/cheats/Broadcast.t.sol index c7d05acb7c501..fea35228de9ce 100644 --- a/testdata/cheats/Broadcast.t.sol +++ b/testdata/cheats/Broadcast.t.sol @@ -199,6 +199,19 @@ contract BroadcastTestNoLinking is DSTest { test.t(0); } + function deployFlashbots() public { + cheats.startBroadcast(ACCOUNT_B); + cheats.startBundle(uint64(block.number + 1), 500 gwei); + NoLink test = new NoLink(); + cheats.stopBroadcast(); + + // this will + cheats.startBroadcast(ACCOUNT_B); + test.t(2); + cheats.stopBundle(); + cheats.stopBroadcast(); + } + function deployMany() public { assert(vm.getNonce(msg.sender) == 0); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index e05ea43bb7aa7..f3a350b0acd36 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -320,6 +320,12 @@ interface Vm { // Has all subsequent calls (at this call depth only) create transactions with the private key provided that can later be signed and sent onchain function startBroadcast(uint256) external; + // Has all subsequent transactions add to a flashbots bundle instead of being sent directly and awaiting receipts + function startBundle(uint64 futureBlock, uint256 gasPrice) external; + + // Stops having all subsequent transactions add to a flashbots bundle instead of being sent directly and awaiting receipts + function stopBundle() external; + // Stops collecting onchain transactions function stopBroadcast() external;