Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cast): simulate published transaction locally #1358

Merged
merged 9 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -766,6 +766,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> {
Comment on lines +45 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this a simple async function instead and not implement the Cmd trait here, so that we don't need to deal with the runtime here

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to a separate function?

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 @@ -619,6 +619,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