diff --git a/abi/abi/HEVM.sol b/abi/abi/HEVM.sol index c92c30eee9c2..3fb5b8734d69 100644 --- a/abi/abi/HEVM.sol +++ b/abi/abi/HEVM.sol @@ -2,9 +2,11 @@ struct Log { bytes32[] topics; bytes data; } 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 FfiResult { int32 exit_code; bytes stdout; bytes stderr; } allowCheatcodes(address) +tryFfi(string[])(FfiResult) ffi(string[])(bytes) breakpoint(string) diff --git a/abi/src/bindings/hevm.rs b/abi/src/bindings/hevm.rs index 428f2ba91662..f282717d767e 100644 --- a/abi/src/bindings/hevm.rs +++ b/abi/src/bindings/hevm.rs @@ -11,7 +11,7 @@ pub use hevm::*; )] pub mod hevm { #[rustfmt::skip] - const __ABI: &str = "[\nstruct Log { bytes32[] topics; bytes data; }\nstruct Rpc { string name; string url; }\nstruct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; }\nstruct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; }\n\nallowCheatcodes(address)\n\nffi(string[])(bytes)\n\nbreakpoint(string)\nbreakpoint(string,bool)\n\nroll(uint256)\nwarp(uint256)\ndifficulty(uint256)\nprevrandao(bytes32)\nfee(uint256)\ncoinbase(address)\nstore(address,bytes32,bytes32)\nload(address,bytes32)(bytes32)\n\nsetEnv(string,string)\nenvBool(string)(bool)\nenvUint(string)(uint256)\nenvInt(string)(int256)\nenvAddress(string)(address)\nenvBytes32(string)(bytes32)\nenvString(string)(string)\nenvBytes(string)(bytes)\nenvBool(string,string)(bool[])\nenvUint(string,string)(uint256[])\nenvInt(string,string)(int256[])\nenvAddress(string,string)(address[])\nenvBytes32(string,string)(bytes32[])\nenvString(string,string)(string[])\nenvBytes(string,string)(bytes[])\nenvOr(string,bool)(bool)\nenvOr(string,uint256)(uint256)\nenvOr(string,int256)(int256)\nenvOr(string,address)(address)\nenvOr(string,bytes32)(bytes32)\nenvOr(string,string)(string)\nenvOr(string,bytes)(bytes)\nenvOr(string,string,bool[])(bool[])\nenvOr(string,string,uint256[])(uint256[])\nenvOr(string,string,int256[])(int256[])\nenvOr(string,string,address[])(address[])\nenvOr(string,string,bytes32[])(bytes32[])\nenvOr(string,string,string[])(string[])\nenvOr(string,string,bytes[])(bytes[])\n\naddr(uint256)(address)\nsign(uint256,bytes32)(uint8,bytes32,bytes32)\nderiveKey(string,uint32)(uint256)\nderiveKey(string,string,uint32)(uint256)\nrememberKey(uint256)(address)\n\nprank(address)\nprank(address,address)\nreadCallers()(uint256,address,address)\nstartPrank(address)\nstartPrank(address,address)\nstopPrank()\n\ndeal(address,uint256)\netch(address,bytes)\nexpectRevert()\nexpectRevert(bytes)\nexpectRevert(bytes4)\nrecord()\naccesses(address)(bytes32[],bytes32[])\n\nrecordLogs()\ngetRecordedLogs()(Log[])\n\nexpectEmit()\nexpectEmit(address)\nexpectEmit(bool,bool,bool,bool)\nexpectEmit(bool,bool,bool,bool,address)\n\nmockCall(address,bytes,bytes)\nmockCall(address,uint256,bytes,bytes)\nmockCallRevert(address,bytes,bytes)\nmockCallRevert(address,uint256,bytes,bytes)\nclearMockedCalls()\n\nexpectCall(address,bytes)\nexpectCall(address,bytes,uint64)\nexpectCall(address,uint256,bytes)\nexpectCall(address,uint256,bytes,uint64)\nexpectCall(address,uint256,uint64,bytes)\nexpectCall(address,uint256,uint64,bytes,uint64)\nexpectCallMinGas(address,uint256,uint64,bytes)\nexpectCallMinGas(address,uint256,uint64,bytes,uint64)\nexpectSafeMemory(uint64,uint64)\nexpectSafeMemoryCall(uint64,uint64)\n\ngetCode(string)\ngetDeployedCode(string)\nlabel(address,string)\ngetLabel(address)(string)\nassume(bool)\nsetNonce(address,uint64)\ngetNonce(address)\nresetNonce(address)\nsetNonceUnsafe(address,uint64)\nchainId(uint256)\ntxGasPrice(uint256)\n\nbroadcast()\nbroadcast(address)\nbroadcast(uint256)\nstartBroadcast()\nstartBroadcast(address)\nstartBroadcast(uint256)\nstopBroadcast()\n\nprojectRoot()(string)\nreadFile(string)(string)\nreadFileBinary(string)(bytes)\nwriteFile(string,string)\nwriteFileBinary(string,bytes)\nopenFile(string)\nreadLine(string)(string)\nwriteLine(string,string)\ncloseFile(string)\nremoveFile(string)\ncreateDir(string, bool)\nremoveDir(string, bool)\nreadDir(string)(DirEntry[])\nreadDir(string, uint64)(DirEntry[])\nreadDir(string, uint64, bool)(DirEntry[])\nreadLink(string)(string)\nfsMetadata(string)(FsMetadata)\n\ntoString(bytes)\ntoString(address)\ntoString(uint256)\ntoString(int256)\ntoString(bytes32)\ntoString(bool)\nparseBytes(string)(bytes)\nparseAddress(string)(address)\nparseUint(string)(uint256)\nparseInt(string)(int256)\nparseBytes32(string)(bytes32)\nparseBool(string)(bool)\n\nsnapshot()(uint256)\nrevertTo(uint256)(bool)\ncreateFork(string,uint256)(uint256)\ncreateFork(string,bytes32)(uint256)\ncreateFork(string)(uint256)\ncreateSelectFork(string,uint256)(uint256)\ncreateSelectFork(string,bytes32)(uint256)\ncreateSelectFork(string)(uint256)\nselectFork(uint256)\nactiveFork()(uint256)\ntransact(bytes32)\ntransact(uint256,bytes32)\nmakePersistent(address)\nmakePersistent(address,address)\nmakePersistent(address,address,address)\nmakePersistent(address[])\nrevokePersistent(address)\nrevokePersistent(address[])\nisPersistent(address)(bool)\nrollFork(uint256)\nrollFork(bytes32)\nrollFork(uint256,uint256)\nrollFork(uint256,bytes32)\nrpcUrl(string)(string)\nrpcUrls()(string[2][])\nrpcUrlStructs()(Rpc[])\n\nwriteJson(string, string)\nwriteJson(string, string, string)\nparseJson(string)(bytes)\nparseJson(string, string)(bytes)\nparseJsonUint(string, string)(uint256)\nparseJsonUintArray(string, string)(uint256[])\nparseJsonInt(string, string)(int256)\nparseJsonIntArray(string, string)(int256[])\nparseJsonString(string, string)(string)\nparseJsonStringArray(string, string)(string[])\nparseJsonAddress(string, string)(address)\nparseJsonAddressArray(string, string)(address[])\nparseJsonBool(string, string)(bool)\nparseJsonBoolArray(string, string)(bool[])\nparseJsonBytes(string, string)(bytes)\nparseJsonBytesArray(string, string)(bytes[])\nparseJsonBytes32(string, string)(bytes32)\nparseJsonBytes32Array(string, string)(bytes32[])\nserializeBool(string,string,bool)(string)\nserializeBool(string,string,bool[])(string)\nserializeUint(string,string,uint256)(string)\nserializeUint(string,string,uint256[])(string)\nserializeInt(string,string,int256)(string)\nserializeInt(string,string,int256[])(string)\nserializeAddress(string,string,address)(string)\nserializeAddress(string,string,address[])(string)\nserializeBytes32(string,string,bytes32)(string)\nserializeBytes32(string,string,bytes32[])(string)\nserializeString(string,string,string)(string)\nserializeString(string,string,string[])(string)\nserializeBytes(string,string,bytes)(string)\nserializeBytes(string,string,bytes[])(string)\n\npauseGasMetering()\nresumeGasMetering()\n\n]"; + const __ABI: &str = "[\nstruct Log { bytes32[] topics; bytes data; }\nstruct Rpc { string name; string url; }\nstruct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; }\nstruct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; }\nstruct FfiResult { int32 exit_code; bytes stdout; bytes stderr; }\n\nallowCheatcodes(address)\n\ntryFfi(string[])(FfiResult)\nffi(string[])(bytes)\n\nbreakpoint(string)\nbreakpoint(string,bool)\n\nroll(uint256)\nwarp(uint256)\ndifficulty(uint256)\nprevrandao(bytes32)\nfee(uint256)\ncoinbase(address)\nstore(address,bytes32,bytes32)\nload(address,bytes32)(bytes32)\n\nsetEnv(string,string)\nenvBool(string)(bool)\nenvUint(string)(uint256)\nenvInt(string)(int256)\nenvAddress(string)(address)\nenvBytes32(string)(bytes32)\nenvString(string)(string)\nenvBytes(string)(bytes)\nenvBool(string,string)(bool[])\nenvUint(string,string)(uint256[])\nenvInt(string,string)(int256[])\nenvAddress(string,string)(address[])\nenvBytes32(string,string)(bytes32[])\nenvString(string,string)(string[])\nenvBytes(string,string)(bytes[])\nenvOr(string,bool)(bool)\nenvOr(string,uint256)(uint256)\nenvOr(string,int256)(int256)\nenvOr(string,address)(address)\nenvOr(string,bytes32)(bytes32)\nenvOr(string,string)(string)\nenvOr(string,bytes)(bytes)\nenvOr(string,string,bool[])(bool[])\nenvOr(string,string,uint256[])(uint256[])\nenvOr(string,string,int256[])(int256[])\nenvOr(string,string,address[])(address[])\nenvOr(string,string,bytes32[])(bytes32[])\nenvOr(string,string,string[])(string[])\nenvOr(string,string,bytes[])(bytes[])\n\naddr(uint256)(address)\nsign(uint256,bytes32)(uint8,bytes32,bytes32)\nderiveKey(string,uint32)(uint256)\nderiveKey(string,string,uint32)(uint256)\nrememberKey(uint256)(address)\n\nprank(address)\nprank(address,address)\nreadCallers()(uint256,address,address)\nstartPrank(address)\nstartPrank(address,address)\nstopPrank()\n\ndeal(address,uint256)\netch(address,bytes)\nexpectRevert()\nexpectRevert(bytes)\nexpectRevert(bytes4)\nrecord()\naccesses(address)(bytes32[],bytes32[])\n\nrecordLogs()\ngetRecordedLogs()(Log[])\n\nexpectEmit()\nexpectEmit(address)\nexpectEmit(bool,bool,bool,bool)\nexpectEmit(bool,bool,bool,bool,address)\n\nmockCall(address,bytes,bytes)\nmockCall(address,uint256,bytes,bytes)\nmockCallRevert(address,bytes,bytes)\nmockCallRevert(address,uint256,bytes,bytes)\nclearMockedCalls()\n\nexpectCall(address,bytes)\nexpectCall(address,bytes,uint64)\nexpectCall(address,uint256,bytes)\nexpectCall(address,uint256,bytes,uint64)\nexpectCall(address,uint256,uint64,bytes)\nexpectCall(address,uint256,uint64,bytes,uint64)\nexpectCallMinGas(address,uint256,uint64,bytes)\nexpectCallMinGas(address,uint256,uint64,bytes,uint64)\nexpectSafeMemory(uint64,uint64)\nexpectSafeMemoryCall(uint64,uint64)\n\ngetCode(string)\ngetDeployedCode(string)\nlabel(address,string)\ngetLabel(address)(string)\nassume(bool)\nsetNonce(address,uint64)\ngetNonce(address)\nresetNonce(address)\nsetNonceUnsafe(address,uint64)\nchainId(uint256)\ntxGasPrice(uint256)\n\nbroadcast()\nbroadcast(address)\nbroadcast(uint256)\nstartBroadcast()\nstartBroadcast(address)\nstartBroadcast(uint256)\nstopBroadcast()\n\nprojectRoot()(string)\nreadFile(string)(string)\nreadFileBinary(string)(bytes)\nwriteFile(string,string)\nwriteFileBinary(string,bytes)\nopenFile(string)\nreadLine(string)(string)\nwriteLine(string,string)\ncloseFile(string)\nremoveFile(string)\ncreateDir(string, bool)\nremoveDir(string, bool)\nreadDir(string)(DirEntry[])\nreadDir(string, uint64)(DirEntry[])\nreadDir(string, uint64, bool)(DirEntry[])\nreadLink(string)(string)\nfsMetadata(string)(FsMetadata)\n\ntoString(bytes)\ntoString(address)\ntoString(uint256)\ntoString(int256)\ntoString(bytes32)\ntoString(bool)\nparseBytes(string)(bytes)\nparseAddress(string)(address)\nparseUint(string)(uint256)\nparseInt(string)(int256)\nparseBytes32(string)(bytes32)\nparseBool(string)(bool)\n\nsnapshot()(uint256)\nrevertTo(uint256)(bool)\ncreateFork(string,uint256)(uint256)\ncreateFork(string,bytes32)(uint256)\ncreateFork(string)(uint256)\ncreateSelectFork(string,uint256)(uint256)\ncreateSelectFork(string,bytes32)(uint256)\ncreateSelectFork(string)(uint256)\nselectFork(uint256)\nactiveFork()(uint256)\ntransact(bytes32)\ntransact(uint256,bytes32)\nmakePersistent(address)\nmakePersistent(address,address)\nmakePersistent(address,address,address)\nmakePersistent(address[])\nrevokePersistent(address)\nrevokePersistent(address[])\nisPersistent(address)(bool)\nrollFork(uint256)\nrollFork(bytes32)\nrollFork(uint256,uint256)\nrollFork(uint256,bytes32)\nrpcUrl(string)(string)\nrpcUrls()(string[2][])\nrpcUrlStructs()(Rpc[])\n\nwriteJson(string, string)\nwriteJson(string, string, string)\nparseJson(string)(bytes)\nparseJson(string, string)(bytes)\nparseJsonUint(string, string)(uint256)\nparseJsonUintArray(string, string)(uint256[])\nparseJsonInt(string, string)(int256)\nparseJsonIntArray(string, string)(int256[])\nparseJsonString(string, string)(string)\nparseJsonStringArray(string, string)(string[])\nparseJsonAddress(string, string)(address)\nparseJsonAddressArray(string, string)(address[])\nparseJsonBool(string, string)(bool)\nparseJsonBoolArray(string, string)(bool[])\nparseJsonBytes(string, string)(bytes)\nparseJsonBytesArray(string, string)(bytes[])\nparseJsonBytes32(string, string)(bytes32)\nparseJsonBytes32Array(string, string)(bytes32[])\nserializeBool(string,string,bool)(string)\nserializeBool(string,string,bool[])(string)\nserializeUint(string,string,uint256)(string)\nserializeUint(string,string,uint256[])(string)\nserializeInt(string,string,int256)(string)\nserializeInt(string,string,int256[])(string)\nserializeAddress(string,string,address)(string)\nserializeAddress(string,string,address[])(string)\nserializeBytes32(string,string,bytes32)(string)\nserializeBytes32(string,string,bytes32[])(string)\nserializeString(string,string,string)(string)\nserializeString(string,string,string[])(string)\nserializeBytes(string,string,bytes)(string)\nserializeBytes(string,string,bytes[])(string)\n\npauseGasMetering()\nresumeGasMetering()\n\n]"; ///The parsed human-readable ABI of the contract. pub static HEVM_ABI: ::ethers_contract::Lazy<::ethers_core::abi::Abi> = ::ethers_contract::Lazy::new(|| ::ethers_core::abi::parse_abi_str(__ABI).expect("ABI is always valid")); @@ -1895,6 +1895,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, @@ -4552,6 +4564,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, @@ -4833,6 +4858,7 @@ pub mod hevm { ToString5(ToString5Call), Transact0(Transact0Call), Transact1(Transact1Call), + TryFfi(TryFfiCall), TxGasPrice(TxGasPriceCall), Warp(WarpCall), WriteFile(WriteFileCall), @@ -5606,6 +5632,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)); @@ -6072,6 +6102,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) } @@ -6291,6 +6322,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), @@ -7201,6 +7233,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) @@ -8437,6 +8474,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, @@ -8455,6 +8506,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/evm/src/executor/inspector/cheatcodes/ext.rs b/evm/src/executor/inspector/cheatcodes/ext.rs index 1f35826b74bc..88c59a1e9559 100644 --- a/evm/src/executor/inspector/cheatcodes/ext.rs +++ b/evm/src/executor/inspector/cheatcodes/ext.rs @@ -5,6 +5,7 @@ use ethers::{ prelude::artifacts::CompactContractBytecode, types::*, }; +use foundry_abi::hevm::FfiResult; use foundry_common::{fmt::*, fs, get_artifact_path}; use foundry_config::fs_permissions::FsAccessKind; use hex::FromHex; @@ -46,6 +47,52 @@ fn ffi(state: &Cheatcodes, args: &[String]) -> Result { } } +/// 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 mut cmd = Command::new(&args[0]); + 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(); + + let encoded_stdout: Bytes = + if let Ok(hex) = hex::decode(trimmed_stdout.strip_prefix("0x").unwrap_or(trimmed_stdout)) { + abi::encode(&[Token::Bytes(hex)]).into() + } else { + trimmed_stdout.encode().into() + }; + + let trimmed_stderr = String::from_utf8(output.stderr)?; + let trimmed_stderr = trimmed_stderr.trim(); + + let encoded_stderr: Bytes = + if let Ok(hex) = hex::decode(trimmed_stderr.strip_prefix("0x").unwrap_or(trimmed_stderr)) { + abi::encode(&[Token::Bytes(hex)]).into() + } else { + trimmed_stderr.encode().into() + }; + + let ffi_result = FfiResult { exit_code, stdout: encoded_stdout, stderr: encoded_stderr }; + + Ok(ffi_result.encode().into()) +} + /// An enum which unifies the deserialization of Hardhat-style artifacts with Forge-style artifacts /// to get their bytecode. #[derive(Deserialize)] @@ -369,6 +416,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/Cheats.sol b/testdata/cheats/Cheats.sol index 2b1dfd5b0991..0790af35659c 100644 --- a/testdata/cheats/Cheats.sol +++ b/testdata/cheats/Cheats.sol @@ -44,6 +44,12 @@ interface Cheats { uint256 created; } + struct FfiResult { + int32 exit_code; + bytes stdout; + bytes stderr; + } + // Set block.timestamp (newTimestamp) function warp(uint256) external; @@ -87,6 +93,9 @@ interface Cheats { // 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; diff --git a/testdata/cheats/TryFfi.t.sol b/testdata/cheats/TryFfi.t.sol new file mode 100644 index 000000000000..901b482630a4 --- /dev/null +++ b/testdata/cheats/TryFfi.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.18; + +import "ds-test/test.sol"; +import "./Cheats.sol"; + +contract TryFfiTest is DSTest { + Cheats constant cheats = Cheats(HEVM_ADDRESS); + + function testTryFfi() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = + "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + + Cheats.FfiResult memory f = cheats.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] = "ls foo"; + + Cheats.FfiResult memory f = cheats.tryFfi(inputs); + // assert(f.exit_code != 0); + // assertEq(string(f.stderr), string("command not found")); + } +}