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
4 changes: 4 additions & 0 deletions crates/abi/abi/HEVM.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
struct Log { bytes32[] topics; bytes data; }
struct Rpc { string name; string url; }
struct EthGetLogs { address emitter; bytes32[] topics; bytes data; uint256 blockNumber; bytes32 transactionHash; uint256 transactionIndex; bytes32 blockHash; uint256 logIndex; bool removed; }
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; }
Expand Down Expand Up @@ -185,6 +186,9 @@ rollFork(uint256,bytes32)
rpcUrl(string)(string)
rpcUrls()(string[2][])
rpcUrlStructs()(Rpc[])
eth_getLogs(uint256,uint256,address,bytes32[])(EthGetLogs[])
rpc(string,string)(bytes)


writeJson(string, string)
writeJson(string, string, string)
Expand Down
249 changes: 249 additions & 0 deletions crates/abi/src/bindings/hevm.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/evm/src/executor/inspector/cheatcodes/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ fn get_env(key: &str, ty: ParamType, delim: Option<&str>, default: Option<String
/// 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> {
pub fn value_to_token(value: &Value) -> Result<Token> {
match value {
Value::Null => Ok(Token::FixedBytes(vec![0; 32])),
Value::Bool(boolean) => Ok(Token::Bool(*boolean)),
Expand Down
119 changes: 116 additions & 3 deletions crates/evm/src/executor/inspector/cheatcodes/fork.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use super::{fmt_err, Cheatcodes, Error, Result};
use crate::{
abi::HEVMCalls,
executor::{backend::DatabaseExt, fork::CreateFork},
executor::{
backend::DatabaseExt, fork::CreateFork, inspector::cheatcodes::ext::value_to_token,
},
utils::{b160_to_h160, RuntimeOrHandle},
};
use ethers::{
abi::AbiEncode,
abi::{self, AbiEncode, Token, Tokenizable, Tokenize},
prelude::U256,
types::{Bytes, H256},
providers::Middleware,
types::{Bytes, Filter, H256},
};
use foundry_abi::hevm::{EthGetLogsCall, RpcCall};
use foundry_common::ProviderBuilder;
use revm::EVMData;
use serde_json::Value;

fn empty<T>(_: T) -> Bytes {
Bytes::new()
Expand Down Expand Up @@ -140,6 +147,8 @@ pub fn apply<DB: DatabaseExt>(
)
.map(empty)
.map_err(Into::into),
HEVMCalls::EthGetLogs(inner) => eth_getlogs(data, inner),
HEVMCalls::Rpc(inner) => rpc(data, inner),
_ => return None,
};
Some(result)
Expand Down Expand Up @@ -246,3 +255,107 @@ fn create_fork_request<DB: DatabaseExt>(
};
Ok(fork)
}

/// Retrieve the logs specified for the current fork.
/// Equivalent to eth_getLogs but on a cheatcode.
fn eth_getlogs<DB: DatabaseExt>(data: &EVMData<DB>, inner: &EthGetLogsCall) -> Result {
let url = data.db.active_fork_url().ok_or(fmt_err!("No active fork url found"))?;
if inner.0 > U256::from(u64::MAX) || inner.1 > U256::from(u64::MAX) {
return Err(fmt_err!("Blocks in block range must be less than 2^64 - 1"))
}
// Cannot possibly have more than 4 topics in the topics array.
if inner.3.len() > 4 {
return Err(fmt_err!("Topics array must be less than 4 elements"))
}

let provider = ProviderBuilder::new(url).build()?;
let mut filter = Filter::new()
.address(b160_to_h160(inner.2.into()))
.from_block(inner.0.as_u64())
.to_block(inner.1.as_u64());
for (i, item) in inner.3.iter().enumerate() {
match i {
0 => filter = filter.topic0(U256::from(item)),
1 => filter = filter.topic1(U256::from(item)),
2 => filter = filter.topic2(U256::from(item)),
3 => filter = filter.topic3(U256::from(item)),
_ => return Err(fmt_err!("Topics array should be less than 4 elements")),
};
}

let logs = RuntimeOrHandle::new()
.block_on(provider.get_logs(&filter))
.map_err(|_| fmt_err!("Error in calling eth_getLogs"))?;

if logs.is_empty() {
let empty: Bytes = abi::encode(&[Token::Array(vec![])]).into();
return Ok(empty)
}

let result = abi::encode(
&logs
.iter()
.map(|entry| {
Token::Tuple(vec![
entry.address.into_token(),
entry.topics.clone().into_token(),
Token::Bytes(entry.data.to_vec()),
entry
.block_number
.expect("eth_getLogs response should include block_number field")
.as_u64()
.into_token(),
entry
.transaction_hash
.expect("eth_getLogs response should include transaction_hash field")
.into_token(),
entry
.transaction_index
.expect("eth_getLogs response should include transaction_index field")
.as_u64()
.into_token(),
entry
.block_hash
.expect("eth_getLogs response should include block_hash field")
.into_token(),
entry
.log_index
.expect("eth_getLogs response should include log_index field")
.into_token(),
entry
.removed
.expect("eth_getLogs response should include removed field")
.into_token(),
])
})
.collect::<Vec<Token>>()
.into_tokens(),
)
.into();
Ok(result)
}

fn rpc<DB: DatabaseExt>(data: &EVMData<DB>, inner: &RpcCall) -> Result {
let url = data.db.active_fork_url().ok_or(fmt_err!("No active fork url found"))?;
let provider = ProviderBuilder::new(url).build()?;

let method = inner.0.as_str();
let params = inner.1.as_str();
let params_json: Value = serde_json::from_str(params)?;

let result: Value = RuntimeOrHandle::new()
.block_on(provider.request(method, params_json))
.map_err(|err| fmt_err!("Error in calling {:?}: {:?}", method, err))?;

let result_as_tokens =
value_to_token(&result).map_err(|err| fmt_err!("Failed to parse result: {err}"))?;

let abi_encoded: Vec<u8> = match result_as_tokens {
Token::Tuple(vec) | Token::Array(vec) | Token::FixedArray(vec) => abi::encode(&vec),
_ => {
let vec = vec![result_as_tokens];
abi::encode(&vec)
}
};
Ok(abi_encoded.into())
}
30 changes: 28 additions & 2 deletions crates/forge/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

use crate::{
config::*,
test_helpers::{filter::Filter, RE_PATH_SEPARATOR},
test_helpers::{filter::Filter, PROJECT, RE_PATH_SEPARATOR},
};
use forge::result::SuiteResult;
use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions};

/// Executes reverting fork test
#[tokio::test(flavor = "multi_thread")]
Expand Down Expand Up @@ -36,9 +37,34 @@ async fn test_cheats_fork_revert() {
/// Executes all non-reverting fork cheatcodes
#[tokio::test(flavor = "multi_thread")]
async fn test_cheats_fork() {
let mut config = Config::with_root(PROJECT.root());
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
let runner = runner_with_config(config);
let filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
.exclude_tests(".*Revert");
TestConfig::filter(filter).await.run().await;
TestConfig::with_filter(runner.await, filter).run().await;
}

/// Executes eth_getLogs cheatcode
#[tokio::test(flavor = "multi_thread")]
async fn test_get_logs_fork() {
let mut config = Config::with_root(PROJECT.root());
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
let runner = runner_with_config(config);
let filter = Filter::new("testEthGetLogs", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
.exclude_tests(".*Revert");
TestConfig::with_filter(runner.await, filter).run().await;
}

/// Executes rpc cheatcode
#[tokio::test(flavor = "multi_thread")]
async fn test_rpc_fork() {
let mut config = Config::with_root(PROJECT.root());
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
let runner = runner_with_config(config);
let filter = Filter::new("testRpc", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
.exclude_tests(".*Revert");
TestConfig::with_filter(runner.await, filter).run().await;
}

/// Tests that we can launch in forking mode
Expand Down
62 changes: 62 additions & 0 deletions testdata/cheats/Fork2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "../logs/console.sol";
import "./Vm.sol";

struct MyStruct {
Expand Down Expand Up @@ -165,6 +166,67 @@ contract ForkTest is DSTest {
// this will revert since `dummy` does not exists on the currently active fork
string memory msg2 = dummy.hello();
}

struct EthGetLogsJsonParseable {
bytes32 blockHash;
bytes blockNumber; // Should be uint256, but is returned from RPC in 0x... format
bytes32 data; // Should be bytes, but in our particular example is bytes32
address emitter;
bytes logIndex; // Should be uint256, but is returned from RPC in 0x... format
bool removed;
bytes32[] topics;
bytes32 transactionHash;
bytes transactionIndex; // Should be uint256, but is returned from RPC in 0x... format
}

function testEthGetLogs() public {
vm.selectFork(mainnetFork);
address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
bytes32 withdrawalTopic = 0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65;
uint256 blockNumber = 17623835;

string memory path = "fixtures/Rpc/eth_getLogs.json";
string memory file = vm.readFile(path);
bytes memory parsed = vm.parseJson(file);
EthGetLogsJsonParseable[] memory fixtureLogs = abi.decode(parsed, (EthGetLogsJsonParseable[]));

bytes32[] memory topics = new bytes32[](1);
topics[0] = withdrawalTopic;
Vm.EthGetLogs[] memory logs = vm.eth_getLogs(blockNumber, blockNumber, weth, topics);
assertEq(logs.length, 3);

for (uint256 i = 0; i < logs.length; i++) {
Vm.EthGetLogs memory log = logs[i];
assertEq(log.emitter, fixtureLogs[i].emitter);

string memory i_str;
if (i == 0) i_str = "0";
if (i == 1) i_str = "1";
if (i == 2) i_str = "2";

assertEq(log.blockNumber, vm.parseJsonUint(file, string.concat("[", i_str, "].blockNumber")));
assertEq(log.logIndex, vm.parseJsonUint(file, string.concat("[", i_str, "].logIndex")));
assertEq(log.transactionIndex, vm.parseJsonUint(file, string.concat("[", i_str, "].transactionIndex")));

assertEq(log.blockHash, fixtureLogs[i].blockHash);
assertEq(log.removed, fixtureLogs[i].removed);
assertEq(log.transactionHash, fixtureLogs[i].transactionHash);

// In this specific example, the log.data is bytes32
assertEq(bytes32(log.data), fixtureLogs[i].data);
assertEq(log.topics.length, 2);
assertEq(log.topics[0], withdrawalTopic);
assertEq(log.topics[1], fixtureLogs[i].topics[1]);
}
}

function testRpc() public {
vm.selectFork(mainnetFork);
string memory path = "fixtures/Rpc/balance_params.json";
string memory file = vm.readFile(path);
bytes memory result = vm.rpc("eth_getBalance", file);
assertEq(result, hex"65a221ccb194dc");
}
}

contract DummyContract {
Expand Down
19 changes: 19 additions & 0 deletions testdata/cheats/Vm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ interface Vm {
string url;
}

// Used in eth_getLogs
struct EthGetLogs {
address emitter;
bytes32[] topics;
bytes data;
uint256 blockNumber;
bytes32 transactionHash;
uint256 transactionIndex;
bytes32 blockHash;
uint256 logIndex;
bool removed;
}

// Used in readDir
struct DirEntry {
string errorMessage;
Expand Down Expand Up @@ -559,6 +572,12 @@ interface Vm {
/// Returns all rpc urls and their aliases as an array of structs
function rpcUrlStructs() external returns (Rpc[] memory);

// Gets all the logs according to specified filter
function eth_getLogs(uint256, uint256, address, bytes32[] memory) external returns (EthGetLogs[] memory);

// Generic rpc call function
function rpc(string calldata, string calldata) external returns (bytes memory);

function parseJson(string calldata, string calldata) external returns (bytes memory);

function parseJson(string calldata) external returns (bytes memory);
Expand Down
25 changes: 25 additions & 0 deletions testdata/fixtures/Rpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Fixture Generation Instructions

### `eth_getLogs.json`

To generate this fixture, send a POST request to a Eth Mainnet (chainId = 1) RPC

```
{
"jsonrpc": "2.0",
"method": "eth_getLogs",
"id": "1",
"params": [
{
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"fromBlock": "0x10CEB1B",
"toBlock": "0x10CEB1B",
"topics": [
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"
]
}
]
}
```

Then you must change the `address` key to `emitter` because in Solidity, a struct's name cannot be `address` as that is a keyword.
1 change: 1 addition & 0 deletions testdata/fixtures/Rpc/balance_params.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["0x8D97689C9818892B700e27F316cc3E41e17fBeb9", "latest"]
44 changes: 44 additions & 0 deletions testdata/fixtures/Rpc/eth_getLogs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"topics": [
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
"0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"
],
"data": "0x0000000000000000000000000000000000000000000000000186faccfe3e2bcc",
"blockNumber": "0x10ceb1b",
"transactionHash": "0xa08f7b4aaa57cb2baec601ad96878d227ae3289a8dd48df98cce30c168588ce7",
"transactionIndex": "0xc",
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
"logIndex": "0x42",
"removed": false
},
{
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"topics": [
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
"0x0000000000000000000000002ec705d306b51e486b1bc0d6ebee708e0661add1"
],
"data": "0x000000000000000000000000000000000000000000000000004befaedcfaea00",
"blockNumber": "0x10ceb1b",
"transactionHash": "0x2cd5355bd917ec5c28194735ad539a4cb58e4b08815a038f6e2373290caeee1d",
"transactionIndex": "0x11",
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
"logIndex": "0x56",
"removed": false
},
{
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"topics": [
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
"0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"
],
"data": "0x000000000000000000000000000000000000000000000000003432a29cd0ed22",
"blockNumber": "0x10ceb1b",
"transactionHash": "0x4e762d9a572084e0ec412ddf6c4e6d0b746b10e9714d4e786c13579e2e3c3187",
"transactionIndex": "0x16",
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
"logIndex": "0x68",
"removed": false
}
]