diff --git a/crates/abi/abi/HEVM.sol b/crates/abi/abi/HEVM.sol index 3e020e6640e49..ee657241d9ea3 100644 --- a/crates/abi/abi/HEVM.sol +++ b/crates/abi/abi/HEVM.sol @@ -3,9 +3,11 @@ struct Rpc { string name; string url; } struct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; } struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; } struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; } +struct FfiResult { int32 exit_code; bytes stdout; bytes stderr; } allowCheatcodes(address) +tryFfi(string[])(FfiResult) ffi(string[])(bytes) breakpoint(string) diff --git a/crates/abi/src/bindings/hevm.rs b/crates/abi/src/bindings/hevm.rs index 34e50d45ef032..62b2a4467491b 100644 --- a/crates/abi/src/bindings/hevm.rs +++ b/crates/abi/src/bindings/hevm.rs @@ -4875,6 +4875,40 @@ pub mod hevm { }, ], ), + ( + ::std::borrow::ToOwned::to_owned("tryFfi"), + ::std::vec![ + ::ethers_core::abi::ethabi::Function { + name: ::std::borrow::ToOwned::to_owned("tryFfi"), + inputs: ::std::vec![ + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::Array( + ::std::boxed::Box::new( + ::ethers_core::abi::ethabi::ParamType::String, + ), + ), + internal_type: ::core::option::Option::None, + }, + ], + outputs: ::std::vec![ + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::Tuple( + ::std::vec![ + ::ethers_core::abi::ethabi::ParamType::Int(32usize), + ::ethers_core::abi::ethabi::ParamType::Bytes, + ::ethers_core::abi::ethabi::ParamType::Bytes, + ], + ), + internal_type: ::core::option::Option::None, + }, + ], + constant: ::core::option::Option::None, + state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, + }, + ], + ), ( ::std::borrow::ToOwned::to_owned("txGasPrice"), ::std::vec![ @@ -7109,6 +7143,18 @@ pub mod hevm { .method_hash([77, 138, 188, 75], (p0, p1)) .expect("method not found (this should never happen)") } + ///Calls the contract's `tryFfi` (0xf45c1ce7) function + pub fn try_ffi( + &self, + p0: ::std::vec::Vec<::std::string::String>, + ) -> ::ethers_contract::builders::ContractCall< + M, + (i32, ::ethers_core::types::Bytes, ::ethers_core::types::Bytes), + > { + self.0 + .method_hash([244, 92, 28, 231], p0) + .expect("method not found (this should never happen)") + } ///Calls the contract's `txGasPrice` (0x48f50c0f) function pub fn tx_gas_price( &self, @@ -10012,6 +10058,19 @@ pub mod hevm { )] #[ethcall(name = "transact", abi = "transact(uint256,bytes32)")] pub struct Transact1Call(pub ::ethers_core::types::U256, pub [u8; 32]); + ///Container type for all input parameters for the `tryFfi` function with signature `tryFfi(string[])` and selector `0xf45c1ce7` + #[derive( + Clone, + ::ethers_contract::EthCall, + ::ethers_contract::EthDisplay, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + #[ethcall(name = "tryFfi", abi = "tryFfi(string[])")] + pub struct TryFfiCall(pub ::std::vec::Vec<::std::string::String>); ///Container type for all input parameters for the `txGasPrice` function with signature `txGasPrice(uint256)` and selector `0x48f50c0f` #[derive( Clone, @@ -10310,6 +10369,7 @@ pub mod hevm { ToString5(ToString5Call), Transact0(Transact0Call), Transact1(Transact1Call), + TryFfi(TryFfiCall), TxGasPrice(TxGasPriceCall), Warp(WarpCall), WriteFile(WriteFileCall), @@ -11157,6 +11217,10 @@ pub mod hevm { = ::decode(data) { return Ok(Self::Transact1(decoded)); } + if let Ok(decoded) + = ::decode(data) { + return Ok(Self::TryFfi(decoded)); + } if let Ok(decoded) = ::decode(data) { return Ok(Self::TxGasPrice(decoded)); @@ -11668,6 +11732,7 @@ pub mod hevm { Self::Transact1(element) => { ::ethers_core::abi::AbiEncode::encode(element) } + Self::TryFfi(element) => ::ethers_core::abi::AbiEncode::encode(element), Self::TxGasPrice(element) => { ::ethers_core::abi::AbiEncode::encode(element) } @@ -11910,6 +11975,7 @@ pub mod hevm { Self::ToString5(element) => ::core::fmt::Display::fmt(element, f), Self::Transact0(element) => ::core::fmt::Display::fmt(element, f), Self::Transact1(element) => ::core::fmt::Display::fmt(element, f), + Self::TryFfi(element) => ::core::fmt::Display::fmt(element, f), Self::TxGasPrice(element) => ::core::fmt::Display::fmt(element, f), Self::Warp(element) => ::core::fmt::Display::fmt(element, f), Self::WriteFile(element) => ::core::fmt::Display::fmt(element, f), @@ -12905,6 +12971,11 @@ pub mod hevm { Self::Transact1(value) } } + impl ::core::convert::From for HEVMCalls { + fn from(value: TryFfiCall) -> Self { + Self::TryFfi(value) + } + } impl ::core::convert::From for HEVMCalls { fn from(value: TxGasPriceCall) -> Self { Self::TxGasPrice(value) @@ -14270,6 +14341,20 @@ pub mod hevm { Hash )] pub struct SnapshotReturn(pub ::ethers_core::types::U256); + ///Container type for all return fields from the `tryFfi` function with signature `tryFfi(string[])` and selector `0xf45c1ce7` + #[derive( + Clone, + ::ethers_contract::EthAbiType, + ::ethers_contract::EthAbiCodec, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + pub struct TryFfiReturn( + pub (i32, ::ethers_core::types::Bytes, ::ethers_core::types::Bytes), + ); ///`DirEntry(string,string,uint64,bool,bool)` #[derive( Clone, @@ -14288,6 +14373,22 @@ pub mod hevm { pub is_dir: bool, pub is_symlink: bool, } + ///`FfiResult(int32,bytes,bytes)` + #[derive( + Clone, + ::ethers_contract::EthAbiType, + ::ethers_contract::EthAbiCodec, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + pub struct FfiResult { + pub exit_code: i32, + pub stdout: ::ethers_core::types::Bytes, + pub stderr: ::ethers_core::types::Bytes, + } ///`FsMetadata(bool,bool,uint256,bool,uint256,uint256,uint256)` #[derive( Clone, diff --git a/crates/evm/src/executor/inspector/cheatcodes/ext.rs b/crates/evm/src/executor/inspector/cheatcodes/ext.rs index ee4707019de58..a138e422e2d9d 100644 --- a/crates/evm/src/executor/inspector/cheatcodes/ext.rs +++ b/crates/evm/src/executor/inspector/cheatcodes/ext.rs @@ -11,6 +11,50 @@ use serde::Deserialize; use serde_json::Value; use std::{collections::BTreeMap, env, path::Path, process::Command}; +/// Invokes a `Command` with the given args and returns the exit code, stdout, and stderr. +/// +/// If stdout or stderr are valid hex, it returns the hex decoded value. +fn try_ffi(state: &Cheatcodes, args: &[String]) -> Result { + if args.is_empty() || args[0].is_empty() { + bail!("Can't execute empty command"); + } + let name = &args[0]; + let mut cmd = Command::new(name); + if args.len() > 1 { + cmd.args(&args[1..]); + } + + trace!(?args, "invoking try_ffi"); + + let output = cmd + .current_dir(&state.config.root) + .output() + .map_err(|err| fmt_err!("Failed to execute command: {err}"))?; + + let exit_code = output.status.code().unwrap_or(1); + + let trimmed_stdout = String::from_utf8(output.stdout)?; + let trimmed_stdout = trimmed_stdout.trim(); + + // The stdout might be encoded on valid hex, or it might just be a string, + // so we need to determine which it is to avoid improperly encoding later. + let encoded_stdout: Token = + if let Ok(hex) = hex::decode(trimmed_stdout.strip_prefix("0x").unwrap_or(trimmed_stdout)) { + Token::Bytes(hex) + } else { + Token::Bytes(trimmed_stdout.into()) + }; + + let res = abi::encode(&[Token::Tuple(vec![ + Token::Int(exit_code.into()), + encoded_stdout, + // We can grab the stderr output as-is. + Token::Bytes(output.stderr), + ])]); + + Ok(res.into()) +} + /// Invokes a `Command` with the given args and returns the abi encoded response /// /// If the output of the command is valid hex, it returns the hex decoded value @@ -461,6 +505,13 @@ pub fn apply(state: &mut Cheatcodes, call: &HEVMCalls) -> Option { Err(fmt_err!("FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts.")) } } + HEVMCalls::TryFfi(inner) => { + if state.config.ffi { + try_ffi(state, &inner.0) + } else { + Err(fmt_err!("FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts.")) + } + } HEVMCalls::GetCode(inner) => get_code(state, &inner.0), HEVMCalls::GetDeployedCode(inner) => get_deployed_code(state, &inner.0), HEVMCalls::SetEnv(inner) => set_env(&inner.0, &inner.1), diff --git a/testdata/cheats/TryFfi.sol b/testdata/cheats/TryFfi.sol new file mode 100644 index 0000000000000..f33769cc82b4e --- /dev/null +++ b/testdata/cheats/TryFfi.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.18; + +import "ds-test/test.sol"; +import "./Vm.sol"; + +contract TryFfiTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testTryFfi() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = + "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + + Vm.FfiResult memory f = vm.tryFfi(inputs); + (string memory output) = abi.decode(f.stdout, (string)); + assertEq(output, "ffi works", "ffi failed"); + assertEq(f.exit_code, 0, "ffi failed"); + } + + function testTryFfiFail() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = "quikmafs"; + + Vm.FfiResult memory f = vm.tryFfi(inputs); + assert(f.exit_code != 0); + assertEq(string(f.stderr), string("bash: quikmafs: command not found\n")); + } +} diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 2b35df6db0c8c..a83f70ed6a4e2 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -52,6 +52,12 @@ interface Vm { uint256 privateKey; } + struct FfiResult { + int32 exit_code; + bytes stdout; + bytes stderr; + } + // Set block.timestamp (newTimestamp) function warp(uint256) external; @@ -116,6 +122,9 @@ interface Vm { // Performs a foreign function call via terminal, (stringInputs) => (result) function ffi(string[] calldata) external returns (bytes memory); + // Performs a foreign function call via terminal and returns the exit code, stdout, and stderr + function tryFfi(string[] calldata) external returns (FfiResult memory); + // Set environment variables, (name, value) function setEnv(string calldata, string calldata) external;