Skip to content

Commit 1f9b52c

Browse files
teddavklkvrzerosnacksmattsse
authored
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 <klkvrr@gmail.com> Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: zerosnacks <zerosnacks@protonmail.com> Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
1 parent e606f53 commit 1f9b52c

File tree

22 files changed

+737
-145
lines changed

22 files changed

+737
-145
lines changed

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ alloy-primitives.workspace = true
2828
alloy-genesis.workspace = true
2929
alloy-sol-types.workspace = true
3030
alloy-provider.workspace = true
31-
alloy-rpc-types.workspace = true
31+
alloy-rpc-types = { workspace = true, features = ["k256"] }
3232
alloy-signer.workspace = true
3333
alloy-signer-local = { workspace = true, features = [
3434
"mnemonic-all-languages",
3535
"keystore",
3636
] }
3737
parking_lot.workspace = true
38+
alloy-consensus = { workspace = true, features = ["k256"] }
39+
alloy-rlp.workspace = true
3840

3941
eyre.workspace = true
4042
itertools.workspace = true

crates/cheatcodes/assets/cheatcodes.json

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

+4
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,10 @@ interface Vm {
17591759
#[cheatcode(group = Scripting)]
17601760
function stopBroadcast() external;
17611761

1762+
/// Takes a signed transaction and broadcasts it to the network.
1763+
#[cheatcode(group = Scripting)]
1764+
function broadcastRawTransaction(bytes calldata data) external;
1765+
17621766
// ======== Utilities ========
17631767

17641768
// -------- Strings --------

crates/cheatcodes/src/evm.rs

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
//! Implementations of [`Evm`](spec::Group::Evm) cheatcodes.
22
3-
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*};
3+
use crate::{
4+
BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*,
5+
};
6+
use alloy_consensus::TxEnvelope;
47
use alloy_genesis::{Genesis, GenesisAccount};
58
use alloy_primitives::{Address, Bytes, B256, U256};
9+
use alloy_rlp::Decodable;
610
use alloy_sol_types::SolValue;
711
use foundry_common::fs::{read_json_file, write_json_file};
812
use foundry_evm_core::{
@@ -567,6 +571,35 @@ impl Cheatcode for stopAndReturnStateDiffCall {
567571
}
568572
}
569573

574+
impl Cheatcode for broadcastRawTransactionCall {
575+
fn apply_full<DB: DatabaseExt, E: CheatcodesExecutor>(
576+
&self,
577+
ccx: &mut CheatsCtxt<DB>,
578+
executor: &mut E,
579+
) -> Result {
580+
let mut data = self.data.as_ref();
581+
let tx = TxEnvelope::decode(&mut data).map_err(|err| {
582+
fmt_err!("broadcastRawTransaction: error decoding transaction ({err})")
583+
})?;
584+
585+
ccx.ecx.db.transact_from_tx(
586+
tx.clone().into(),
587+
&ccx.ecx.env,
588+
&mut ccx.ecx.journaled_state,
589+
&mut executor.get_inspector(ccx.state),
590+
)?;
591+
592+
if ccx.state.broadcast.is_some() {
593+
ccx.state.broadcastable_transactions.push_back(BroadcastableTransaction {
594+
rpc: ccx.db.active_fork_url(),
595+
transaction: tx.try_into()?,
596+
});
597+
}
598+
599+
Ok(Default::default())
600+
}
601+
}
602+
570603
impl Cheatcode for setBlockhashCall {
571604
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
572605
let Self { blockNumber, blockHash } = *self;

crates/cheatcodes/src/inspector.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
use alloy_primitives::{hex, Address, Bytes, Log, TxKind, B256, U256};
2020
use alloy_rpc_types::request::{TransactionInput, TransactionRequest};
2121
use alloy_sol_types::{SolCall, SolInterface, SolValue};
22-
use foundry_common::{evm::Breakpoints, SELECTOR_LEN};
22+
use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN};
2323
use foundry_config::Config;
2424
use foundry_evm_core::{
2525
abi::Vm::stopExpectSafeMemoryCall,
@@ -188,12 +188,12 @@ impl Context {
188188
}
189189

190190
/// Helps collecting transactions from different forks.
191-
#[derive(Clone, Debug, Default)]
191+
#[derive(Clone, Debug)]
192192
pub struct BroadcastableTransaction {
193193
/// The optional RPC URL.
194194
pub rpc: Option<String>,
195195
/// The transaction to broadcast.
196-
pub transaction: TransactionRequest,
196+
pub transaction: TransactionMaybeSigned,
197197
}
198198

199199
/// List of transactions that can be broadcasted.
@@ -513,7 +513,8 @@ impl Cheatcodes {
513513
None
514514
},
515515
..Default::default()
516-
},
516+
}
517+
.into(),
517518
});
518519

519520
input.log_debug(self, &input.scheme().unwrap_or(CreateScheme::Create));
@@ -849,7 +850,8 @@ impl Cheatcodes {
849850
None
850851
},
851852
..Default::default()
852-
},
853+
}
854+
.into(),
853855
});
854856
debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call");
855857

crates/common/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ alloy-transport-http = { workspace = true, features = [
4141
alloy-transport-ipc.workspace = true
4242
alloy-transport-ws.workspace = true
4343
alloy-transport.workspace = true
44+
alloy-consensus = { workspace = true, features = ["k256"] }
4445

4546
tower.workspace = true
4647

crates/common/src/transactions.rs

+88-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Wrappers for transactions.
22
3+
use alloy_consensus::{Transaction, TxEnvelope};
4+
use alloy_primitives::{Address, TxKind, U256};
35
use alloy_provider::{network::AnyNetwork, Provider};
4-
use alloy_rpc_types::{AnyTransactionReceipt, BlockId};
6+
use alloy_rpc_types::{AnyTransactionReceipt, BlockId, TransactionRequest};
57
use alloy_serde::WithOtherFields;
68
use alloy_transport::Transport;
79
use eyre::Result;
@@ -144,3 +146,88 @@ mod tests {
144146
assert_eq!(extract_revert_reason(error_string_2), None);
145147
}
146148
}
149+
150+
/// Used for broadcasting transactions
151+
/// A transaction can either be a [`TransactionRequest`] waiting to be signed
152+
/// or a [`TxEnvelope`], already signed
153+
#[derive(Clone, Debug, Serialize, Deserialize)]
154+
#[serde(untagged)]
155+
pub enum TransactionMaybeSigned {
156+
Signed {
157+
#[serde(flatten)]
158+
tx: TxEnvelope,
159+
from: Address,
160+
},
161+
Unsigned(WithOtherFields<TransactionRequest>),
162+
}
163+
164+
impl TransactionMaybeSigned {
165+
/// Creates a new (unsigned) transaction for broadcast
166+
pub fn new(tx: WithOtherFields<TransactionRequest>) -> Self {
167+
Self::Unsigned(tx)
168+
}
169+
170+
/// Creates a new signed transaction for broadcast.
171+
pub fn new_signed(
172+
tx: TxEnvelope,
173+
) -> core::result::Result<Self, alloy_primitives::SignatureError> {
174+
let from = tx.recover_signer()?;
175+
Ok(Self::Signed { tx, from })
176+
}
177+
178+
pub fn as_unsigned_mut(&mut self) -> Option<&mut WithOtherFields<TransactionRequest>> {
179+
match self {
180+
Self::Unsigned(tx) => Some(tx),
181+
_ => None,
182+
}
183+
}
184+
185+
pub fn from(&self) -> Option<Address> {
186+
match self {
187+
Self::Signed { from, .. } => Some(*from),
188+
Self::Unsigned(tx) => tx.from,
189+
}
190+
}
191+
192+
pub fn input(&self) -> Option<&[u8]> {
193+
match self {
194+
Self::Signed { tx, .. } => Some(tx.input()),
195+
Self::Unsigned(tx) => tx.input.input().map(|i| i.as_ref()),
196+
}
197+
}
198+
199+
pub fn to(&self) -> Option<TxKind> {
200+
match self {
201+
Self::Signed { tx, .. } => Some(tx.to()),
202+
Self::Unsigned(tx) => tx.to,
203+
}
204+
}
205+
206+
pub fn value(&self) -> Option<U256> {
207+
match self {
208+
Self::Signed { tx, .. } => Some(tx.value()),
209+
Self::Unsigned(tx) => tx.value,
210+
}
211+
}
212+
213+
pub fn gas(&self) -> Option<u128> {
214+
match self {
215+
Self::Signed { tx, .. } => Some(tx.gas_limit()),
216+
Self::Unsigned(tx) => tx.gas,
217+
}
218+
}
219+
}
220+
221+
impl From<TransactionRequest> for TransactionMaybeSigned {
222+
fn from(tx: TransactionRequest) -> Self {
223+
Self::new(WithOtherFields::new(tx))
224+
}
225+
}
226+
227+
impl TryFrom<TxEnvelope> for TransactionMaybeSigned {
228+
type Error = alloy_primitives::SignatureError;
229+
230+
fn try_from(tx: TxEnvelope) -> core::result::Result<Self, Self::Error> {
231+
Self::new_signed(tx)
232+
}
233+
}

crates/evm/core/src/backend/cow.rs

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
};
1111
use alloy_genesis::GenesisAccount;
1212
use alloy_primitives::{Address, B256, U256};
13+
use alloy_rpc_types::TransactionRequest;
1314
use eyre::WrapErr;
1415
use foundry_fork_db::DatabaseError;
1516
use revm::{
@@ -190,6 +191,16 @@ impl<'a> DatabaseExt for CowBackend<'a> {
190191
self.backend_mut(env).transact(id, transaction, env, journaled_state, inspector)
191192
}
192193

194+
fn transact_from_tx(
195+
&mut self,
196+
transaction: TransactionRequest,
197+
env: &Env,
198+
journaled_state: &mut JournaledState,
199+
inspector: &mut dyn InspectorExt<Backend>,
200+
) -> eyre::Result<()> {
201+
self.backend_mut(env).transact_from_tx(transaction, env, journaled_state, inspector)
202+
}
203+
193204
fn active_fork_id(&self) -> Option<LocalForkId> {
194205
self.backend.active_fork_id()
195206
}

0 commit comments

Comments
 (0)