From c2e7007628c5ebb9d9420b75085daa96d0cd4ad9 Mon Sep 17 00:00:00 2001 From: teddav Date: Wed, 29 May 2024 12:52:47 +0200 Subject: [PATCH] feat: sendRawTransaction cheatcode --- .gitignore | 2 + Cargo.lock | 46 ++++--------- Cargo.toml | 30 ++++++++- crates/cheatcodes/Cargo.toml | 4 +- crates/cheatcodes/assets/cheatcodes.json | 20 ++++++ crates/cheatcodes/spec/src/vm.rs | 4 ++ crates/cheatcodes/src/evm.rs | 26 +++++++- crates/cheatcodes/src/inspector.rs | 12 ++-- crates/common/src/transactions.rs | 59 ++++++++++++++++- crates/evm/core/src/backend/cow.rs | 11 ++++ crates/evm/core/src/backend/mod.rs | 78 ++++++++++++++++++++-- crates/evm/core/src/utils.rs | 2 +- crates/script/Cargo.toml | 2 + crates/script/src/broadcast.rs | 83 ++++++++++++++++-------- crates/script/src/execute.rs | 6 +- crates/script/src/sequence.rs | 6 +- crates/script/src/transaction.rs | 14 ++-- testdata/cheats/Vm.sol | 1 + 18 files changed, 318 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index 14e811f2d511..77ed6b11956e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ out.json .idea .vscode bloat* +/test_implem/ +/test_alloy_rcp/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 90effef319a8..f1e4c75f8c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,6 @@ dependencies = [ [[package]] name = "alloy-consensus" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -94,7 +93,6 @@ dependencies = [ [[package]] name = "alloy-contract" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -134,7 +132,6 @@ dependencies = [ [[package]] name = "alloy-eips" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -148,7 +145,6 @@ dependencies = [ [[package]] name = "alloy-genesis" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-primitives", "alloy-serde", @@ -170,7 +166,6 @@ dependencies = [ [[package]] name = "alloy-json-rpc" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-primitives", "serde", @@ -182,7 +177,6 @@ dependencies = [ [[package]] name = "alloy-network" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -198,9 +192,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c715249705afa1e32be79dabfd35e2ef0f1cc02ad2cf48c9d1e20026ee637b" +checksum = "db8aa973e647ec336810a9356af8aea787249c9d00b1525359f3db29a68d231b" dependencies = [ "alloy-rlp", "arbitrary", @@ -226,7 +220,6 @@ dependencies = [ [[package]] name = "alloy-provider" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-eips", "alloy-json-rpc", @@ -257,7 +250,6 @@ dependencies = [ [[package]] name = "alloy-pubsub" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -297,7 +289,6 @@ dependencies = [ [[package]] name = "alloy-rpc-client" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -321,7 +312,6 @@ dependencies = [ [[package]] name = "alloy-rpc-types" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -339,7 +329,6 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-eips", @@ -356,7 +345,6 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-primitives", "alloy-rpc-types", @@ -368,7 +356,6 @@ dependencies = [ [[package]] name = "alloy-serde" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-primitives", "serde", @@ -378,7 +365,6 @@ dependencies = [ [[package]] name = "alloy-signer" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -393,7 +379,6 @@ dependencies = [ [[package]] name = "alloy-signer-aws" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-network", @@ -410,7 +395,6 @@ dependencies = [ [[package]] name = "alloy-signer-ledger" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-network", @@ -428,7 +412,6 @@ dependencies = [ [[package]] name = "alloy-signer-trezor" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-network", @@ -444,7 +427,6 @@ dependencies = [ [[package]] name = "alloy-signer-wallet" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-consensus", "alloy-network", @@ -521,7 +503,6 @@ dependencies = [ [[package]] name = "alloy-transport" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-json-rpc", "base64 0.22.0", @@ -539,7 +520,6 @@ dependencies = [ [[package]] name = "alloy-transport-http" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -552,7 +532,6 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -572,7 +551,6 @@ dependencies = [ [[package]] name = "alloy-transport-ws" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=8808d21#8808d21677ed9a05ff04000ac7f4acdd2fde94e3" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1741,9 +1719,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3130f3d8717cc02e668a896af24984d5d5d4e8bf12e278e982e0f1bd88a0f9af" +checksum = "cdf100c4cea8f207e883ff91ca886d621d8a166cb04971dfaa9bb8fd99ed95df" dependencies = [ "blst", "cc", @@ -3588,12 +3566,14 @@ name = "forge-script" version = "0.2.0" dependencies = [ "alloy-chains", + "alloy-consensus", "alloy-dyn-abi", "alloy-eips", "alloy-json-abi", "alloy-network", "alloy-primitives", "alloy-provider", + "alloy-rlp", "alloy-rpc-types", "alloy-signer", "alloy-transport", @@ -3690,11 +3670,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-wallet", @@ -7017,8 +6999,6 @@ dependencies = [ [[package]] name = "revm" version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a454c1c650b2b2e23f0c461af09e6c31e1d15e1cbebe905a701c46b8a50afc" dependencies = [ "auto_impl", "cfg-if", @@ -7049,8 +7029,6 @@ dependencies = [ [[package]] name = "revm-interpreter" version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d322f2730cd300e99d271a1704a2dfb8973d832428f5aa282aaa40e2473b5eec" dependencies = [ "revm-primitives", "serde", @@ -7059,8 +7037,6 @@ dependencies = [ [[package]] name = "revm-precompile" version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931f692f3f4fc72ec39d5d270f8e9d208c4a6008de7590ee96cf948e3b6d3f8d" dependencies = [ "aurora-engine-modexp", "c-kzg", @@ -7076,8 +7052,6 @@ dependencies = [ [[package]] name = "revm-primitives" version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbbc9640790cebcb731289afb7a7d96d16ad94afeb64b5d0b66443bd151e79d6" dependencies = [ "alloy-primitives", "auto_impl", @@ -9590,3 +9564,7 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "alloy-node-bindings" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 550a0e644e4d..b26db4379673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -224,4 +224,32 @@ axum = "0.7" hyper = "1.0" reqwest = { version = "0.12", default-features = false } tower = "0.4" -tower-http = "0.5" \ No newline at end of file +tower-http = "0.5" + +[patch.'https://github.com/alloy-rs/alloy'] +alloy-consensus = { path = "../alloy/crates/consensus" } +alloy-contract = { path = "../alloy/crates/contract" } +alloy-eips = { path = "../alloy/crates/eips" } +alloy-genesis = { path = "../alloy/crates/genesis" } +alloy-json-rpc = { path = "../alloy/crates/json-rpc" } +alloy-network = { path = "../alloy/crates/network" } +alloy-node-bindings = { path = "../alloy/crates/node-bindings" } +alloy-provider = { path = "../alloy/crates/provider" } +alloy-pubsub = { path = "../alloy/crates/pubsub" } +alloy-rpc-client = { path = "../alloy/crates/rpc-client" } +alloy-rpc-types-engine = { path = "../alloy/crates/rpc-types-engine" } +alloy-rpc-types-trace = { path = "../alloy/crates/rpc-types-trace" } +alloy-rpc-types = { path = "../alloy/crates/rpc-types" } +alloy-signer = { path = "../alloy/crates/signer" } +alloy-signer-wallet = { path = "../alloy/crates/signer-wallet" } +alloy-signer-aws = { path = "../alloy/crates/signer-aws" } +alloy-signer-ledger = { path = "../alloy/crates/signer-ledger" } +alloy-signer-trezor = { path = "../alloy/crates/signer-trezor" } +alloy-transport = { path = "../alloy/crates/transport" } +alloy-transport-http = { path = "../alloy/crates/transport-http" } +alloy-transport-ipc = { path = "../alloy/crates/transport-ipc" } +alloy-transport-ws = { path = "../alloy/crates/transport-ws" } + +[patch.crates-io] +revm = { path = "../revm/crates/revm" } +revm-primitives = { path = "../revm/crates/primitives" } diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index a4e9a0ad8574..d540f3ec0efa 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -25,12 +25,14 @@ 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-wallet = { workspace = true, features = [ "mnemonic-all-languages", "keystore", ] } +alloy-consensus = { workspace = true, features = ["k256"] } +alloy-rlp.workspace = true parking_lot = "0.12" eyre.workspace = true diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 39b58ff5e7fe..91e226a5b8ff 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -6971,6 +6971,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "sendRawTransaction", + "description": "takes a signed transaction as `bytes` and executes it", + "declaration": "function sendRawTransaction(bytes calldata data) external;", + "visibility": "external", + "mutability": "", + "signature": "sendRawTransaction(bytes)", + "selector": "0xbc03d9f7", + "selectorBytes": [ + 188, + 3, + 217, + 247 + ] + }, + "group": "evm", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "serializeAddress_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 4bbf63169020..2b06eea84361 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -677,6 +677,10 @@ interface Vm { #[cheatcode(group = Evm, safety = Safe)] function lastCallGas() external view returns (Gas memory gas); + /// takes a signed transaction as bytes and executes it + #[cheatcode(group = Evm, safety = Safe)] + function sendRawTransaction(bytes calldata data) external; + // ======== Test Assertions and Utilities ======== /// If the condition is false, discard this run's fuzz inputs and generate new ones. diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 6189980716b5..846d91697829 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -1,8 +1,10 @@ //! Implementations of [`Evm`](crate::Group::Evm) cheatcodes. -use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; +use crate::{BroadcastableTransaction, Cheatcode, Cheatcodes, 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::{ @@ -552,6 +554,28 @@ impl Cheatcode for stopAndReturnStateDiffCall { } } +impl Cheatcode for sendRawTransactionCall { + fn apply_full(&self, ccx: &mut CheatsCtxt) -> Result { + let mut data = self.data.as_ref(); + let tx = TxEnvelope::decode(&mut data).map_err(|err| fmt_err!("{err}"))?; + + if ccx.state.broadcast.is_some() { + ccx.state.broadcastable_transactions.push_back(BroadcastableTransaction { + rpc: ccx.db.active_fork_url(), + transaction: tx.clone().into(), + }); + } + + ccx.ecx.db.transact_from_tx( + tx.into(), + &ccx.ecx.env, + &mut ccx.ecx.journaled_state, + ccx.state, + )?; + Ok(Default::default()) + } +} + pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result { super::script::correct_sender_nonce(ccx)?; let (account, _) = ccx.ecx.journaled_state.load_account(*address, &mut ccx.ecx.db)?; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index b66d262e21dc..bf2c49fccaea 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -18,7 +18,7 @@ use crate::{ use alloy_primitives::{Address, Bytes, Log, TxKind, B256, U256}; use alloy_rpc_types::request::{TransactionInput, TransactionRequest}; use alloy_sol_types::{SolInterface, SolValue}; -use foundry_common::{evm::Breakpoints, SELECTOR_LEN}; +use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN}; use foundry_evm_core::{ abi::Vm::stopExpectSafeMemoryCall, backend::{DatabaseExt, RevertDiagnostic}, @@ -82,7 +82,7 @@ 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. @@ -913,7 +913,7 @@ impl Inspector for Cheatcodes { self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ecx.db.active_fork_url(), - transaction: TransactionRequest { + transaction: TransactionMaybeSigned::new(TransactionRequest { from: Some(broadcast.new_origin), to: Some(TxKind::from(Some(call.contract))), value: Some(call.transfer.value), @@ -925,7 +925,7 @@ impl Inspector for Cheatcodes { None }, ..Default::default() - }, + }), }); debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); @@ -1335,7 +1335,7 @@ impl Inspector for Cheatcodes { self.broadcastable_transactions.push_back(BroadcastableTransaction { rpc: ecx.db.active_fork_url(), - transaction: TransactionRequest { + transaction: TransactionMaybeSigned::new(TransactionRequest { from: Some(broadcast.new_origin), to: None, value: Some(call.value), @@ -1347,7 +1347,7 @@ impl Inspector for Cheatcodes { None }, ..Default::default() - }, + }), }); let kind = match call.scheme { diff --git a/crates/common/src/transactions.rs b/crates/common/src/transactions.rs index 1f7d6228eef3..91df1f32118a 100644 --- a/crates/common/src/transactions.rs +++ b/crates/common/src/transactions.rs @@ -1,6 +1,9 @@ //! wrappers for transactions +use std::ops::{Deref, DerefMut}; + +use alloy_consensus::TxEnvelope; use alloy_provider::{network::AnyNetwork, Provider}; -use alloy_rpc_types::{AnyTransactionReceipt, BlockId, WithOtherFields}; +use alloy_rpc_types::{AnyTransactionReceipt, BlockId, TransactionRequest, WithOtherFields}; use alloy_transport::Transport; use eyre::Result; use serde::{Deserialize, Serialize}; @@ -95,3 +98,57 @@ 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, Default, Serialize, Deserialize)] +pub struct TransactionMaybeSigned { + /// if tx is already signed, this is equal to `signed_tx` stripped of its signature + pub tx: TransactionRequest, + /// the signed transaction + pub signed_tx: Option, +} + +impl TransactionMaybeSigned { + /// Creates a new (unsigned) transaction for broadcast + pub fn new(tx: TransactionRequest) -> Self { + Self { tx, signed_tx: None } + } + + /// Creates a new signed transaction for broadcast + pub fn new_signed(tx: TxEnvelope) -> Self { + Self { tx: tx.clone().into(), signed_tx: Some(tx) } + } +} + +impl From for TransactionMaybeSigned { + fn from(tx: TransactionRequest) -> Self { + Self::new(tx) + } +} + +impl From for TransactionMaybeSigned { + fn from(envelope: TxEnvelope) -> Self { + Self { tx: envelope.clone().into(), signed_tx: Some(envelope) } + } +} + +impl Into for TransactionMaybeSigned { + fn into(self) -> TransactionRequest { + self.tx + } +} + +impl Deref for TransactionMaybeSigned { + type Target = TransactionRequest; + fn deref(&self) -> &Self::Target { + &self.tx + } +} + +impl DerefMut for TransactionMaybeSigned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.tx + } +} diff --git a/crates/evm/core/src/backend/cow.rs b/crates/evm/core/src/backend/cow.rs index 9b53fb4ee29a..a90bece4070a 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 revm::{ db::DatabaseRef, @@ -188,6 +189,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 I, + ) -> 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 90e27444b222..bf1d8cf6bcd9 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, SharedBackend}, snapshot::Snapshots, - utils::configure_tx_env, + utils::{configure_tx_env, new_evm_with_inspector}, InspectorExt, }; use alloy_genesis::GenesisAccount; -use alloy_primitives::{b256, keccak256, Address, B256, U256}; -use alloy_rpc_types::{Block, BlockNumberOrTag, BlockTransactions, Transaction, WithOtherFields}; +use alloy_primitives::{b256, keccak256, Address, TxKind, B256, U256}; +use alloy_rpc_types::{ + Block, BlockNumberOrTag, BlockTransactions, Transaction, TransactionRequest, WithOtherFields, +}; use eyre::Context; use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE}; use revm::{ @@ -188,7 +190,7 @@ pub trait DatabaseExt: Database { journaled_state: &mut JournaledState, ) -> eyre::Result<()>; - /// Fetches the given transaction for the fork and executes it, committing the state in the DB + /// Fetches the given transaction from the fork and executes it, committing the state in the DB fn transact>( &mut self, id: Option, @@ -200,6 +202,17 @@ pub trait DatabaseExt: Database { where Self: Sized; + /// 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 I, + ) -> eyre::Result<()> + where + Self: Sized; + /// Returns the `ForkId` that's currently used in the database, if fork mode is on fn active_fork_id(&self) -> Option; @@ -1258,6 +1271,63 @@ impl DatabaseExt for Backend { commit_transaction(tx, env, journaled_state, fork, &fork_id, inspector) } + fn transact_from_tx>( + &mut self, + tx: TransactionRequest, + env: &Env, + journaled_state: &mut JournaledState, + inspector: &mut I, + ) -> 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.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() + .map(|item| { + ( + item.address, + item.storage_keys + .into_iter() + .map(|key| alloy_primitives::U256::from_be_bytes(key.0)) + .collect(), + ) + }) + .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 = match tx.to { + Some(TxKind::Call(a)) => TransactTo::Call(a), + Some(TxKind::Create) => TransactTo::create(), + None => TransactTo::create(), + }; + + self.commit(journaled_state.state.clone()); + + let res = { + let db = self.clone(); + let env = self.env_with_handler_cfg(env.clone()); + new_evm_with_inspector(db, env, inspector).transact()? + }; + + self.commit(res.state); + update_state(&mut journaled_state.state, self)?; + + Ok(()) + } + fn active_fork_id(&self) -> Option { self.active_fork_ids.map(|(id, _)| id) } diff --git a/crates/evm/core/src/utils.rs b/crates/evm/core/src/utils.rs index 20d8aa647864..84bb72729283 100644 --- a/crates/evm/core/src/utils.rs +++ b/crates/evm/core/src/utils.rs @@ -96,7 +96,7 @@ pub fn configure_tx_env(env: &mut revm::primitives::Env, tx: &Transaction) { .collect(); env.tx.value = tx.value.to(); env.tx.data = alloy_primitives::Bytes(tx.input.0.clone()); - env.tx.transact_to = tx.to.map(TransactTo::Call).unwrap_or_else(TransactTo::create) + env.tx.transact_to = tx.to.map(TransactTo::Call).unwrap_or_else(TransactTo::create); } /// Get the gas used, accounting for refunds diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 3d41ac795478..40c2206ac822 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -50,6 +50,8 @@ alloy-dyn-abi.workspace = true alloy-primitives.workspace = true alloy-eips.workspace = true alloy-transport.workspace = true +alloy-consensus.workspace = true +alloy-rlp.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/script/src/broadcast.rs b/crates/script/src/broadcast.rs index a278e44d7591..584b3f1b07db 100644 --- a/crates/script/src/broadcast.rs +++ b/crates/script/src/broadcast.rs @@ -4,11 +4,12 @@ use crate::{ ScriptConfig, }; use alloy_chains::Chain; +use alloy_consensus::TxEnvelope; use alloy_eips::eip2718::Encodable2718; use alloy_network::{AnyNetwork, EthereumSigner, TransactionBuilder}; use alloy_primitives::{utils::format_units, Address, TxHash}; use alloy_provider::{utils::Eip1559Estimation, Provider}; -use alloy_rpc_types::{BlockId, TransactionRequest, WithOtherFields}; +use alloy_rpc_types::{BlockId, IntoTransactionRequest, WithOtherFields}; use alloy_transport::Transport; use eyre::{bail, Context, Result}; use forge_verify::provider::VerificationProviderType; @@ -19,7 +20,7 @@ use foundry_cli::{ }; 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}; @@ -30,7 +31,7 @@ use std::{ }; pub async fn estimate_gas( - tx: &mut WithOtherFields, + tx: &mut WithOtherFields, provider: &P, estimate_multiplier: u64, ) -> Result<()> @@ -42,9 +43,10 @@ where // set in the request and omit the estimate altogether, so we remove it here tx.gas = None; + let t = tx.clone().into_transaction_request(); tx.set_gas_limit( provider - .estimate_gas(tx, BlockId::latest()) + .estimate_gas(&t, BlockId::latest()) .await .wrap_err("Failed to estimate gas for tx")? * estimate_multiplier as u128 / @@ -61,7 +63,7 @@ pub async fn next_nonce(caller: Address, provider_url: &str) -> eyre::Result, - mut tx: WithOtherFields, + mut tx: WithOtherFields, kind: SendTransactionKind<'_>, sequential_broadcast: bool, is_fixed_gas_limit: bool, @@ -90,16 +92,20 @@ pub async fn send_transaction( debug!("sending transaction from unlocked account {:?}: {:?}", addr, tx); // Submit the transaction - provider.send_transaction(tx).await? + provider.send_transaction(tx.into_transaction_request()).await? } SendTransactionKind::Raw(signer) => { debug!("sending transaction: {:?}", tx); - let signed = tx.build(signer).await?; + let signed = tx.into_transaction_request().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()) @@ -110,6 +116,7 @@ pub async fn send_transaction( pub enum SendTransactionKind<'a> { Unlocked(Address), Raw(&'a EthereumSigner), + Signed(TxEnvelope), } /// Represents how to send _all_ transactions @@ -117,7 +124,7 @@ pub enum SendTransactionsKind { /// Send via `eth_sendTransaction` and rely on the `from` address being unlocked. Unlocked(HashSet
), /// Send a signed transaction via `eth_sendRawTransaction` - Raw(HashMap), + Raw(HashMap, Vec), } impl SendTransactionsKind { @@ -132,10 +139,17 @@ impl SendTransactionsKind { } Ok(SendTransactionKind::Unlocked(*addr)) } - SendTransactionsKind::Raw(wallets) => { + SendTransactionsKind::Raw(wallets, signed_txs) => { if let Some(wallet) = wallets.get(addr) { Ok(SendTransactionKind::Raw(wallet)) } else { + for tx in signed_txs { + let sender = tx.recover_signer()?; + if sender == *addr { + return Ok(SendTransactionKind::Signed(tx.clone())) + } + } + bail!("No matching signer for {:?} found", addr) } } @@ -143,10 +157,19 @@ impl SendTransactionsKind { } /// How many signers are set - pub fn signers_count(&self) -> usize { + pub fn signers_count(&self) -> Result { match self { - SendTransactionsKind::Unlocked(addr) => addr.len(), - SendTransactionsKind::Raw(signers) => signers.len(), + SendTransactionsKind::Unlocked(addr) => Ok(addr.len()), + SendTransactionsKind::Raw(signers, signed_txs) => { + let mut len = signers.len(); + for tx in signed_txs { + let from = tx.recover_signer()?; + if signers.get(&from).is_none() { + len += 1; + } + } + Ok(len) + } } } } @@ -187,16 +210,24 @@ impl BundledState { /// Broadcasts transactions from all sequences. pub async fn broadcast(mut self) -> Result { - let required_addresses = self - .sequence - .sequences() - .iter() - .flat_map(|sequence| { - sequence - .transactions() - .map(|tx| (tx.from().expect("No sender for onchain transaction!"))) - }) - .collect::>(); + let (required_addresses, signed_txs): (Vec>, Vec>) = + self.sequence + .sequences() + .iter() + .flat_map(|sequence| { + sequence.transactions().map(|tx| { + if tx.signed_tx.is_some() { + (None, tx.signed_tx.clone()) + } else { + (Some(tx.from.expect("No sender for onchain transaction!")), None) + } + }) + }) + .unzip(); + + let required_addresses = + required_addresses.into_iter().filter_map(|a| a).collect::>(); + let signed_txs = signed_txs.into_iter().filter_map(|a| a).collect::>(); if required_addresses.contains(&Config::DEFAULT_SENDER) { eyre::bail!( @@ -229,7 +260,7 @@ impl BundledState { .map(|(addr, signer)| (addr, EthereumSigner::new(signer))) .collect(); - SendTransactionsKind::Raw(signers) + SendTransactionsKind::Raw(signers, signed_txs) }; for i in 0..self.sequence.sequences().len() { @@ -278,7 +309,7 @@ impl BundledState { .skip(already_broadcasted) .map(|tx_with_metadata| { let tx = tx_with_metadata.tx(); - let from = tx.from().expect("No sender for onchain transaction!"); + 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; @@ -287,7 +318,7 @@ impl BundledState { tx.set_chain_id(sequence.chain); // Set TxKind::Create explicityly to satify `check_reqd_fields` in alloy - if tx.to().is_none() { + if tx.to.is_none() { tx.set_create(); } @@ -313,7 +344,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 || + send_kind.signers_count()? != 1 || !has_batch_support(sequence.chain); let pb = init_progress!(transactions, "txes"); diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index ea307de5bd04..7c626fbbd90b 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -17,7 +17,7 @@ use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; use foundry_common::{ fmt::{format_token, format_token_raw}, provider::get_http_provider, - shell, ContractData, ContractsByArtifact, + shell, ContractData, ContractsByArtifact, TransactionMaybeSigned, }; use foundry_config::{Config, NamedChain}; use foundry_debugger::Debugger; @@ -129,12 +129,12 @@ impl PreExecutionState { .enumerate() .map(|(i, bytes)| BroadcastableTransaction { rpc: self.script_config.evm_opts.fork_url.clone(), - transaction: TransactionRequest { + transaction: TransactionMaybeSigned::new(TransactionRequest { from: Some(self.script_config.evm_opts.sender), input: Some(bytes.clone()).into(), nonce: Some(self.script_config.sender_nonce + i as u64), ..Default::default() - }, + }), }) .chain(txs) .collect(), diff --git a/crates/script/src/sequence.rs b/crates/script/src/sequence.rs index f98326564ffb..8e95b89cf351 100644 --- a/crates/script/src/sequence.rs +++ b/crates/script/src/sequence.rs @@ -4,11 +4,11 @@ use crate::{ verify::VerifyBundle, }; use alloy_primitives::{Address, TxHash}; -use alloy_rpc_types::{AnyTransactionReceipt, TransactionRequest, WithOtherFields}; +use alloy_rpc_types::{AnyTransactionReceipt, WithOtherFields}; use 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}; @@ -353,7 +353,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/transaction.rs b/crates/script/src/transaction.rs index c46301f9f398..81b615598544 100644 --- a/crates/script/src/transaction.rs +++ b/crates/script/src/transaction.rs @@ -1,9 +1,9 @@ use super::ScriptResult; use alloy_dyn_abi::JsonAbiExt; use alloy_primitives::{Address, Bytes, TxKind, B256}; -use alloy_rpc_types::{request::TransactionRequest, WithOtherFields}; +use alloy_rpc_types::{TransactionRequest, 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; @@ -35,7 +35,7 @@ pub struct TransactionWithMetadata { pub arguments: Option>, #[serde(skip)] pub rpc: String, - pub transaction: WithOtherFields, + pub transaction: WithOtherFields, pub additional_contracts: Vec, pub is_fixed_gas_limit: bool, } @@ -53,12 +53,12 @@ fn default_vec_of_strings() -> Option> { } impl TransactionWithMetadata { - pub fn from_tx_request(transaction: TransactionRequest) -> Self { + pub fn from_tx_request(transaction: TransactionMaybeSigned) -> Self { Self { transaction: WithOtherFields::new(transaction), ..Default::default() } } pub fn new( - transaction: TransactionRequest, + transaction: TransactionMaybeSigned, rpc: String, result: &ScriptResult, local_contracts: &BTreeMap, @@ -209,11 +209,11 @@ impl TransactionWithMetadata { Ok(()) } - pub fn tx(&self) -> &WithOtherFields { + pub fn tx(&self) -> &WithOtherFields { &self.transaction } - pub fn tx_mut(&mut self) -> &mut WithOtherFields { + pub fn tx_mut(&mut self) -> &mut WithOtherFields { &mut self.transaction } diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index db8e45ada043..7cb2f8fe5121 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -344,6 +344,7 @@ interface Vm { function rpcUrlStructs() external view returns (Rpc[] memory urls); function rpcUrls() external view returns (string[2][] memory urls); function selectFork(uint256 forkId) external; + function sendRawTransaction(bytes calldata data) external; function serializeAddress(string calldata objectKey, string calldata valueKey, address value) external returns (string memory json); function serializeAddress(string calldata objectKey, string calldata valueKey, address[] calldata values) external returns (string memory json); function serializeBool(string calldata objectKey, string calldata valueKey, bool value) external returns (string memory json);