Skip to content

Commit

Permalink
feat: getLogs cheatcode (#5297)
Browse files Browse the repository at this point in the history
* Initial implementation

* More comprehensive test

* Added TODOs

* Test passes

* Cleaning up PR

* Tests pass

* Cleaned up get_logs, starting to work on rpc

* eth get logs should be done. still working on rpc

* RPC test works with get_balance

* Formatting

* Removed pub

* Minor solidity fixes

* Remake public

* Cheats -> vm

* chore: docs

* chore: docs

* chore: clippy

* fmt

* chore: fix path

* chore: enable permissions

* enable permissions

---------

Co-authored-by: Enrique Ortiz <[email protected]>
  • Loading branch information
puma314 and Evalir authored Aug 21, 2023
1 parent 369fb72 commit 1b2a239
Show file tree
Hide file tree
Showing 10 changed files with 549 additions and 6 deletions.
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
}
]

0 comments on commit 1b2a239

Please sign in to comment.