Skip to content
Closed
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
28 changes: 28 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,36 @@ SUBCOMMANDS:
namehash returns ENS namehash of provided name
nonce Prints the number of transactions sent from <address>
resolve-name Returns the address the provided ENS name resolves to
run Executes arbitrary bytecode in hex format
send Publish a transaction signed by <from> to call <to> with <data>
storage Show the raw value of a contract's storage slot
tx Show information about the transaction <tx-hash>
wallet Set of wallet management utilities
```


### Run

Cast's `run` subcommand executes arbitrary bytecode provided in hex format.

To use the `run` command, run `cast run <BYTECODE> [CALLDATA]`.
Where `<BYTECODE>` is the bytcode in hex format (eg. `0x604260005260206000F3`) and `<CALLDATA>` is the calldata in hex format (eg. `0x70a08231000000000000000000000000b4c79dab8f259c7aee6e5b2aa729821864227e84`).

To view extensive documentation of `cast run`, run `cast run --help`:
```
cast-run
Executes arbitrary bytecode in hex format

USAGE:
cast run [OPTIONS] <BYTECODE> [--] [CALLDATA]

ARGS:
<BYTECODE>
the bytecode to execute

<CALLDATA>
the calldata to pass to the contract

OPTIONS:
...
```
1 change: 1 addition & 0 deletions cli/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ async fn main() -> eyre::Result<()> {
eyre::bail!("No wallet or sender address provided.")
}
}
Subcommands::Run(cmd) => cmd.run()?,
Subcommands::PublishTx { eth, raw_tx, cast_async } => {
let provider = Provider::try_from(eth.rpc_url()?)?;
let cast = Cast::new(&provider);
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;
164 changes: 164 additions & 0 deletions cli/src/cmd/cast/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use crate::{
cmd::{forge::build::BuildArgs, Cmd},
opts::evm::EvmArgs,
utils,
};

use ansi_term::Colour;
use clap::Parser;
use ethers::types::{Address, Bytes, U256};

use forge::{
executor::{
opts::EvmOpts, DatabaseRef, DeployResult, Executor, ExecutorBuilder, RawCallResult,
},
trace::TraceKind,
CALLER,
};
use foundry_config::{figment::Figment, Config};
use foundry_utils::{encode_args, IntoFunction};
use hex::ToHex;

// Loads project's figment and merges the build cli arguments into it
foundry_config::impl_figment_convert!(RunArgs, opts, evm_opts);

#[derive(Debug, Clone, Parser)]
pub struct RunArgs {
#[clap(help = "the bytecode to execute")]
pub bytecode: String,

// Optional Calldata
#[clap(help = "the calldata to pass to the contract")]
pub calldata: Option<String>,

/// Open the bytecode execution in debug mode
#[clap(long, help = "debug the bytecode execution")]
pub debug: bool,

#[clap(flatten)]
opts: BuildArgs,

#[clap(flatten)]
pub evm_opts: EvmArgs,
}

impl Cmd for RunArgs {
type Output = ();

fn run(self) -> eyre::Result<Self::Output> {
// Load figment
let figment: Figment = From::from(&self);
let evm_opts = figment.extract::<EvmOpts>()?;
let verbosity = evm_opts.verbosity;
let config = Config::from_provider(figment).sanitized();

// Parse bytecode string
let parsed_bytecode = self.bytecode.parse::<Bytes>()?;

// Parse Calldata
let calldata: Bytes = if let Some(calldata) =
self.calldata.unwrap_or_else(|| "0x".to_string()).strip_prefix("0x")
{
hex::decode(calldata)?.into()
} else {
let args: Vec<String> = vec![];
encode_args(&IntoFunction::into("".to_string()), &args)?.into()
};

// Create executor
let mut builder = ExecutorBuilder::new()
.with_cheatcodes(evm_opts.ffi)
.with_config(evm_opts.evm_env())
.with_spec(crate::utils::evm_spec(&config.evm_version))
.with_fork(utils::get_fork(&evm_opts, &config.rpc_storage_caching));
if verbosity >= 3 {
builder = builder.with_tracing();
}
if self.debug {
builder = builder.with_tracing().with_debugger();
}

// Create the runner
let mut runner = Runner::new(builder.build(), evm_opts.sender);

// Deploy the bytecode
let DeployResult { address, .. } = runner.setup(parsed_bytecode)?;

// Run the bytecode at the deployed address
let rcr = runner.run(address, calldata)?;

// TODO: Waterfall debug
// Ex: https://twitter.com/danielvf/status/1503756428212936710

// Unwrap Traces
let mut traces =
rcr.traces.map(|traces| vec![(TraceKind::Execution, traces)]).unwrap_or_default();

if verbosity >= 3 {
if traces.is_empty() {
eyre::bail!("Unexpected error: No traces despite verbosity level. Please report this as a bug: https://github.com/gakonst/foundry/issues/new?assignees=&labels=T-bug&template=BUG-FORM.yml");
}

if rcr.reverted {
println!("Traces:");
for (kind, trace) in &mut traces {
let should_include = match kind {
TraceKind::Setup => (verbosity >= 5) || (verbosity == 4),
TraceKind::Execution => verbosity > 3,
_ => false,
};

if should_include {
// TODO: Create decoder using local fork
// decoder.decode(trace);
println!("{}", trace);
}
}
println!();
}
}

if rcr.reverted {
println!("{}", Colour::Red.paint("[REVERT]"));
println!("Gas consumed: {}", rcr.gas);
} else {
println!("{}", Colour::Green.paint("[SUCCESS]"));
let o = rcr.result.encode_hex::<String>();
if !o.is_empty() {
println!("Output: {}", o);
} else {
println!("{}", Colour::Yellow.paint("No Output"));
}
println!("Gas consumed: {}", rcr.gas);
}

Ok(())
}
}

struct Runner<DB: DatabaseRef> {
pub executor: Executor<DB>,
pub sender: Address,
}

impl<DB: DatabaseRef> Runner<DB> {
pub fn new(executor: Executor<DB>, sender: Address) -> Self {
Self { executor, sender }
}

pub fn setup(&mut self, code: Bytes) -> eyre::Result<DeployResult> {
// We max out their balance so that they can deploy and make calls.
self.executor.set_balance(self.sender, U256::MAX);
self.executor.set_balance(*CALLER, U256::MAX);

// We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools
self.executor.set_nonce(self.sender, 1);

// Deploy an instance of the contract
Ok(self.executor.deploy(self.sender, code.0, 0u32.into()).expect("couldn't deploy"))
}

pub fn run(&mut self, address: Address, calldata: Bytes) -> eyre::Result<RawCallResult> {
self.executor.call_raw(self.sender, address, calldata.0, 0_u64.into())
}
}
7 changes: 6 additions & 1 deletion cli/src/opts/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use ethers::{
};

use super::{ClapChain, EthereumOpts, Wallet};
use crate::{cmd::cast::find_block::FindBlockArgs, utils::parse_u256};
use crate::{
cmd::cast::{find_block::FindBlockArgs, run::RunArgs},
utils::parse_u256,
};

#[derive(Debug, Subcommand)]
#[clap(about = "Perform Ethereum RPC calls from the comfort of your command line.")]
Expand Down Expand Up @@ -255,6 +258,8 @@ pub enum Subcommands {
#[clap(long = "json", short = 'j')]
to_json: bool,
},
#[clap(alias = "r", about = "Executes arbitrary bytecode in hex format")]
Run(Box<RunArgs>),
#[clap(name = "publish")]
#[clap(about = "Publish a raw transaction to the network")]
PublishTx {
Expand Down
14 changes: 14 additions & 0 deletions cli/test-utils/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ macro_rules! forgetest {
};
}

#[macro_export]
macro_rules! casttest {
($test:ident, $fun:expr) => {
$crate::casttest!($test, $crate::ethers_solc::PathStyle::Dapptools, $fun);
};
($test:ident, $style:expr, $fun:expr) => {
#[test]
fn $test() {
let (prj, cmd) = $crate::util::setup_cast(stringify!($test), $style);
$fun(prj, cmd);
}
};
}
Comment on lines +49 to +60
Copy link
Member

Choose a reason for hiding this comment

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

nice!


/// A helper macro to ignore `forgetest!` that should not run on CI
#[macro_export]
macro_rules! forgetest_ignore {
Expand Down
32 changes: 32 additions & 0 deletions cli/test-utils/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ pub fn setup_project(test: TestProject) -> (TestProject, TestCommand) {
(test, cmd)
}

pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
setup_cast_project(TestProject::new(name, style))
}

pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
let cmd = test.ccommand();
(test, cmd)
}

/// `TestProject` represents a temporary project to run tests against.
///
/// Test projects are created from a global atomic counter to avoid duplicates.
Expand Down Expand Up @@ -197,12 +206,31 @@ impl TestProject {
}
}

/// Creates a new command that is set to use the cast executable for this project
pub fn ccommand(&self) -> TestCommand {
let mut cmd = self.cbin();
cmd.current_dir(&self.inner.root());
TestCommand {
project: self.clone(),
cmd,
saved_env_vars: HashMap::new(),
current_dir_lock: None,
saved_cwd: pretty_err(".", std::env::current_dir()),
}
}

/// Returns the path to the forge executable.
pub fn bin(&self) -> process::Command {
let forge = self.root.join(format!("../forge{}", env::consts::EXE_SUFFIX));
process::Command::new(forge)
}

/// Returns the path to the cast executable.
pub fn cbin(&self) -> process::Command {
let cast = self.root.join(format!("../cast{}", env::consts::EXE_SUFFIX));
process::Command::new(cast)
}

/// Returns the `Config` as spit out by `forge config`
pub fn config_from_output<I, A>(&self, args: I) -> Config
where
Expand Down Expand Up @@ -288,6 +316,10 @@ impl TestCommand {
self.set_cmd(self.project.bin())
}

pub fn cfuse(&mut self) -> &mut TestCommand {
self.set_cmd(self.project.cbin())
}

/// Sets the current working directory
pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
drop(self.current_dir_lock.take());
Expand Down
67 changes: 67 additions & 0 deletions cli/tests/cast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! Contains various tests for checking cast commands
use foundry_cli_test_utils::{
casttest,
util::{TestCommand, TestProject},
};

// tests that the `cast run` command works correctly
casttest!(run_bytecode, |_: TestProject, mut cmd: TestCommand| {
// Create executable bytecode
let raw_bytecode = "0x604260005260206000F3".to_string();
let raw_calldata =
"0x70a08231000000000000000000000000b4c79dab8f259c7aee6e5b2aa729821864227e84".to_string();

// Call `cast run`
cmd.arg("run").arg(raw_bytecode).arg(raw_calldata);
let output = cmd.stdout_lossy();

// Expect successful bytecode execution
assert!(output.contains("[SUCCESS]"));
assert!(output.contains("Gas consumed:"));
assert!(output.contains("No Output"));

// Create reverting bytecode
let reverting_bytecode = "0x610101610102016000526001601ff3".to_string();
let revert_calldata =
"0x70a08231000000000000000000000000b4c79dab8f259c7aee6e5b2aa729821864227e84".to_string();

// Call `cast run`
cmd.cfuse().arg("run").arg(reverting_bytecode).arg(revert_calldata);
let revert_output = cmd.stdout_lossy();

// Expect bytecode execution to revert
assert!(revert_output.contains("[REVERT]"));
assert!(revert_output.contains("Gas consumed:"));

// On revert, we don't have an output
assert!(!revert_output.contains("Output"));
});

// tests that `cast run` works without calldata
casttest!(run_bytecode_absent_calldata, |_: TestProject, mut cmd: TestCommand| {
// Create executable bytecode
let raw_bytecode = "0x604260005260206000F3".to_string();

// Call `cast run`
cmd.arg("run").arg(raw_bytecode);
let output = cmd.stdout_lossy();

// Expect successful bytecode execution
assert!(output.contains("[SUCCESS]"));
assert!(output.contains("Gas consumed:"));
assert!(output.contains("No Output"));

// Create reverting bytecode
let reverting_bytecode = "0x610101610102016000526001601ff3".to_string();

// Call `cast run`
cmd.cfuse().arg("run").arg(reverting_bytecode);
let revert_output = cmd.stdout_lossy();

// Expect bytecode execution to revert
assert!(revert_output.contains("[REVERT]"));
assert!(revert_output.contains("Gas consumed:"));

// On revert, we don't have an output
assert!(!revert_output.contains("Output"));
});