Skip to content

Commit

Permalink
feat: sendRawTransaction cheatcode (#4931)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
5 people committed Jul 26, 2024
1 parent e606f53 commit 1f9b52c
Show file tree
Hide file tree
Showing 22 changed files with 737 additions and 145 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 --------
Expand Down
35 changes: 34 additions & 1 deletion crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -567,6 +571,35 @@ impl Cheatcode for stopAndReturnStateDiffCall {
}
}

impl Cheatcode for broadcastRawTransactionCall {
fn apply_full<DB: DatabaseExt, E: CheatcodesExecutor>(
&self,
ccx: &mut CheatsCtxt<DB>,
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<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { blockNumber, blockHash } = *self;
Expand Down
12 changes: 7 additions & 5 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<String>,
/// The transaction to broadcast.
pub transaction: TransactionRequest,
pub transaction: TransactionMaybeSigned,
}

/// List of transactions that can be broadcasted.
Expand Down Expand Up @@ -513,7 +513,8 @@ impl Cheatcodes {
None
},
..Default::default()
},
}
.into(),
});

input.log_debug(self, &input.scheme().unwrap_or(CreateScheme::Create));
Expand Down Expand Up @@ -849,7 +850,8 @@ impl Cheatcodes {
None
},
..Default::default()
},
}
.into(),
});
debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call");

Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
89 changes: 88 additions & 1 deletion crates/common/src/transactions.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<TransactionRequest>),
}

impl TransactionMaybeSigned {
/// Creates a new (unsigned) transaction for broadcast
pub fn new(tx: WithOtherFields<TransactionRequest>) -> Self {
Self::Unsigned(tx)
}

/// Creates a new signed transaction for broadcast.
pub fn new_signed(
tx: TxEnvelope,
) -> core::result::Result<Self, alloy_primitives::SignatureError> {
let from = tx.recover_signer()?;
Ok(Self::Signed { tx, from })
}

pub fn as_unsigned_mut(&mut self) -> Option<&mut WithOtherFields<TransactionRequest>> {
match self {
Self::Unsigned(tx) => Some(tx),
_ => None,
}
}

pub fn from(&self) -> Option<Address> {
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<TxKind> {
match self {
Self::Signed { tx, .. } => Some(tx.to()),
Self::Unsigned(tx) => tx.to,
}
}

pub fn value(&self) -> Option<U256> {
match self {
Self::Signed { tx, .. } => Some(tx.value()),
Self::Unsigned(tx) => tx.value,
}
}

pub fn gas(&self) -> Option<u128> {
match self {
Self::Signed { tx, .. } => Some(tx.gas_limit()),
Self::Unsigned(tx) => tx.gas,
}
}
}

impl From<TransactionRequest> for TransactionMaybeSigned {
fn from(tx: TransactionRequest) -> Self {
Self::new(WithOtherFields::new(tx))
}
}

impl TryFrom<TxEnvelope> for TransactionMaybeSigned {
type Error = alloy_primitives::SignatureError;

fn try_from(tx: TxEnvelope) -> core::result::Result<Self, Self::Error> {
Self::new_signed(tx)
}
}
11 changes: 11 additions & 0 deletions crates/evm/core/src/backend/cow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Backend>,
) -> eyre::Result<()> {
self.backend_mut(env).transact_from_tx(transaction, env, journaled_state, inspector)
}

fn active_fork_id(&self) -> Option<LocalForkId> {
self.backend.active_fork_id()
}
Expand Down
Loading

0 comments on commit 1f9b52c

Please sign in to comment.