Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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 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; }

Expand Down Expand Up @@ -175,6 +176,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 abi/src/bindings/hevm.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion evm/src/executor/inspector/cheatcodes/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,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 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)
}

fn eth_getlogs<DB: DatabaseExt>(data: &mut EVMData<DB>, inner: &EthGetLogsCall) -> Result {
Comment thread
Evalir marked this conversation as resolved.
Outdated
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"))
}
if inner.3.len() > 4 {
return Err(fmt_err!("Topics array must be less than 4 elements"))
}
Comment thread
Evalir marked this conversation as resolved.

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.len() == 0 {
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: &mut EVMData<DB>, inner: &RpcCall) -> Result {
Comment thread
Evalir marked this conversation as resolved.
Outdated
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_encoded = abi::encode(&vec);
}
_ => {
let vec = vec![result_as_tokens];
abi_encoded = abi::encode(&vec);
}
}
Ok(abi_encoded.into())
}
16 changes: 16 additions & 0 deletions forge/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ async fn test_cheats_fork() {
TestConfig::filter(filter).await.run().await;
}

/// Executes eth_getLogs cheatcode
#[tokio::test(flavor = "multi_thread")]
async fn test_get_logs_fork() {
let filter = Filter::new("testEthGetLogs", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
.exclude_tests(".*Revert");
TestConfig::filter(filter).await.run().await;
}

/// Executes rpc cheatcode
#[tokio::test(flavor = "multi_thread")]
async fn test_rpc_fork() {
let filter = Filter::new("testRpc", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
.exclude_tests(".*Revert");
TestConfig::filter(filter).await.run().await;
}

/// Tests that we can launch in forking mode
#[tokio::test(flavor = "multi_thread")]
async fn test_launch_fork() {
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 = "../testdata/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 = "../testdata/fixtures/Rpc/balance_params.json";
string memory file = vm.readFile(path);
bytes memory result = vm.rpc("eth_getBalance", file);
assertEq(result, hex'03202879715fd8');
}
Comment thread
Evalir marked this conversation as resolved.
}

contract DummyContract {
Expand Down
18 changes: 18 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 @@ -518,6 +531,11 @@ 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);

function rpc(string calldata, string calldata) external returns(bytes memory);
Comment thread
Evalir marked this conversation as resolved.
Outdated

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
}
]