Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions evm/src/executor/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ abigen!(
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)

ffi(string[])(bytes)
tryFfi(string[])(FfiResult)

breakpoint(string)
breakpoint(string,bool)
Expand Down
58 changes: 57 additions & 1 deletion evm/src/executor/inspector/cheatcodes/ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use super::{bail, ensure, err, Cheatcodes, Result};
use crate::{abi::HEVMCalls, executor::inspector::cheatcodes::util};
use crate::{
abi::{FfiResult, HEVMCalls},
executor::inspector::cheatcodes::util,
};
use ethers::{
abi::{self, AbiEncode, JsonAbi, ParamType, Token},
prelude::artifacts::CompactContractBytecode,
Expand Down Expand Up @@ -46,6 +49,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| 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)]
Expand Down Expand Up @@ -372,6 +421,13 @@ pub fn apply(state: &mut Cheatcodes, call: &HEVMCalls) -> Option<Result> {
Err(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(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),
Expand Down
9 changes: 9 additions & 0 deletions testdata/cheats/Cheats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ interface Cheats {
uint256 created;
}

struct FfiResult {
int32 exit_code;
bytes stdout;
bytes stderr;
}

// Set block.timestamp (newTimestamp)
function warp(uint256) external;

Expand Down Expand Up @@ -78,6 +84,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;

Expand Down
33 changes: 33 additions & 0 deletions testdata/cheats/TryFfi.t.sol
Original file line number Diff line number Diff line change
@@ -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"));
}
}