Skip to content
Merged
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
2 changes: 2 additions & 0 deletions crates/abi/abi/HEVM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
101 changes: 101 additions & 0 deletions crates/abi/src/bindings/hevm.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions crates/evm/src/executor/inspector/cheatcodes/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -461,6 +505,13 @@ pub fn apply(state: &mut Cheatcodes, call: &HEVMCalls) -> Option<Result> {
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),
Expand Down
33 changes: 33 additions & 0 deletions testdata/cheats/TryFfi.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 "./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"));
}
}
9 changes: 9 additions & 0 deletions testdata/cheats/Vm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down