From 6f6b86e6e04b1a115d143f174f1b9969b6c2a554 Mon Sep 17 00:00:00 2001 From: teddav Date: Fri, 26 Jul 2024 11:29:13 +0200 Subject: [PATCH] feat: sendRawTransaction cheatcode (#4931) * feat: sendRawTransaction cheatcode * added unit tests * clippy + forge fmt * rebase * rename cheatcode to broadcastrawtransaction * revert anvil to sendrawtransaction + rename enum to Unsigned * better TransactionMaybeSigned * fix: ci * fixes * review fixes * add newline * Update crates/common/src/transactions.rs * Update crates/script/src/broadcast.rs * revm now uses Alloys AccessList: https://github.com/bluealloy/revm/pull/1552/files * only broadcast if you can transact, reorder cheatcode to be in broadcast section + document its behavior * update spec --------- Co-authored-by: Arsenii Kulikov Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks Co-authored-by: Matthias Seitz --- Cargo.lock | 4 + crates/cheatcodes/Cargo.toml | 4 +- crates/cheatcodes/assets/cheatcodes.json | 20 + crates/cheatcodes/spec/src/vm.rs | 4 + crates/cheatcodes/src/evm.rs | 35 +- crates/cheatcodes/src/inspector.rs | 12 +- crates/common/Cargo.toml | 1 + crates/common/src/transactions.rs | 89 ++++- crates/evm/core/src/backend/cow.rs | 11 + crates/evm/core/src/backend/mod.rs | 61 ++- crates/script/Cargo.toml | 1 + crates/script/src/broadcast.rs | 128 ++++--- crates/script/src/build.rs | 2 +- crates/script/src/execute.rs | 4 +- crates/script/src/lib.rs | 6 +- crates/script/src/runner.rs | 6 +- crates/script/src/sequence.rs | 13 +- crates/script/src/simulate.rs | 97 ++--- crates/script/src/transaction.rs | 35 +- docs/dev/cheatcodes.md | 2 +- testdata/cheats/Vm.sol | 1 + .../cheats/BroadcastRawTransaction.t.sol | 346 ++++++++++++++++++ 22 files changed, 737 insertions(+), 145 deletions(-) create mode 100644 testdata/default/cheats/BroadcastRawTransaction.t.sol diff --git a/Cargo.lock b/Cargo.lock index a0797e8ad68d..dccf91eef047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3444,6 +3444,7 @@ name = "forge-script" version = "0.2.0" dependencies = [ "alloy-chains", + "alloy-consensus", "alloy-dyn-abi", "alloy-eips", "alloy-json-abi", @@ -3563,11 +3564,13 @@ dependencies = [ name = "foundry-cheatcodes" version = "0.2.0" dependencies = [ + "alloy-consensus", "alloy-dyn-abi", "alloy-genesis", "alloy-json-abi", "alloy-primitives", "alloy-provider", + "alloy-rlp", "alloy-rpc-types", "alloy-signer", "alloy-signer-local", @@ -3649,6 +3652,7 @@ dependencies = [ name = "foundry-common" version = "0.2.0" dependencies = [ + "alloy-consensus", "alloy-contract", "alloy-dyn-abi", "alloy-json-abi", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 93636c3f2f6d..60304a47dd0d 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -28,13 +28,15 @@ alloy-primitives.workspace = true alloy-genesis.workspace = true alloy-sol-types.workspace = true alloy-provider.workspace = true -alloy-rpc-types.workspace = true +alloy-rpc-types = { workspace = true, features = ["k256"] } alloy-signer.workspace = true alloy-signer-local = { workspace = true, features = [ "mnemonic-all-languages", "keystore", ] } parking_lot.workspace = true +alloy-consensus = { workspace = true, features = ["k256"] } +alloy-rlp.workspace = true eyre.workspace = true itertools.workspace = true diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index a6a9c94795ee..ec12113eb31b 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -3051,6 +3051,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "broadcastRawTransaction", + "description": "Takes a signed transaction and broadcasts it to the network.", + "declaration": "function broadcastRawTransaction(bytes calldata data) external;", + "visibility": "external", + "mutability": "", + "signature": "broadcastRawTransaction(bytes)", + "selector": "0x8c0c72e0", + "selectorBytes": [ + 140, + 12, + 114, + 224 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "broadcast_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index a009c9f598e2..ff9a92e58930 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1759,6 +1759,10 @@ interface Vm { #[cheatcode(group = Scripting)] function stopBroadcast() external; + /// Takes a signed transaction and broadcasts it to the network. + #[cheatcode(group = Scripting)] + function broadcastRawTransaction(bytes calldata data) external; + // ======== Utilities ======== // -------- Strings -------- diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 33b064201159..97923a948791 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1,8 +1,12 @@ //! Implementations of [`Evm`](spec::Group::Evm) cheatcodes. -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; +use crate::{ + BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, +}; +use alloy_consensus::TxEnvelope; use alloy_genesis::{Genesis, GenesisAccount}; use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_rlp::Decodable; use alloy_sol_types::SolValue; use foundry_common::fs::{read_json_file, write_json_file}; use foundry_evm_core::{ @@ -567,6 +571,35 @@ impl Cheatcode for stopAndReturnStateDiffCall { } } +impl Cheatcode for broadcastRawTransactionCall { + fn apply_full( + &self, + ccx: &mut CheatsCtxt, + executor: &mut E, + ) -> Result { + let mut data = self.data.as_ref(); + let tx = TxEnvelope::decode(&mut data).map_err(|err| { + fmt_err!("broadcastRawTransaction: error decoding transaction ({err})") + })?; + + ccx.ecx.db.transact_from_tx( + tx.clone().into(), + &ccx.ecx.env, + &mut ccx.ecx.journaled_state, + &mut executor.get_inspector(ccx.state), + )?; + + if ccx.state.broadcast.is_some() { + ccx.state.broadcastable_transactions.push_back(BroadcastableTransaction { + rpc: ccx.db.active_fork_url(), + transaction: tx.try_into()?, + }); + } + + Ok(Default::default()) + } +} + impl Cheatcode for setBlockhashCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { blockNumber, blockHash } = *self; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index c989659e115b..13cc02df373b 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -19,7 +19,7 @@ use crate::{ use alloy_primitives::{hex, Address, Bytes, Log, TxKind, B256, U256}; use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; use alloy_sol_types::{SolCall, SolInterface, SolValue}; -use foundry_common::{evm::Breakpoints, SELECTOR_LEN}; +use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_config::Config; use foundry_evm_core::{ abi::Vm::stopExpectSafeMemoryCall, @@ -188,12 +188,12 @@ impl Context { } /// Helps collecting transactions from different forks. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct BroadcastableTransaction { /// The optional RPC URL. pub rpc: Option, /// The transaction to broadcast. - pub transaction: TransactionRequest, + pub transaction: TransactionMaybeSigned, } /// List of transactions that can be broadcasted. @@ -513,7 +513,8 @@ impl Cheatcodes { None }, ..Default::default() - }, + } + .into(), }); input.log_debug(self, &input.scheme().unwrap_or(CreateScheme::Create)); @@ -849,7 +850,8 @@ impl Cheatcodes { None }, ..Default::default() - }, + } + .into(), }); debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 094f32bed652..5c843979a97e 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -41,6 +41,7 @@ alloy-transport-http = { workspace = true, features = [ alloy-transport-ipc.workspace = true alloy-transport-ws.workspace = true alloy-transport.workspace = true +alloy-consensus = { workspace = true, features = ["k256"] } tower.workspace = true diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index 2693c8ac249b..c927d575dcc1 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -1,7 +1,9 @@ //! Wrappers for transactions. +use alloy_consensus::{Transaction, TxEnvelope}; +use alloy_primitives::{Address, TxKind, U256}; use alloy_provider::{network::AnyNetwork, Provider}; -use alloy_rpc_types::{AnyTransactionReceipt, BlockId}; +use alloy_rpc_types::{AnyTransactionReceipt, BlockId, TransactionRequest}; use alloy_serde::WithOtherFields; use alloy_transport::Transport; use eyre::Result; @@ -144,3 +146,88 @@ mod tests { assert_eq!(extract_revert_reason(error_string_2), None); } } + +/// Used for broadcasting transactions +/// A transaction can either be a [`TransactionRequest`] waiting to be signed +/// or a [`TxEnvelope`], already signed +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TransactionMaybeSigned { + Signed { + #[serde(flatten)] + tx: TxEnvelope, + from: Address, + }, + Unsigned(WithOtherFields), +} + +impl TransactionMaybeSigned { + /// Creates a new (unsigned) transaction for broadcast + pub fn new(tx: WithOtherFields) -> Self { + Self::Unsigned(tx) + } + + /// Creates a new signed transaction for broadcast. + pub fn new_signed( + tx: TxEnvelope, + ) -> core::result::Result { + let from = tx.recover_signer()?; + Ok(Self::Signed { tx, from }) + } + + pub fn as_unsigned_mut(&mut self) -> Option<&mut WithOtherFields> { + match self { + Self::Unsigned(tx) => Some(tx), + _ => None, + } + } + + pub fn from(&self) -> Option
{ + match self { + Self::Signed { from, .. } => Some(*from), + Self::Unsigned(tx) => tx.from, + } + } + + pub fn input(&self) -> Option<&[u8]> { + match self { + Self::Signed { tx, .. } => Some(tx.input()), + Self::Unsigned(tx) => tx.input.input().map(|i| i.as_ref()), + } + } + + pub fn to(&self) -> Option { + match self { + Self::Signed { tx, .. } => Some(tx.to()), + Self::Unsigned(tx) => tx.to, + } + } + + pub fn value(&self) -> Option { + match self { + Self::Signed { tx, .. } => Some(tx.value()), + Self::Unsigned(tx) => tx.value, + } + } + + pub fn gas(&self) -> Option { + match self { + Self::Signed { tx, .. } => Some(tx.gas_limit()), + Self::Unsigned(tx) => tx.gas, + } + } +} + +impl From for TransactionMaybeSigned { + fn from(tx: TransactionRequest) -> Self { + Self::new(WithOtherFields::new(tx)) + } +} + +impl TryFrom for TransactionMaybeSigned { + type Error = alloy_primitives::SignatureError; + + fn try_from(tx: TxEnvelope) -> core::result::Result { + Self::new_signed(tx) + } +} diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index 9ca63c513660..2dcd985ae912 100644 --- a/crates/evm/core/src/backend/cow.rs +++ b/crates/evm/core/src/backend/cow.rs @@ -10,6 +10,7 @@ use crate::{ }; use alloy_genesis::GenesisAccount; use alloy_primitives::{Address, B256, U256}; +use alloy_rpc_types::TransactionRequest; use eyre::WrapErr; use foundry_fork_db::DatabaseError; use revm::{ @@ -190,6 +191,16 @@ impl<'a> DatabaseExt for CowBackend<'a> { self.backend_mut(env).transact(id, transaction, env, journaled_state, inspector) } + fn transact_from_tx( + &mut self, + transaction: TransactionRequest, + env: &Env, + journaled_state: &mut JournaledState, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result<()> { + self.backend_mut(env).transact_from_tx(transaction, env, journaled_state, inspector) + } + fn active_fork_id(&self) -> Option { self.backend.active_fork_id() } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index 23c54e6e2154..7c2ed11c4b23 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -4,12 +4,14 @@ use crate::{ constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, TEST_CONTRACT_ADDRESS}, fork::{CreateFork, ForkId, MultiFork}, snapshot::Snapshots, - utils::configure_tx_env, + utils::{configure_tx_env, new_evm_with_inspector}, InspectorExt, }; use alloy_genesis::GenesisAccount; -use alloy_primitives::{keccak256, uint, Address, B256, U256}; -use alloy_rpc_types::{Block, BlockNumberOrTag, BlockTransactions, Transaction}; +use alloy_primitives::{keccak256, uint, Address, TxKind, B256, U256}; +use alloy_rpc_types::{ + Block, BlockNumberOrTag, BlockTransactions, Transaction, TransactionRequest, +}; use alloy_serde::WithOtherFields; use eyre::Context; use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE}; @@ -20,7 +22,7 @@ use revm::{ precompile::{PrecompileSpecId, Precompiles}, primitives::{ Account, AccountInfo, Bytecode, Env, EnvWithHandlerCfg, EvmState, EvmStorageSlot, - HashMap as Map, Log, ResultAndState, SpecId, TxKind, KECCAK_EMPTY, + HashMap as Map, Log, ResultAndState, SpecId, KECCAK_EMPTY, }, Database, DatabaseCommit, JournaledState, }; @@ -202,6 +204,15 @@ pub trait DatabaseExt: Database + DatabaseCommit { inspector: &mut dyn InspectorExt, ) -> eyre::Result<()>; + /// Executes a given TransactionRequest, commits the new state to the DB + fn transact_from_tx( + &mut self, + transaction: TransactionRequest, + env: &Env, + journaled_state: &mut JournaledState, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result<()>; + /// Returns the `ForkId` that's currently used in the database, if fork mode is on fn active_fork_id(&self) -> Option; @@ -1246,6 +1257,48 @@ impl DatabaseExt for Backend { ) } + fn transact_from_tx( + &mut self, + tx: TransactionRequest, + env: &Env, + journaled_state: &mut JournaledState, + inspector: &mut dyn InspectorExt, + ) -> eyre::Result<()> { + trace!(?tx, "execute signed transaction"); + + let mut env = env.clone(); + + env.tx.caller = + tx.from.ok_or_else(|| eyre::eyre!("transact_from_tx: No `from` field found"))?; + env.tx.gas_limit = + tx.gas.ok_or_else(|| eyre::eyre!("transact_from_tx: No `gas` field found"))? as u64; + env.tx.gas_price = U256::from(tx.gas_price.or(tx.max_fee_per_gas).unwrap_or_default()); + env.tx.gas_priority_fee = tx.max_priority_fee_per_gas.map(U256::from); + env.tx.nonce = tx.nonce; + env.tx.access_list = tx.access_list.clone().unwrap_or_default().0.into_iter().collect(); + env.tx.value = + tx.value.ok_or_else(|| eyre::eyre!("transact_from_tx: No `value` field found"))?; + env.tx.data = tx.input.into_input().unwrap_or_default(); + env.tx.transact_to = + tx.to.ok_or_else(|| eyre::eyre!("transact_from_tx: No `to` field found"))?; + env.tx.chain_id = tx.chain_id; + + self.commit(journaled_state.state.clone()); + + let res = { + let db = self.clone(); + let env = self.env_with_handler_cfg(env); + let mut evm = new_evm_with_inspector(db, env, inspector); + evm.context.evm.journaled_state.depth = journaled_state.depth + 1; + evm.transact()? + }; + + self.commit(res.state); + update_state(&mut journaled_state.state, self, None)?; + + Ok(()) + } + fn active_fork_id(&self) -> Option { self.active_fork_ids.map(|(id, _)| id) } diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 3ef1f6b68bcf..9c042661775c 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -53,6 +53,7 @@ alloy-dyn-abi.workspace = true alloy-primitives.workspace = true alloy-eips.workspace = true alloy-transport.workspace = true +alloy-consensus.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index b7eb9f5b0bb6..a1113bb2dbd2 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -3,6 +3,7 @@ use crate::{ verify::BroadcastedState, ScriptArgs, ScriptConfig, }; use alloy_chains::Chain; +use alloy_consensus::TxEnvelope; use alloy_eips::eip2718::Encodable2718; use alloy_network::{AnyNetwork, EthereumWallet, TransactionBuilder}; use alloy_primitives::{utils::format_units, Address, TxHash}; @@ -16,7 +17,7 @@ use foundry_cheatcodes::ScriptWallets; use foundry_cli::utils::{has_batch_support, has_different_gas_calc}; use foundry_common::{ provider::{get_http_provider, try_get_http_provider, RetryProvider}, - shell, + shell, TransactionMaybeSigned, }; use foundry_config::Config; use futures::{future::join_all, StreamExt}; @@ -55,45 +56,49 @@ pub async fn next_nonce(caller: Address, provider_url: &str) -> eyre::Result, - mut tx: WithOtherFields, - kind: SendTransactionKind<'_>, + mut kind: SendTransactionKind<'_>, sequential_broadcast: bool, is_fixed_gas_limit: bool, estimate_via_rpc: bool, estimate_multiplier: u64, ) -> Result { - let from = tx.from.expect("no sender"); + if let SendTransactionKind::Raw(tx, _) | SendTransactionKind::Unlocked(tx) = &mut kind { + if sequential_broadcast { + let from = tx.from.expect("no sender"); - if sequential_broadcast { - let nonce = provider.get_transaction_count(from).await?; + let nonce = provider.get_transaction_count(from).await?; - let tx_nonce = tx.nonce.expect("no nonce"); - if nonce != tx_nonce { - bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") + let tx_nonce = tx.nonce.expect("no nonce"); + if nonce != tx_nonce { + bail!("EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider.") + } } - } - // 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 { - estimate_gas(&mut tx, &provider, estimate_multiplier).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 { + estimate_gas(tx, &provider, estimate_multiplier).await?; + } } let pending = match kind { - SendTransactionKind::Unlocked(addr) => { - debug!("sending transaction from unlocked account {:?}: {:?}", addr, tx); + SendTransactionKind::Unlocked(tx) => { + debug!("sending transaction from unlocked account {:?}", tx); // Submit the transaction provider.send_transaction(tx).await? } - SendTransactionKind::Raw(signer) => { + SendTransactionKind::Raw(tx, signer) => { debug!("sending transaction: {:?}", tx); - let signed = tx.build(signer).await?; // Submit the raw transaction provider.send_raw_transaction(signed.encoded_2718().as_ref()).await? } + SendTransactionKind::Signed(tx) => { + debug!("sending transaction: {:?}", tx); + provider.send_raw_transaction(tx.encoded_2718().as_ref()).await? + } }; Ok(*pending.tx_hash()) @@ -102,8 +107,9 @@ pub async fn send_transaction( /// How to send a single transaction #[derive(Clone)] pub enum SendTransactionKind<'a> { - Unlocked(Address), - Raw(&'a EthereumWallet), + Unlocked(WithOtherFields), + Raw(WithOtherFields, &'a EthereumWallet), + Signed(TxEnvelope), } /// Represents how to send _all_ transactions @@ -118,31 +124,27 @@ impl SendTransactionsKind { /// Returns the [`SendTransactionKind`] for the given address /// /// Returns an error if no matching signer is found or the address is not unlocked - pub fn for_sender(&self, addr: &Address) -> Result> { + pub fn for_sender( + &self, + addr: &Address, + tx: WithOtherFields, + ) -> Result> { match self { Self::Unlocked(unlocked) => { if !unlocked.contains(addr) { bail!("Sender address {:?} is not unlocked", addr) } - Ok(SendTransactionKind::Unlocked(*addr)) + Ok(SendTransactionKind::Unlocked(tx)) } Self::Raw(wallets) => { if let Some(wallet) = wallets.get(addr) { - Ok(SendTransactionKind::Raw(wallet)) + Ok(SendTransactionKind::Raw(tx, wallet)) } else { bail!("No matching signer for {:?} found", addr) } } } } - - /// How many signers are set - pub fn signers_count(&self) -> usize { - match self { - Self::Unlocked(addr) => addr.len(), - Self::Raw(signers) => signers.len(), - } - } } /// State after we have bundled all @@ -189,11 +191,7 @@ impl BundledState { .sequence .sequences() .iter() - .flat_map(|sequence| { - sequence - .transactions() - .map(|tx| (tx.from().expect("No sender for onchain transaction!"))) - }) + .flat_map(|sequence| sequence.transactions().map(|tx| tx.from().expect("missing from"))) .collect::>(); if required_addresses.contains(&Config::DEFAULT_SENDER) { @@ -203,7 +201,7 @@ impl BundledState { } let send_kind = if self.args.unlocked { - SendTransactionsKind::Unlocked(required_addresses) + SendTransactionsKind::Unlocked(required_addresses.clone()) } else { let signers = self.script_wallets.into_multi_wallet().into_signers()?; let mut missing_addresses = Vec::new(); @@ -279,29 +277,38 @@ impl BundledState { .iter() .skip(already_broadcasted) .map(|tx_with_metadata| { - let tx = tx_with_metadata.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(sequence.chain); - - // Set TxKind::Create explicityly to satify `check_reqd_fields` in alloy - if tx.to().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); - } - - Ok((tx, kind, is_fixed_gas_limit)) + let kind = match tx_with_metadata.tx().clone() { + TransactionMaybeSigned::Signed { tx, .. } => { + SendTransactionKind::Signed(tx) + } + TransactionMaybeSigned::Unsigned(mut tx) => { + let from = tx.from.expect("No sender for onchain transaction!"); + + tx.set_chain_id(sequence.chain); + + // Set TxKind::Create explicitly to satify `check_reqd_fields` in + // alloy + if tx.to.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); + } + + send_kind.for_sender(&from, tx)? + } + }; + + Ok((kind, is_fixed_gas_limit)) }) .collect::>>()?; @@ -315,7 +322,7 @@ impl BundledState { // Or if we need to invoke eth_estimateGas before sending transactions. let sequential_broadcast = estimate_via_rpc || self.args.slow || - send_kind.signers_count() != 1 || + required_addresses.len() != 1 || !has_batch_support(sequence.chain); // We send transactions and wait for receipts in batches. @@ -330,10 +337,9 @@ impl BundledState { batch_number * batch_size, batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1 )); - for (tx, kind, is_fixed_gas_limit) in batch { + for (kind, is_fixed_gas_limit) in batch { let fut = send_transaction( provider.clone(), - tx.clone(), kind.clone(), sequential_broadcast, *is_fixed_gas_limit, diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 14feb42ff00e..b0c5a2947a4d 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -291,7 +291,7 @@ impl CompiledState { s.transactions .iter() .skip(s.receipts.len()) - .map(|t| t.transaction.from.expect("from is missing in script artifact")) + .map(|t| t.transaction.from().expect("from is missing in script artifact")) }); let available_signers = self diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 8f92f01bd55d..1eff7e0de63a 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -189,8 +189,8 @@ impl PreExecutionState { self.args.evm_opts.sender.is_none() { for tx in txs.iter() { - if tx.transaction.to.is_none() { - let sender = tx.transaction.from.expect("no sender"); + if tx.transaction.to().is_none() { + let sender = tx.transaction.from().expect("no sender"); if let Some(ns) = new_sender { if sender != ns { shell::println("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index b6370b43206c..97a33541f3d1 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -386,11 +386,9 @@ impl ScriptArgs { for (data, to) in result.transactions.iter().flat_map(|txes| { txes.iter().filter_map(|tx| { tx.transaction - .input - .clone() - .into_input() + .input() .filter(|data| data.len() > max_size) - .map(|data| (data, tx.transaction.to)) + .map(|data| (data, tx.transaction.to())) }) }) { let mut offset = 0; diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index a4f437644dbc..7b7a2375bb75 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -78,7 +78,8 @@ impl ScriptRunner { input: Some(code.clone()).into(), nonce: Some(sender_nonce + library_transactions.len() as u64), ..Default::default() - }, + } + .into(), }) }), ScriptPredeployLibraries::Create2(libraries, salt) => { @@ -112,7 +113,8 @@ impl ScriptRunner { nonce: Some(sender_nonce + library_transactions.len() as u64), to: Some(TxKind::Call(DEFAULT_CREATE2_DEPLOYER)), ..Default::default() - }, + } + .into(), }); } diff --git a/crates/script/src/sequence.rs b/crates/script/src/sequence.rs index 5a8e789a60b0..53212f4ebf3b 100644 --- a/crates/script/src/sequence.rs +++ b/crates/script/src/sequence.rs @@ -4,12 +4,11 @@ use crate::{ verify::VerifyBundle, }; use alloy_primitives::{hex, Address, TxHash}; -use alloy_rpc_types::{AnyTransactionReceipt, TransactionRequest}; -use alloy_serde::WithOtherFields; +use alloy_rpc_types::AnyTransactionReceipt; use eyre::{eyre, ContextCompat, Result, WrapErr}; use forge_verify::provider::VerificationProviderType; use foundry_cli::utils::{now, Git}; -use foundry_common::{fs, shell, SELECTOR_LEN}; +use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_compilers::ArtifactId; use foundry_config::Config; use serde::{Deserialize, Serialize}; @@ -284,10 +283,8 @@ impl ScriptSequence { } // Verify contract created directly from the transaction - if let (Some(address), Some(data)) = - (receipt.contract_address, tx.tx().input.input()) - { - match verify.get_verify_args(address, offset, &data.0, &self.libraries) { + if let (Some(address), Some(data)) = (receipt.contract_address, tx.tx().input()) { + match verify.get_verify_args(address, offset, data, &self.libraries) { Some(verify) => future_verifications.push(verify.run()), None => unverifiable_contracts.push(address), }; @@ -363,7 +360,7 @@ impl ScriptSequence { } /// Returns the list of the transactions without the metadata. - pub fn transactions(&self) -> impl Iterator> { + pub fn transactions(&self) -> impl Iterator { self.transactions.iter().map(|tx| tx.tx()) } diff --git a/crates/script/src/simulate.rs b/crates/script/src/simulate.rs index 4a5f44117320..077c507e24a3 100644 --- a/crates/script/src/simulate.rs +++ b/crates/script/src/simulate.rs @@ -13,7 +13,7 @@ use crate::{ ScriptArgs, ScriptConfig, ScriptResult, }; use alloy_network::TransactionBuilder; -use alloy_primitives::{utils::format_units, Address, TxKind, U256}; +use alloy_primitives::{utils::format_units, Address, Bytes, TxKind, U256}; use eyre::{Context, Result}; use foundry_cheatcodes::{BroadcastableTransactions, ScriptWallets}; use foundry_cli::utils::{has_different_gas_calc, now}; @@ -99,14 +99,14 @@ impl PreSimulationState { let mut runner = runners.get(&rpc).expect("invalid rpc url").write(); let mut tx = transaction.transaction; - let to = if let Some(TxKind::Call(to)) = tx.to { Some(to) } else { None }; + let to = if let Some(TxKind::Call(to)) = tx.to() { Some(to) } else { None }; let result = runner .simulate( - tx.from + tx.from() .expect("transaction doesn't have a `from` address at execution time"), to, - tx.input.clone().into_input(), - tx.value, + tx.input().map(Bytes::copy_from_slice), + tx.value(), ) .wrap_err("Internal EVM error during simulation")?; @@ -121,18 +121,25 @@ impl PreSimulationState { runner.executor.env_mut().block.number += U256::from(1); } - let is_fixed_gas_limit = tx.gas.is_some(); - match tx.gas { - // If tx.gas is already set that means it was specified in script - Some(gas) => { - println!("Gas limit was set in script to {gas}"); + let is_fixed_gas_limit = if let Some(tx) = tx.as_unsigned_mut() { + match tx.gas { + // If tx.gas is already set that means it was specified in script + Some(gas) => { + println!("Gas limit was set in script to {gas}"); + true + } + // We inflate the gas used by the user specified percentage + None => { + let gas = result.gas_used * self.args.gas_estimate_multiplier / 100; + tx.gas = Some(gas as u128); + false + } } - // We inflate the gas used by the user specified percentage - None => { - let gas = result.gas_used * self.args.gas_estimate_multiplier / 100; - tx.gas = Some(gas as u128); - } - } + } else { + // for pre-signed transactions we can't alter gas limit + true + }; + let tx = TransactionWithMetadata::new( tx, rpc, @@ -271,40 +278,48 @@ impl FilledTransactionsState { let tx_rpc = tx.rpc.clone(); let provider_info = manager.get_or_init_provider(&tx.rpc, self.args.legacy).await?; - // Handles chain specific requirements. - tx.transaction.set_chain_id(provider_info.chain); + if let Some(tx) = tx.transaction.as_unsigned_mut() { + // Handles chain specific requirements for unsigned transactions. + tx.set_chain_id(provider_info.chain); + } if !self.args.skip_simulation { let tx = tx.tx_mut(); if has_different_gas_calc(provider_info.chain) { - trace!("estimating with different gas calculation"); - let gas = tx.gas.expect("gas is set by simulation."); - - // We are trying to show the user an estimation of the total gas usage. - // - // However, some transactions might depend on previous ones. For - // example, tx1 might deploy a contract that tx2 uses. That - // will result in the following `estimate_gas` call to fail, - // since tx1 hasn't been broadcasted yet. - // - // Not exiting here will not be a problem when actually broadcasting, because - // for chains where `has_different_gas_calc` returns true, - // we await each transaction before broadcasting the next - // one. - if let Err(err) = - estimate_gas(tx, &provider_info.provider, self.args.gas_estimate_multiplier) - .await - { - trace!("gas estimation failed: {err}"); - - // Restore gas value, since `estimate_gas` will remove it. - tx.set_gas_limit(gas); + // only estimate gas for unsigned transactions + if let Some(tx) = tx.as_unsigned_mut() { + trace!("estimating with different gas calculation"); + let gas = tx.gas.expect("gas is set by simulation."); + + // We are trying to show the user an estimation of the total gas usage. + // + // However, some transactions might depend on previous ones. For + // example, tx1 might deploy a contract that tx2 uses. That + // will result in the following `estimate_gas` call to fail, + // since tx1 hasn't been broadcasted yet. + // + // Not exiting here will not be a problem when actually broadcasting, + // because for chains where `has_different_gas_calc` + // returns true, we await each transaction before + // broadcasting the next one. + if let Err(err) = estimate_gas( + tx, + &provider_info.provider, + self.args.gas_estimate_multiplier, + ) + .await + { + trace!("gas estimation failed: {err}"); + + // Restore gas value, since `estimate_gas` will remove it. + tx.set_gas_limit(gas); + } } } let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(0); - *total_gas += tx.gas.expect("gas is set"); + *total_gas += tx.gas().expect("gas is set"); } new_sequence.push_back(tx); diff --git a/crates/script/src/transaction.rs b/crates/script/src/transaction.rs index de91ab3e3b0f..0ef2559d94a9 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -1,10 +1,8 @@ use super::ScriptResult; use alloy_dyn_abi::JsonAbiExt; use alloy_primitives::{hex, Address, Bytes, TxKind, B256}; -use alloy_rpc_types::request::TransactionRequest; -use alloy_serde::WithOtherFields; use eyre::{ContextCompat, Result, WrapErr}; -use foundry_common::{fmt::format_token_raw, ContractData, SELECTOR_LEN}; +use foundry_common::{fmt::format_token_raw, ContractData, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, traces::CallTraceDecoder}; use itertools::Itertools; use revm_inspectors::tracing::types::CallKind; @@ -20,7 +18,7 @@ pub struct AdditionalContract { pub init_code: Bytes, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionWithMetadata { pub hash: Option, @@ -36,7 +34,7 @@ pub struct TransactionWithMetadata { pub arguments: Option>, #[serde(skip)] pub rpc: String, - pub transaction: WithOtherFields, + pub transaction: TransactionMaybeSigned, pub additional_contracts: Vec, pub is_fixed_gas_limit: bool, } @@ -54,12 +52,23 @@ fn default_vec_of_strings() -> Option> { } impl TransactionWithMetadata { - pub fn from_tx_request(transaction: TransactionRequest) -> Self { - Self { transaction: WithOtherFields::new(transaction), ..Default::default() } + pub fn from_tx_request(transaction: TransactionMaybeSigned) -> Self { + Self { + transaction, + hash: Default::default(), + opcode: Default::default(), + contract_name: Default::default(), + contract_address: Default::default(), + function: Default::default(), + arguments: Default::default(), + is_fixed_gas_limit: Default::default(), + additional_contracts: Default::default(), + rpc: Default::default(), + } } pub fn new( - transaction: TransactionRequest, + transaction: TransactionMaybeSigned, rpc: String, result: &ScriptResult, local_contracts: &BTreeMap, @@ -72,7 +81,7 @@ impl TransactionWithMetadata { metadata.is_fixed_gas_limit = is_fixed_gas_limit; // Specify if any contract was directly created with this transaction - if let Some(TxKind::Call(to)) = metadata.transaction.to { + if let Some(TxKind::Call(to)) = metadata.transaction.to() { if to == DEFAULT_CREATE2_DEPLOYER { metadata.set_create( true, @@ -130,7 +139,7 @@ impl TransactionWithMetadata { self.contract_name = info.map(|info| info.name.clone()); self.contract_address = Some(address); - let Some(data) = self.transaction.input.input() else { return Ok(()) }; + let Some(data) = self.transaction.input() else { return Ok(()) }; let Some(info) = info else { return Ok(()) }; let Some(bytecode) = info.bytecode() else { return Ok(()) }; @@ -177,7 +186,7 @@ impl TransactionWithMetadata { self.opcode = CallKind::Call; self.contract_address = Some(target); - let Some(data) = self.transaction.input.input() else { return Ok(()) }; + let Some(data) = self.transaction.input() else { return Ok(()) }; if data.len() < SELECTOR_LEN { return Ok(()); } @@ -208,11 +217,11 @@ impl TransactionWithMetadata { Ok(()) } - pub fn tx(&self) -> &WithOtherFields { + pub fn tx(&self) -> &TransactionMaybeSigned { &self.transaction } - pub fn tx_mut(&mut self) -> &mut WithOtherFields { + pub fn tx_mut(&mut self) -> &mut TransactionMaybeSigned { &mut self.transaction } diff --git a/docs/dev/cheatcodes.md b/docs/dev/cheatcodes.md index ca4226be31f4..9ca0368d33c0 100644 --- a/docs/dev/cheatcodes.md +++ b/docs/dev/cheatcodes.md @@ -170,4 +170,4 @@ update of the files. [`cheatcodes/spec/src/vm.rs`]: ../../crates/cheatcodes/spec/src/vm.rs [`cheatcodes`]: ../../crates/cheatcodes/ [`spec::Cheatcodes::new`]: ../../crates/cheatcodes/spec/src/lib.rs#L74 -[`testdata/cheats/`]: ../../testdata/cheats/ +[`testdata/cheats/`]: ../../testdata/default/cheats/ diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 9c008c4254f4..dfe466d3aada 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -148,6 +148,7 @@ interface Vm { function blobhashes(bytes32[] calldata hashes) external; function breakpoint(string calldata char) external; function breakpoint(string calldata char, bool value) external; + function broadcastRawTransaction(bytes calldata data) external; function broadcast() external; function broadcast(address signer) external; function broadcast(uint256 privateKey) external; diff --git a/testdata/default/cheats/BroadcastRawTransaction.t.sol b/testdata/default/cheats/BroadcastRawTransaction.t.sol new file mode 100644 index 000000000000..7425e9d3773a --- /dev/null +++ b/testdata/default/cheats/BroadcastRawTransaction.t.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract BroadcastRawTransactionTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_revert_not_a_tx() public { + vm.expectRevert("broadcastRawTransaction: error decoding transaction (unexpected string)"); + vm.broadcastRawTransaction(hex"0102"); + } + + function test_revert_missing_signature() public { + vm.expectRevert("broadcastRawTransaction: error decoding transaction (input too short)"); + vm.broadcastRawTransaction(hex"dd806483030d40940993863c19b0defb183ca2b502db7d1b331ded757b80"); + } + + function test_revert_wrong_chainid() public { + vm.expectRevert("transaction validation error: invalid chain ID"); + vm.broadcastRawTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + } + + function test_execute_signed_tx() public { + vm.fee(1); + vm.chainId(1); + + address from = 0x5316812db67073C4d4af8BB3000C5B86c2877e94; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + + uint256 balance = 1 ether; + uint256 amountSent = 17; + + vm.deal(address(from), balance); + assertEq(address(from).balance, balance); + assertEq(address(to).balance, 0); + + /* + Signed transaction: + TransactionRequest { from: Some(0x5316812db67073c4d4af8bb3000c5b86c2877e94), to: Some(Address(0x6fd0a0cff9a87adf51695b40b4fa267855a8f4c6)), gas: Some(200000), gas_price: Some(100), value: Some(17), data: None, nonce: Some(0), chain_id: Some(1) } + */ + vm.broadcastRawTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + + uint256 gasPrice = 100; + assertEq(address(from).balance, balance - (gasPrice * 21_000) - amountSent); + assertEq(address(to).balance, amountSent); + } + + function test_execute_signed_tx2() public { + vm.fee(1); + vm.chainId(1); + + address from = 0x5316812db67073C4d4af8BB3000C5B86c2877e94; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + address random = address(uint160(uint256(keccak256(abi.encodePacked("random"))))); + + uint256 balance = 1 ether; + uint256 amountSent = 17; + + vm.deal(address(from), balance); + assertEq(address(from).balance, balance); + assertEq(address(to).balance, 0); + + /* + Signed transaction: + TransactionRequest { from: Some(0x5316812db67073c4d4af8bb3000c5b86c2877e94), to: Some(Address(0x6fd0a0cff9a87adf51695b40b4fa267855a8f4c6)), gas: Some(200000), gas_price: Some(100), value: Some(17), data: None, nonce: Some(0), chain_id: Some(1) } + */ + vm.broadcastRawTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + + uint256 gasPrice = 100; + assertEq(address(from).balance, balance - (gasPrice * 21_000) - amountSent); + assertEq(address(to).balance, amountSent); + assertEq(address(random).balance, 0); + + uint256 value = 5; + + vm.prank(to); + (bool success,) = random.call{value: value}(""); + require(success); + assertEq(address(to).balance, amountSent - value); + assertEq(address(random).balance, value); + } + + // this test is to make sure that the journaledstate is correctly handled + // i ran into an issue where the test would fail after running `broadcastRawTransaction` + // because there was an issue in the journaledstate + function test_execute_signed_tx_with_revert() public { + vm.fee(1); + vm.chainId(1); + + address from = 0x5316812db67073C4d4af8BB3000C5B86c2877e94; + address to = 0x6Fd0A0CFF9A87aDF51695b40b4fA267855a8F4c6; + + uint256 balance = 1 ether; + uint256 amountSent = 17; + + vm.deal(address(from), balance); + assertEq(address(from).balance, balance); + assertEq(address(to).balance, 0); + + /* + Signed transaction: + TransactionRequest { from: Some(0x5316812db67073c4d4af8bb3000c5b86c2877e94), to: Some(Address(0x6fd0a0cff9a87adf51695b40b4fa267855a8f4c6)), gas: Some(200000), gas_price: Some(100), value: Some(17), data: None, nonce: Some(0), chain_id: Some(1) } + */ + vm.broadcastRawTransaction( + hex"f860806483030d40946fd0a0cff9a87adf51695b40b4fa267855a8f4c6118025a03ebeabbcfe43c2c982e99b376b5fb6e765059d7f215533c8751218cac99bbd80a00a56cf5c382442466770a756e81272d06005c9e90fb8dbc5b53af499d5aca856" + ); + + uint256 gasPrice = 100; + assertEq(address(from).balance, balance - (gasPrice * 21_000) - amountSent); + assertEq(address(to).balance, amountSent); + + vm.expectRevert(); + assert(3 == 4); + } + + function test_execute_multiple_signed_tx() public { + vm.fee(1); + vm.chainId(1); + + address alice = 0x7ED31830602f9F7419307235c0610Fb262AA0375; + address bob = 0x70CF146aB98ffD5dE24e75dd7423F16181Da8E13; + address charlie = 0xae0900Cf97f8C233c64F7089cEC7d5457215BB8d; + + // this is the runtime code of "MyERC20" (see below) + // this is equivalent to: + // type(MyERC20).runtimeCode + bytes memory code = + hex"608060405234801561001057600080fd5b50600436106100625760003560e01c8063095ea7b31461006757806323b872dd1461008f57806370a08231146100a257806394bf804d146100d9578063a9059cbb146100ee578063dd62ed3e14610101575b600080fd5b61007a61007536600461051d565b61013a565b60405190151581526020015b60405180910390f35b61007a61009d366004610547565b610152565b6100cb6100b0366004610583565b6001600160a01b031660009081526020819052604090205490565b604051908152602001610086565b6100ec6100e73660046105a5565b610176565b005b61007a6100fc36600461051d565b610184565b6100cb61010f3660046105d1565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b600033610148818585610192565b5060019392505050565b600033610160858285610286565b61016b858585610318565b506001949350505050565b6101808183610489565b5050565b600033610148818585610318565b6001600160a01b0383166101f95760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b60648201526084015b60405180910390fd5b6001600160a01b03821661025a5760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b60648201526084016101f0565b6001600160a01b0392831660009081526001602090815260408083209490951682529290925291902055565b6001600160a01b03838116600090815260016020908152604080832093861683529290522054600019811461031257818110156103055760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e636500000060448201526064016101f0565b6103128484848403610192565b50505050565b6001600160a01b03831661037c5760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b60648201526084016101f0565b6001600160a01b0382166103de5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b60648201526084016101f0565b6001600160a01b038316600090815260208190526040902054818110156104565760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b60648201526084016101f0565b6001600160a01b039384166000908152602081905260408082209284900390925592909316825291902080549091019055565b6001600160a01b0382166104df5760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f20616464726573730060448201526064016101f0565b6001600160a01b03909116600090815260208190526040902080549091019055565b80356001600160a01b038116811461051857600080fd5b919050565b6000806040838503121561053057600080fd5b61053983610501565b946020939093013593505050565b60008060006060848603121561055c57600080fd5b61056584610501565b925061057360208501610501565b9150604084013590509250925092565b60006020828403121561059557600080fd5b61059e82610501565b9392505050565b600080604083850312156105b857600080fd5b823591506105c860208401610501565b90509250929050565b600080604083850312156105e457600080fd5b6105ed83610501565b91506105c86020840161050156fea2646970667358221220e1fee5cd1c5bbf066a9ce9228e1baf7e7fcb77b5050506c7d614aaf8608b42e364736f6c63430008110033"; + + // this is equivalent to: + // MyERC20 token = new MyERC20{ salt: bytes32(uint256(1)) }(); + // address: 0x5bf11839f61ef5cceeaf1f4153e44df5d02825f7 + MyERC20 token = MyERC20(address(uint160(uint256(keccak256(abi.encodePacked("mytoken")))))); + vm.etch(address(token), code); + + token.mint(100, alice); + + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(charlie), 0); + + vm.deal(alice, 10 ether); + + /* + Signed transaction: + { + from: '0x7ED31830602f9F7419307235c0610Fb262AA0375', + to: '0x5bF11839F61EF5ccEEaf1F4153e44df5D02825f7', + value: 0, + data: '0x095ea7b300000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e130000000000000000000000000000000000000000000000000000000000000032', + nonce: 0, + gasPrice: 100, + gasLimit: 200000, + chainId: 1 + } + */ + // this would be equivalent to using those cheatcodes: + // vm.prank(alice); + // token.approve(bob, 50); + vm.broadcastRawTransaction( + hex"f8a5806483030d40945bf11839f61ef5cceeaf1f4153e44df5d02825f780b844095ea7b300000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e13000000000000000000000000000000000000000000000000000000000000003225a0e25b9ef561d9a413b21755cc0e4bb6e80f2a88a8a52305690956130d612074dfa07bfd418bc2ad3c3f435fa531cdcdc64887f64ed3fb0d347d6b0086e320ad4eb1" + ); + + assertEq(token.allowance(alice, bob), 50); + + vm.deal(bob, 1 ether); + vm.prank(bob); + token.transferFrom(alice, charlie, 20); + + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(charlie), 20); + + vm.deal(charlie, 1 ether); + + /* + Signed transaction: + { + from: '0xae0900Cf97f8C233c64F7089cEC7d5457215BB8d', + to: '0x5bF11839F61EF5ccEEaf1F4153e44df5D02825f7', + value: 0, + data: '0xa9059cbb00000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e130000000000000000000000000000000000000000000000000000000000000005', + nonce: 0, + gasPrice: 100, + gasLimit: 200000, + chainId: 1 + } + */ + // this would be equivalent to using those cheatcodes: + // vm.prank(charlie); + // token.transfer(bob, 5); + vm.broadcastRawTransaction( + hex"f8a5806483030d40945bf11839f61ef5cceeaf1f4153e44df5d02825f780b844a9059cbb00000000000000000000000070cf146ab98ffd5de24e75dd7423f16181da8e13000000000000000000000000000000000000000000000000000000000000000525a0941562f519e33dfe5b44ebc2b799686cebeaeacd617dd89e393620b380797da2a0447dfd38d9444ccd571b000482c81674733761753430c81ee6669e9542c266a1" + ); + + assertEq(token.balanceOf(alice), 80); + assertEq(token.balanceOf(bob), 5); + assertEq(token.balanceOf(charlie), 15); + } +} + +contract MyERC20 { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + function mint(uint256 amount, address to) public { + _mint(to, amount); + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function transfer(address to, uint256 amount) public returns (bool) { + address owner = msg.sender; + _transfer(owner, to, amount); + return true; + } + + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public returns (bool) { + address owner = msg.sender; + _approve(owner, spender, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + address spender = msg.sender; + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + function _transfer(address from, address to, uint256 amount) internal { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + _balances[to] += amount; + } + } + + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + unchecked { + _balances[account] += amount; + } + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + _allowances[owner][spender] = amount; + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } +} + +contract ScriptBroadcastRawTransactionBroadcast is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function runSignedTxBroadcast() public { + uint256 pk_to = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + vm.startBroadcast(pk_to); + + address from = 0x73E1A965542AFA4B412467761b1CED8A764E1D3B; + address to = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address random = address(uint160(uint256(keccak256(abi.encodePacked("random"))))); + + assert(address(from).balance == 1 ether); + assert(address(to).balance == 1 ether); + assert(address(random).balance == 0); + + /* + TransactionRequest { + from: Some( + 0x73e1a965542afa4b412467761b1ced8a764e1d3b, + ), + to: Some( + Address( + 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266, + ), + ), + gas: Some( + 200000, + ), + gas_price: Some( + 10000000000, + ), + value: Some( + 1234, + ), + data: None, + nonce: Some( + 0, + ), + chain_id: Some( + 31337, + ), + } + */ + vm.broadcastRawTransaction( + hex"f869808502540be40083030d4094f39fd6e51aad88f6f4ce6ab8827279cfffb922668204d28082f4f6a061ce3c0f4280cb79c1eb0060a9a491cca1ba48ed32f141e3421ccb60c9dbe444a07fcd35cbec5f81427ac20f60484f4da9d00f59652f5053cd13ee90b992e94ab3" + ); + + uint256 value = 34; + (bool success,) = random.call{value: value}(""); + require(success); + + vm.stopBroadcast(); + + uint256 gasPrice = 10 * 1e9; + assert(address(from).balance == 1 ether - (gasPrice * 21_000) - 1234); + assert(address(to).balance == 1 ether + 1234 - value); + assert(address(random).balance == value); + } + + function runDeployCreate2Deployer() public { + vm.startBroadcast(); + vm.broadcastRawTransaction( + hex"f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222" + ); + vm.stopBroadcast(); + } +}