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

Conversation

joshieDo
Copy link
Collaborator

@joshieDo joshieDo commented Apr 18, 2022

Motivation

Run a published transaction locally with trace and/or debug.

Solution

  • Fork (transaction.block - 1)
  • Load and execute all transactions previous to ours in the transaction.block
  • Execute ours
  • Decode with EtherscanIdentifier
cast-run 
Runs a published transaction in a local environment.

USAGE:
    cast run [OPTIONS] --rpc-url <RPC_URL> <TX>

ARGS:
    <TX>    The transaction hash.

OPTIONS:
    -d, --debug                Debugs the transaction.
    -h, --help                 Print help information
        --label <LABEL>        Labels address in the trace.
                               0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth
    -q, --quick                Executes the transaction only with the state from the previous block. May
                               result in different results than the live execution!
    -r, --rpc-url <RPC_URL>    [env: ETH_RPC_URL=https://rpc.flashbots.net

Example (BEAN exploit):

cast run 0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7 --rpc-url $ARCHIVE_RPC

image

too long, didn't fit lol

Comment on lines 45 to 46
#[clap(long)]
pub rpc: String,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

redundant, but how can i make the one from evm_opts mandatory?

@onbjerg onbjerg added the T-feature Type: feature label Apr 18, 2022
@mds1
Copy link
Collaborator

mds1 commented Apr 18, 2022

This is awesome, I was excited for this functionality!

Though I think this should be part of cast instead. #29 is where cast run-tx (analogous to seth run-tx) is tracked, and that's pretty much exactly what this is

@joshieDo
Copy link
Collaborator Author

forge run-tx 😅 ?

I thought about including it on cast but in the end i decided for forge because of this:

use forge::{
    debug::DebugArena,
    decode::decode_console_logs,
    executor::{builder::Backend, opts::EvmOpts, DeployResult, ExecutorBuilder, RawCallResult},
    trace::{identifier::EtherscanIdentifier, CallTraceArena, CallTraceDecoderBuilder, TraceKind},
};

The dependencies are mostly in the forge crate, and making it accessible on cast is messy no?

@onbjerg
Copy link
Member

onbjerg commented Apr 18, 2022

All of those are re-exports of the evm crate

@mds1
Copy link
Collaborator

mds1 commented Apr 18, 2022

Should we call it cast run-tx, or just cast run? I kind of like the shorter version, and it parallels forge run nicely.

Also @joshieDo no pressure at all to implement all of the below in this PR, but figured this is a good spot to document the list of features I liked from seth run-tx + what I wish seth had. I can move these all into a separate issue depending on how much is done here. But it's about time I get them out of my head and documented somewhere (I thought I did this somewhere but can't find it) 😅

  • cast run <tx> --trace to show a trace like you have above
  • cast run <tx> --debug to launch the tx in the debugger (these first two are analogous to seth)
  • --quick skips simulating previous txs, for speed (docs should note this is unsound and may give different results than the live tx). Also how slow was simulating previous txs for you? In seth it was painfully slow so I always skipped it
  • If a contract name is verified on etherscan, use it's name in the trace (and append IDs if there's more than one with the same name, e.g. Vyper_contract-1, Vyper_contract-2). Seems this is partly done already 🎉
  • --label <addr1> <name1> --label <addr2> <name2> to give nice labels to arbitrary addresses
  • --dense flag to hide addresses for labeled contracts (can only hide them entirely if we ensure unique labels, or just shrinking to 0x123...def works too
  • replace numbers with more human friendly representations, e.g. lots of numbers in that screenshot can be replaced with 2^255 - 1

@gakonst
Copy link
Member

gakonst commented Apr 19, 2022

+1 on "cast run" and using the foundry_evm exports

@joshieDo
Copy link
Collaborator Author

joshieDo commented Apr 19, 2022

Moved it and simplified some stuff

Thanks @mds1 , i like those suggestions

cast run --trace to show a trace like you have above

default behaviour has tracing, so I didn't include the flag. What does seth show if no flag is set?

--quick skips simulating previous txs, for speed (docs should note this is unsound and may give different results than the live tx). Also how slow was simulating previous txs for you? In seth it was painfully slow so I always skipped it

Added the --quick. Regarding the slowness... it depends on how many transactions there are in the block, how intense is their interaction with multiple states and the RPC responsiveness. However, I have a feeling the provider is caching the requests, so it should happen only once. Emulating the actual transactions on the EVM is pretty damn fast in my experience.

--label --label to give nice labels to arbitrary addresses

I went for the format address:label

--

All the other suggestions, should probably done in other issues, since it touches on how traces are displayed, if I'm not mistaken.

@joshieDo
Copy link
Collaborator Author

joshieDo commented Apr 19, 2022

Example on a block with 17 transactions, but with a lot of storage accesses I guess

Not cached:

time cargo run --bin cast -- run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/cast run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC
Executing previous transactions from the block.
Traces:
  [0] 0x165c…5e14::fallback{value: 750000000000000000000}() 
    └─  ()


Script ran successfully.
Gas used: 21000
cargo run --bin cast -- run  --rpc-url   2.61s user 0.25s system 4% cpu 1:01.39 total

Cached:

time cargo run --bin cast -- run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/cast run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC
Executing previous transactions from the block.
Traces:
  [0] 0x165c…5e14::fallback{value: 750000000000000000000}() 
    └─  ()


Script ran successfully.
Gas used: 21000
cargo run --bin cast -- run  --rpc-url   1.24s user 0.13s system 40% cpu 3.399 total

Not cached + quick:

time cargo run --bin cast -- run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC --label 0x165CD37b4C644C2921454429E7F9358d18A45e14:ukraine --quick
    Finished dev [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/cast run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC --label '0x165CD37b4C644C2921454429E7F9358d18A45e14:ukraine' --quick`
Traces:
  [0] ukraine::fallback{value: 750000000000000000000}() 
    └─  ()


Script ran successfully.
Gas used: 21000
cargo run --bin cast -- run  --rpc-url  --label  --quick  0.68s user 0.13s system 11% cpu 7.245 total

cached + quick:

time cargo run --bin cast -- run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC --label 0x165CD37b4C644C2921454429E7F9358d18A45e14:ukraine --quick
    Finished dev [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/cast run 0xa8faf11f4e4c0a93fe9df333cc18d15e854d94b9f19d2237899213cf73c4699a --rpc-url $RPC --label '0x165CD37b4C644C2921454429E7F9358d18A45e14:ukraine' --quick`
Traces:
  [0] ukraine::fallback{value: 750000000000000000000}() 
    └─  ()


Script ran successfully.
Gas used: 21000
cargo run --bin cast -- run  --rpc-url  --label  --quick  0.52s user 0.11s system 24% cpu 2.562 total

@joshieDo joshieDo changed the title feat(forge): simulate published transaction locally feat(cast): simulate published transaction locally Apr 19, 2022
@mds1
Copy link
Collaborator

mds1 commented Apr 19, 2022

default behaviour has tracing, so I didn't include the flag. What does seth show if no flag is set?

Defaulting to trace seems good to me! seth's default is to run the tx and then print any return data. Here was my test to confirm I'm remembering correctly:

$ seth run-tx 0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7        
seth---to-hexdata: error: invalid hexdata: `null'
0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000284c69624469616d6f6e644375743a205f696e6974206164647265737320686173206e6f20636f6465000000000000000000000000000000000000000000000000

That return data decodes to Error(string) with a message of "LibDiamondCut: _init address has no code". That tx obviously did not revert, so I'm not sure offhand why that's the return data 🤷

So to confirm I took a random ordinary DAI transfer, which returns a bool, and you can see it returns true (encoded):

$ seth run-tx 0x96ac50cc5dcb1c3787082d122217d2cf3f692fe06273aeff9c18157f48be52be
0x0000000000000000000000000000000000000000000000000000000000000001

Added the --quick. Regarding the slowness... it depends on how many transactions there are in the block, how intense is their interaction with multiple states and the RPC responsiveness. However, I have a feeling the provider is caching the requests, so it should happen only once. Emulating the actual transactions on the EVM is pretty damn fast in my experience.

I went for the format address:label

All the other suggestions, should probably done in other issues, since it touches on how traces are displayed, if I'm not mistaken.

All of this sounds great, I'll pull that set of features into a separate issue

Example on a block with 17 transactions, but with a lot of storage accesses I guess

Looks great 😍 Lmk when you think this PR is in a good state (maybe once out of draft) and I can help test it

@joshieDo joshieDo marked this pull request as ready for review April 19, 2022 17:38
@joshieDo
Copy link
Collaborator Author

opening for review

I was thinking of maybe looking into debugging with the source code mapping in another PR, but open to include it in this one

@mds1
Copy link
Collaborator

mds1 commented Apr 20, 2022

Working great!! Thanks a lot @joshieDo 🔥

Only issue I can find is that if a tx fails in the constructor, you get a panic instead of a trace, but this is present in forge too, so not a problem caused by your PR. @onbjerg I recall we've briefly discussed this in an issue before, but not sure if there's a dedicated issue for this? If so let me know and I can create one

cast run 0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7 -q
The application panicked (crashed).
Message:  called `Result::unwrap()` on an `Err` value:
   0: deployment failed: Revert

Location:
   ~/.foundry/foundry-rs/foundry/evm/src/executor/mod.rs:456

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Location: cli/src/cmd/cast/run.rs:116

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
[1]    37788 abort      cast run 0xcd314668aaa9bbfebaf1a0bd2b6553d01dd58899c508d4729fa7311dc5d33ad7 -

let builder = ExecutorBuilder::new()
.with_config(env)
.with_spec(crate::utils::evm_spec(&config.evm_version))
.with_gas_limit(evm_opts.gas_limit());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably want to use the txs gas limit with tx.gas here instead?

@@ -619,6 +619,8 @@ 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.")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
#[clap(name = "run", about = "Runs a published transaction in a local environment.")]
#[clap(name = "run", about = "Runs a published transaction in a local environment and prints the trace.")]

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

some nits

Comment on lines +44 to +46
impl Cmd for RunArgs {
type Output = ();
fn run(self) -> eyre::Result<Self::Output> {
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

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?

Comment on lines 218 to 220
pub fn enable_tracing(&mut self) {
self.inspector_config.tracing = true;
}
Copy link
Member

Choose a reason for hiding this comment

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

these look like builder functions,

there are two common patterns for these kinds of functions, either fn set(mut self, val) -> Self

or fn set(&mut self, val)-> &mut Self

seems like we can apply the latter here and return &mut Self?

cli/src/cmd/cast/run.rs Outdated Show resolved Hide resolved
Co-authored-by: Matt Solomon <matt@mattsolomon.dev>
Copy link
Member

@gakonst gakonst left a comment

Choose a reason for hiding this comment

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

LGTM. Let's ship it and if there's any bugs iterate from there?
@joshieDo this is a fun enough feature, feel free to tweet it out to the world :)

@gakonst gakonst merged commit 92427e7 into foundry-rs:master Apr 21, 2022
This was referenced Apr 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-feature Type: feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants