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
84 changes: 82 additions & 2 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ serde_json = "1.0.67"
serde = "1.0.130"
hex = "0.4.3"
ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "abigen"] }
jsonpath-rust = "0.1.5"

# Error handling
eyre = "0.6.5"
Expand Down
2 changes: 2 additions & 0 deletions evm/src/executor/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ ethers::contract::abigen!(
rollFork(uint256,uint256)
rpcUrl(string)(string)
rpcUrls()(string[2][])
parseJson(string, string)(bytes)
parseJson(string)(bytes)
]"#,
);
pub use hevm::{HEVMCalls, HEVM_ABI};
Expand Down
94 changes: 80 additions & 14 deletions evm/src/executor/inspector/cheatcodes/ext.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use crate::{
abi::HEVMCalls,
executor::inspector::{cheatcodes::util, Cheatcodes},
executor::inspector::{
Copy link
Collaborator

@mds1 mds1 Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(arbitrary comment location to start a thread)

Have not take a close look at this yet, but just wanted to comment on this:

Address inference from a string is not great. Could it misinterpret a hex-encoded bytes string of the same length? Is this an assumption we are willing to make?

I think ethers.js has a good convention for distinguishing hex strings that are numbers from hex strings that are bytes. From their docs:

For example, the string "0x1234" is 6 characters long (and in this case 6 bytes long). This is not equivalent to the array [ 0x12, 0x34 ], which is 2 bytes long.

So we can use the same convention for json parsing / script output writing. I quickly sanity checked based on that portion of their docs, but would love someone to double check that / confirm it's a good approach here

cheatcodes::util::{self},
Cheatcodes,
},
};
use bytes::Bytes;
use ethers::{
abi::{self, AbiEncode, ParamType, Token},
prelude::{artifacts::CompactContractBytecode, ProjectPathsConfig},
types::{Address, I256, U256},
types::*,
utils::hex::FromHex,
};
use foundry_common::fs;
use jsonpath_rust::JsonPathFinder;
use serde::Deserialize;
use serde_json::Value;
use std::{
env,
io::{BufRead, BufReader, Write},
Expand Down Expand Up @@ -127,15 +132,7 @@ fn set_env(key: &str, val: &str) -> Result<Bytes, Bytes> {
Ok(Bytes::new())
}
}

fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, Bytes> {
let val = env::var(key).map_err::<Bytes, _>(|e| e.to_string().encode().into())?;
let val = if let Some(d) = delim {
val.split(d).map(|v| v.trim()).collect()
} else {
vec![val.as_str()]
};

fn value_to_abi(val: Vec<String>, r#type: ParamType, is_array: bool) -> Result<Bytes, Bytes> {
let parse_bool = |v: &str| v.to_lowercase().parse::<bool>();
let parse_uint = |v: &str| {
if v.starts_with("0x") {
Expand Down Expand Up @@ -172,15 +169,26 @@ fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, B
})
.collect::<Result<Vec<Token>, String>>()
.map(|mut tokens| {
if delim.is_none() {
abi::encode(&[tokens.remove(0)]).into()
} else {
if is_array {
abi::encode(&[Token::Array(tokens)]).into()
} else {
abi::encode(&[tokens.remove(0)]).into()
}
})
.map_err(|e| e.into())
}

fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, Bytes> {
let val = env::var(key).map_err::<Bytes, _>(|e| e.to_string().encode().into())?;
let val = if let Some(d) = delim {
val.split(d).map(|v| v.trim().to_string()).collect()
} else {
vec![val]
};
let is_array: bool = delim.is_some();
value_to_abi(val, r#type, is_array)
}

fn full_path(state: &Cheatcodes, path: impl AsRef<Path>) -> PathBuf {
state.config.root.join(path)
}
Expand Down Expand Up @@ -262,6 +270,60 @@ fn remove_file(state: &mut Cheatcodes, path: impl AsRef<Path>) -> Result<Bytes,
Ok(Bytes::new())
}

/// Converts a serde_json::Value to an abi::Token
/// The function is designed to run recursively, so that in case of an object
/// it will call itself to convert each of it's value and encode the whole as a
/// Tuple
fn value_to_token(value: &Value) -> Result<Token, Token> {
if value.is_boolean() {
Ok(Token::Bool(value.as_bool().unwrap()))
} else if value.is_string() {
let val = value.as_str().unwrap();
// If it can decoded as an address, it's an address
if let Ok(addr) = H160::from_str(val) {
Ok(Token::Address(addr))
} else {
Ok(Token::String(val.to_owned()))
}
} else if value.is_u64() {
Ok(Token::Uint(value.as_u64().unwrap().into()))
} else if value.is_i64() {
Ok(Token::Int(value.as_i64().unwrap().into()))
} else if value.is_array() {
let arr = value.as_array().unwrap();
Ok(Token::Array(arr.iter().map(|val| value_to_token(val).unwrap()).collect::<Vec<Token>>()))
} else if value.is_object() {
let values = value
.as_object()
.unwrap()
.values()
.map(|val| value_to_token(val).unwrap())
.collect::<Vec<Token>>();
Ok(Token::Tuple(values))
} else {
Err(Token::String("Could not decode field".to_string()))
}
}
/// Parses a JSON and returns a single value, an array or an entire JSON object encoded as tuple.
/// As the JSON object is parsed serially, with the keys ordered alphabetically, they must be
/// deserialized in the same order. That means that the solidity `struct` should order it's fields
/// alphabetically and not by efficient packing or some other taxonomy.
fn parse_json(_state: &mut Cheatcodes, json: &str, key: &str) -> Result<Bytes, Bytes> {
let values: Value = JsonPathFinder::from_str(json, key)?.find();
// values is an array of items. Depending on the JsonPath key, they
// can be many or a single item. An item can be a single value or
// an entire JSON object.
let res = values
.as_array()
.ok_or_else(|| util::encode_error("JsonPath did not return an array"))?
.iter()
.map(|inner| value_to_token(inner).map_err(util::encode_error))
.collect::<Result<Vec<Token>, Bytes>>();
// encode the bytes as the 'bytes' solidity type
let abi_encoded = abi::encode(&[Token::Bytes(abi::encode(&res?))]);
Ok(abi_encoded.into())
}

pub fn apply(
state: &mut Cheatcodes,
ffi_enabled: bool,
Expand Down Expand Up @@ -299,6 +361,10 @@ pub fn apply(
HEVMCalls::WriteLine(inner) => write_line(state, &inner.0, &inner.1),
HEVMCalls::CloseFile(inner) => close_file(state, &inner.0),
HEVMCalls::RemoveFile(inner) => remove_file(state, &inner.0),
// If no key argument is passed, return the whole JSON object.
// "$" is the JSONPath key for the root of the object
HEVMCalls::ParseJson0(inner) => parse_json(state, &inner.0, "$"),
HEVMCalls::ParseJson1(inner) => parse_json(state, &inner.0, &inner.1),
_ => return None,
})
}
Expand Down
1 change: 1 addition & 0 deletions forge/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub fn base_runner() -> MultiContractRunnerBuilder {
pub fn runner() -> MultiContractRunner {
let mut config = Config::with_root(PROJECT.root());
config.rpc_endpoints = rpc_endpoints();
config.allow_paths.push(env!("CARGO_MANIFEST_DIR").into());

base_runner()
.with_cheats_config(CheatsConfig::new(&config, &EVM_OPTS))
Expand Down
2 changes: 2 additions & 0 deletions testdata/cheats/Cheats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,6 @@ interface Cheats {
function rpcUrl(string calldata) external returns(string memory);
/// Returns all rpc urls and their aliases `[alias, url][]`
function rpcUrls() external returns(string[2][] memory);
function parseJson(string calldata, string calldata) external returns(bytes memory);
function parseJson(string calldata) external returns(bytes memory);
}
Loading