Skip to content

Commit

Permalink
feat(cast): simulate published transaction locally (#1358)
Browse files Browse the repository at this point in the history
* wip

* no need to set executor nonce

* move forge sim to cast run

* add missing Cargo.lock

* improve about message

* change enable to set pattern

* turn run_tx into an async func

* small refactor

* Update cli/src/cmd/cast/run.rs

Co-authored-by: Matt Solomon <matt@mattsolomon.dev>

Co-authored-by: Matt Solomon <matt@mattsolomon.dev>
  • Loading branch information
joshieDo and mds1 authored Apr 21, 2022
1 parent 7d8372f commit 92427e7
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions cast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ keywords = ["ethereum", "web3"]

[dependencies]
foundry-utils = { path = "../utils" }
foundry-evm = { path = "./../evm" }
futures = "0.3.17"
ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use ethers_core::{
use ethers_etherscan::Client;
use ethers_providers::{Middleware, PendingTransaction};
use eyre::{Context, Result};
pub use foundry_evm::*;
use foundry_utils::{encode_args, to_table};
use print_utils::{get_pretty_block_attr, get_pretty_tx_attr, UIfmt};
use rustc_hex::{FromHexIter, ToHex};
Expand Down
1 change: 1 addition & 0 deletions cli/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ async fn main() -> eyre::Result<()> {
Subcommands::Completions { shell } => {
generate(shell, &mut Opts::command(), "cast", &mut std::io::stdout())
}
Subcommands::Run(cmd) => cmd.run()?,
};
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions cli/src/cmd/cast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
//! [`foundry_config::Config`].

pub mod find_block;
pub mod run;
210 changes: 210 additions & 0 deletions cli/src/cmd/cast/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use crate::{cmd::Cmd, utils};
use ansi_term::Colour;
use cast::trace::CallTraceDecoder;
use clap::Parser;
use ethers::{
abi::Address,
prelude::{Middleware, Provider},
types::H256,
};
use forge::{
debug::DebugArena,
executor::{builder::Backend, opts::EvmOpts, DeployResult, ExecutorBuilder, RawCallResult},
trace::{identifier::EtherscanIdentifier, CallTraceArena, CallTraceDecoderBuilder, TraceKind},
};
use foundry_config::Config;
use foundry_utils::RuntimeOrHandle;
use std::{
collections::{BTreeMap, HashMap},
str::FromStr,
time::Duration,
};
use ui::{TUIExitReason, Tui, Ui};

#[derive(Debug, Clone, Parser)]
pub struct RunArgs {
#[clap(help = "The transaction hash.")]
tx: String,
#[clap(short, long, env = "ETH_RPC_URL")]
rpc_url: String,
#[clap(long, short = 'd', help = "Debugs the transaction.")]
debug: bool,
#[clap(
long,
short = 'q',
help = "Executes the transaction only with the state from the previous block. May result in different results than the live execution!"
)]
quick: bool,
#[clap(
long,
help = "Labels address in the trace. 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth"
)]
label: Vec<String>,
}

impl Cmd for RunArgs {
type Output = ();
fn run(self) -> eyre::Result<Self::Output> {
RuntimeOrHandle::new().block_on(self.run_tx())
}
}

impl RunArgs {
async fn run_tx(self) -> eyre::Result<()> {
let figment = Config::figment();
let mut evm_opts = figment.extract::<EvmOpts>()?;
let config = Config::from_provider(figment).sanitized();

let provider =
Provider::try_from(self.rpc_url.as_str()).expect("could not instantiate provider");

if let Some(tx) =
provider.get_transaction(H256::from_str(&self.tx).expect("invalid tx hash")).await?
{
let tx_block_number = tx.block_number.expect("no block number").as_u64();
let tx_hash = tx.hash();
evm_opts.fork_url = Some(self.rpc_url);
evm_opts.fork_block_number = Some(tx_block_number - 1);

// Set up the execution environment
let env = evm_opts.evm_env().await;
let db =
Backend::new(utils::get_fork(&evm_opts, &config.rpc_storage_caching), &env).await;

let builder = ExecutorBuilder::new()
.with_config(env)
.with_spec(crate::utils::evm_spec(&config.evm_version));

let mut executor = builder.build(db);

// Set the state to the moment right before the transaction
if !self.quick {
println!("Executing previous transactions from the block.");

let block_txes = provider.get_block_with_txs(tx_block_number).await?;

for past_tx in block_txes.unwrap().transactions.into_iter() {
if past_tx.hash().eq(&tx_hash) {
break
}

executor.set_gas_limit(past_tx.gas);

if let Some(to) = past_tx.to {
executor
.call_raw_committing(past_tx.from, to, past_tx.input.0, past_tx.value)
.unwrap();
} else {
executor.deploy(past_tx.from, past_tx.input.0, past_tx.value).unwrap();
}
}
}

// Execute our transaction
let mut result = {
executor.set_tracing(true).set_gas_limit(tx.gas);

if self.debug {
executor.set_debugger(true);
}

if let Some(to) = tx.to {
let RawCallResult { reverted, gas, traces, debug: run_debug, .. } =
executor.call_raw_committing(tx.from, to, tx.input.0, tx.value)?;

RunResult {
success: !reverted,
traces: vec![(TraceKind::Execution, traces.unwrap_or_default())],
debug: run_debug.unwrap_or_default(),
gas,
}
} else {
let DeployResult { gas, traces, debug: run_debug, .. }: DeployResult =
executor.deploy(tx.from, tx.input.0, tx.value).unwrap();

RunResult {
success: true,
traces: vec![(TraceKind::Execution, traces.unwrap_or_default())],
debug: run_debug.unwrap_or_default(),
gas,
}
}
};

let etherscan_identifier = EtherscanIdentifier::new(
evm_opts.get_remote_chain_id(),
config.etherscan_api_key,
Config::foundry_etherscan_cache_dir(evm_opts.get_chain_id()),
Duration::from_secs(24 * 60 * 60),
);

let labeled_addresses: BTreeMap<Address, String> = self
.label
.iter()
.filter_map(|label_str| {
let mut iter = label_str.split(':');

if let Some(addr) = iter.next() {
if let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next()) {
return Some((address, label.to_string()))
}
}
None
})
.collect();

let mut decoder = CallTraceDecoderBuilder::new().with_labels(labeled_addresses).build();

for (_, trace) in &mut result.traces {
decoder.identify(trace, &etherscan_identifier);
}

if self.debug {
run_debugger(result, decoder)?;
} else {
print_traces(&mut result, decoder)?;
}
}
Ok(())
}
}

fn run_debugger(result: RunResult, decoder: CallTraceDecoder) -> eyre::Result<()> {
// TODO Get source from etherscan
let source_code: BTreeMap<u32, String> = BTreeMap::new();
let calls: Vec<DebugArena> = vec![result.debug];
let flattened = calls.last().expect("we should have collected debug info").flatten(0);
let tui = Tui::new(flattened, 0, decoder.contracts, HashMap::new(), source_code)?;
match tui.start().expect("Failed to start tui") {
TUIExitReason::CharExit => Ok(()),
}
}

fn print_traces(result: &mut RunResult, decoder: CallTraceDecoder) -> eyre::Result<()> {
if result.traces.is_empty() {
eyre::bail!("Unexpected error: No traces. Please report this as a bug: https://github.com/foundry-rs/foundry/issues/new?assignees=&labels=T-bug&template=BUG-FORM.yml");
}

println!("Traces:");
for (_, trace) in &mut result.traces {
decoder.decode(trace);
println!("{trace}");
}
println!();

if result.success {
println!("{}", Colour::Green.paint("Script ran successfully."));
} else {
println!("{}", Colour::Red.paint("Script failed."));
}

println!("Gas used: {}", result.gas);
Ok(())
}

struct RunResult {
pub success: bool,
pub traces: Vec<(TraceKind, CallTraceArena)>,
pub debug: DebugArena,
pub gas: u64,
}
7 changes: 6 additions & 1 deletion cli/src/opts/cast.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{ClapChain, EthereumOpts, Wallet};
use crate::{
cmd::cast::find_block::FindBlockArgs,
cmd::cast::{find_block::FindBlockArgs, run::RunArgs},
utils::{parse_ether_value, parse_u256},
};
use clap::{Parser, Subcommand, ValueHint};
Expand Down Expand Up @@ -623,6 +623,11 @@ If an address is specified, then the ABI is fetched from Etherscan."#
#[clap(arg_enum)]
shell: clap_complete::Shell,
},
#[clap(
name = "run",
about = "Runs a published transaction in a local environment and prints the trace."
)]
Run(RunArgs),
}

#[derive(Debug, Parser)]
Expand Down
21 changes: 19 additions & 2 deletions evm/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,12 @@ where
}

/// Set the balance of an account.
pub fn set_balance(&mut self, address: Address, amount: U256) {
pub fn set_balance(&mut self, address: Address, amount: U256) -> &mut Self {
let mut account = self.db.basic(address);
account.balance = amount;

self.db.insert_cache(address, account);
self
}

/// Gets the balance of an account
Expand All @@ -208,11 +209,27 @@ where
}

/// Set the nonce of an account.
pub fn set_nonce(&mut self, address: Address, nonce: u64) {
pub fn set_nonce(&mut self, address: Address, nonce: u64) -> &mut Self {
let mut account = self.db.basic(address);
account.nonce = nonce;

self.db.insert_cache(address, account);
self
}

pub fn set_tracing(&mut self, tracing: bool) -> &mut Self {
self.inspector_config.tracing = tracing;
self
}

pub fn set_debugger(&mut self, debugger: bool) -> &mut Self {
self.inspector_config.debugger = debugger;
self
}

pub fn set_gas_limit(&mut self, gas_limit: U256) -> &mut Self {
self.gas_limit = gas_limit;
self
}

/// Calls the `setUp()` function on a contract.
Expand Down

0 comments on commit 92427e7

Please sign in to comment.