Skip to content

Commit 52ecd5c

Browse files
authored
feat(cheatcodes): tryFfi (rebased) (#5660)
* chore: add tryffi cheatcode to abi * feat: impl * chore: tests
1 parent fe1c1fc commit 52ecd5c

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

crates/abi/abi/HEVM.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ struct Rpc { string name; string url; }
33
struct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; }
44
struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; }
55
struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; }
6+
struct FfiResult { int32 exit_code; bytes stdout; bytes stderr; }
67

78
allowCheatcodes(address)
89

10+
tryFfi(string[])(FfiResult)
911
ffi(string[])(bytes)
1012

1113
breakpoint(string)

crates/abi/src/bindings/hevm.rs

Lines changed: 101 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/evm/src/executor/inspector/cheatcodes/ext.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,50 @@ use serde::Deserialize;
1111
use serde_json::Value;
1212
use std::{collections::BTreeMap, env, path::Path, process::Command};
1313

14+
/// Invokes a `Command` with the given args and returns the exit code, stdout, and stderr.
15+
///
16+
/// If stdout or stderr are valid hex, it returns the hex decoded value.
17+
fn try_ffi(state: &Cheatcodes, args: &[String]) -> Result {
18+
if args.is_empty() || args[0].is_empty() {
19+
bail!("Can't execute empty command");
20+
}
21+
let name = &args[0];
22+
let mut cmd = Command::new(name);
23+
if args.len() > 1 {
24+
cmd.args(&args[1..]);
25+
}
26+
27+
trace!(?args, "invoking try_ffi");
28+
29+
let output = cmd
30+
.current_dir(&state.config.root)
31+
.output()
32+
.map_err(|err| fmt_err!("Failed to execute command: {err}"))?;
33+
34+
let exit_code = output.status.code().unwrap_or(1);
35+
36+
let trimmed_stdout = String::from_utf8(output.stdout)?;
37+
let trimmed_stdout = trimmed_stdout.trim();
38+
39+
// The stdout might be encoded on valid hex, or it might just be a string,
40+
// so we need to determine which it is to avoid improperly encoding later.
41+
let encoded_stdout: Token =
42+
if let Ok(hex) = hex::decode(trimmed_stdout.strip_prefix("0x").unwrap_or(trimmed_stdout)) {
43+
Token::Bytes(hex)
44+
} else {
45+
Token::Bytes(trimmed_stdout.into())
46+
};
47+
48+
let res = abi::encode(&[Token::Tuple(vec![
49+
Token::Int(exit_code.into()),
50+
encoded_stdout,
51+
// We can grab the stderr output as-is.
52+
Token::Bytes(output.stderr),
53+
])]);
54+
55+
Ok(res.into())
56+
}
57+
1458
/// Invokes a `Command` with the given args and returns the abi encoded response
1559
///
1660
/// 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<Result> {
461505
Err(fmt_err!("FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts."))
462506
}
463507
}
508+
HEVMCalls::TryFfi(inner) => {
509+
if state.config.ffi {
510+
try_ffi(state, &inner.0)
511+
} else {
512+
Err(fmt_err!("FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts."))
513+
}
514+
}
464515
HEVMCalls::GetCode(inner) => get_code(state, &inner.0),
465516
HEVMCalls::GetDeployedCode(inner) => get_deployed_code(state, &inner.0),
466517
HEVMCalls::SetEnv(inner) => set_env(&inner.0, &inner.1),

testdata/cheats/TryFfi.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: Unlicense
2+
pragma solidity >=0.8.18;
3+
4+
import "ds-test/test.sol";
5+
import "./Vm.sol";
6+
7+
contract TryFfiTest is DSTest {
8+
Vm constant vm = Vm(HEVM_ADDRESS);
9+
10+
function testTryFfi() public {
11+
string[] memory inputs = new string[](3);
12+
inputs[0] = "bash";
13+
inputs[1] = "-c";
14+
inputs[2] =
15+
"echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000";
16+
17+
Vm.FfiResult memory f = vm.tryFfi(inputs);
18+
(string memory output) = abi.decode(f.stdout, (string));
19+
assertEq(output, "ffi works", "ffi failed");
20+
assertEq(f.exit_code, 0, "ffi failed");
21+
}
22+
23+
function testTryFfiFail() public {
24+
string[] memory inputs = new string[](3);
25+
inputs[0] = "bash";
26+
inputs[1] = "-c";
27+
inputs[2] = "quikmafs";
28+
29+
Vm.FfiResult memory f = vm.tryFfi(inputs);
30+
assert(f.exit_code != 0);
31+
assertEq(string(f.stderr), string("bash: quikmafs: command not found\n"));
32+
}
33+
}

testdata/cheats/Vm.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ interface Vm {
5252
uint256 privateKey;
5353
}
5454

55+
struct FfiResult {
56+
int32 exit_code;
57+
bytes stdout;
58+
bytes stderr;
59+
}
60+
5561
// Set block.timestamp (newTimestamp)
5662
function warp(uint256) external;
5763

@@ -116,6 +122,9 @@ interface Vm {
116122
// Performs a foreign function call via terminal, (stringInputs) => (result)
117123
function ffi(string[] calldata) external returns (bytes memory);
118124

125+
// Performs a foreign function call via terminal and returns the exit code, stdout, and stderr
126+
function tryFfi(string[] calldata) external returns (FfiResult memory);
127+
119128
// Set environment variables, (name, value)
120129
function setEnv(string calldata, string calldata) external;
121130

0 commit comments

Comments
 (0)