Skip to content

Commit 1b2a239

Browse files
puma314Evalir
andauthored
feat: getLogs cheatcode (#5297)
* 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 <hi@enriqueortiz.dev>
1 parent 369fb72 commit 1b2a239

File tree

10 files changed

+549
-6
lines changed

10 files changed

+549
-6
lines changed

crates/abi/abi/HEVM.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
struct Log { bytes32[] topics; bytes data; }
22
struct Rpc { string name; string url; }
3+
struct EthGetLogs { address emitter; bytes32[] topics; bytes data; uint256 blockNumber; bytes32 transactionHash; uint256 transactionIndex; bytes32 blockHash; uint256 logIndex; bool removed; }
34
struct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bool isSymlink; }
45
struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; }
56
struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; }
@@ -185,6 +186,9 @@ rollFork(uint256,bytes32)
185186
rpcUrl(string)(string)
186187
rpcUrls()(string[2][])
187188
rpcUrlStructs()(Rpc[])
189+
eth_getLogs(uint256,uint256,address,bytes32[])(EthGetLogs[])
190+
rpc(string,string)(bytes)
191+
188192

189193
writeJson(string, string)
190194
writeJson(string, string, string)

crates/abi/src/bindings/hevm.rs

Lines changed: 249 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ fn get_env(key: &str, ty: ParamType, delim: Option<&str>, default: Option<String
202202
/// The function is designed to run recursively, so that in case of an object
203203
/// it will call itself to convert each of it's value and encode the whole as a
204204
/// Tuple
205-
fn value_to_token(value: &Value) -> Result<Token> {
205+
pub fn value_to_token(value: &Value) -> Result<Token> {
206206
match value {
207207
Value::Null => Ok(Token::FixedBytes(vec![0; 32])),
208208
Value::Bool(boolean) => Ok(Token::Bool(*boolean)),

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

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
use super::{fmt_err, Cheatcodes, Error, Result};
22
use crate::{
33
abi::HEVMCalls,
4-
executor::{backend::DatabaseExt, fork::CreateFork},
4+
executor::{
5+
backend::DatabaseExt, fork::CreateFork, inspector::cheatcodes::ext::value_to_token,
6+
},
7+
utils::{b160_to_h160, RuntimeOrHandle},
58
};
69
use ethers::{
7-
abi::AbiEncode,
10+
abi::{self, AbiEncode, Token, Tokenizable, Tokenize},
811
prelude::U256,
9-
types::{Bytes, H256},
12+
providers::Middleware,
13+
types::{Bytes, Filter, H256},
1014
};
15+
use foundry_abi::hevm::{EthGetLogsCall, RpcCall};
16+
use foundry_common::ProviderBuilder;
1117
use revm::EVMData;
18+
use serde_json::Value;
1219

1320
fn empty<T>(_: T) -> Bytes {
1421
Bytes::new()
@@ -140,6 +147,8 @@ pub fn apply<DB: DatabaseExt>(
140147
)
141148
.map(empty)
142149
.map_err(Into::into),
150+
HEVMCalls::EthGetLogs(inner) => eth_getlogs(data, inner),
151+
HEVMCalls::Rpc(inner) => rpc(data, inner),
143152
_ => return None,
144153
};
145154
Some(result)
@@ -246,3 +255,107 @@ fn create_fork_request<DB: DatabaseExt>(
246255
};
247256
Ok(fork)
248257
}
258+
259+
/// Retrieve the logs specified for the current fork.
260+
/// Equivalent to eth_getLogs but on a cheatcode.
261+
fn eth_getlogs<DB: DatabaseExt>(data: &EVMData<DB>, inner: &EthGetLogsCall) -> Result {
262+
let url = data.db.active_fork_url().ok_or(fmt_err!("No active fork url found"))?;
263+
if inner.0 > U256::from(u64::MAX) || inner.1 > U256::from(u64::MAX) {
264+
return Err(fmt_err!("Blocks in block range must be less than 2^64 - 1"))
265+
}
266+
// Cannot possibly have more than 4 topics in the topics array.
267+
if inner.3.len() > 4 {
268+
return Err(fmt_err!("Topics array must be less than 4 elements"))
269+
}
270+
271+
let provider = ProviderBuilder::new(url).build()?;
272+
let mut filter = Filter::new()
273+
.address(b160_to_h160(inner.2.into()))
274+
.from_block(inner.0.as_u64())
275+
.to_block(inner.1.as_u64());
276+
for (i, item) in inner.3.iter().enumerate() {
277+
match i {
278+
0 => filter = filter.topic0(U256::from(item)),
279+
1 => filter = filter.topic1(U256::from(item)),
280+
2 => filter = filter.topic2(U256::from(item)),
281+
3 => filter = filter.topic3(U256::from(item)),
282+
_ => return Err(fmt_err!("Topics array should be less than 4 elements")),
283+
};
284+
}
285+
286+
let logs = RuntimeOrHandle::new()
287+
.block_on(provider.get_logs(&filter))
288+
.map_err(|_| fmt_err!("Error in calling eth_getLogs"))?;
289+
290+
if logs.is_empty() {
291+
let empty: Bytes = abi::encode(&[Token::Array(vec![])]).into();
292+
return Ok(empty)
293+
}
294+
295+
let result = abi::encode(
296+
&logs
297+
.iter()
298+
.map(|entry| {
299+
Token::Tuple(vec![
300+
entry.address.into_token(),
301+
entry.topics.clone().into_token(),
302+
Token::Bytes(entry.data.to_vec()),
303+
entry
304+
.block_number
305+
.expect("eth_getLogs response should include block_number field")
306+
.as_u64()
307+
.into_token(),
308+
entry
309+
.transaction_hash
310+
.expect("eth_getLogs response should include transaction_hash field")
311+
.into_token(),
312+
entry
313+
.transaction_index
314+
.expect("eth_getLogs response should include transaction_index field")
315+
.as_u64()
316+
.into_token(),
317+
entry
318+
.block_hash
319+
.expect("eth_getLogs response should include block_hash field")
320+
.into_token(),
321+
entry
322+
.log_index
323+
.expect("eth_getLogs response should include log_index field")
324+
.into_token(),
325+
entry
326+
.removed
327+
.expect("eth_getLogs response should include removed field")
328+
.into_token(),
329+
])
330+
})
331+
.collect::<Vec<Token>>()
332+
.into_tokens(),
333+
)
334+
.into();
335+
Ok(result)
336+
}
337+
338+
fn rpc<DB: DatabaseExt>(data: &EVMData<DB>, inner: &RpcCall) -> Result {
339+
let url = data.db.active_fork_url().ok_or(fmt_err!("No active fork url found"))?;
340+
let provider = ProviderBuilder::new(url).build()?;
341+
342+
let method = inner.0.as_str();
343+
let params = inner.1.as_str();
344+
let params_json: Value = serde_json::from_str(params)?;
345+
346+
let result: Value = RuntimeOrHandle::new()
347+
.block_on(provider.request(method, params_json))
348+
.map_err(|err| fmt_err!("Error in calling {:?}: {:?}", method, err))?;
349+
350+
let result_as_tokens =
351+
value_to_token(&result).map_err(|err| fmt_err!("Failed to parse result: {err}"))?;
352+
353+
let abi_encoded: Vec<u8> = match result_as_tokens {
354+
Token::Tuple(vec) | Token::Array(vec) | Token::FixedArray(vec) => abi::encode(&vec),
355+
_ => {
356+
let vec = vec![result_as_tokens];
357+
abi::encode(&vec)
358+
}
359+
};
360+
Ok(abi_encoded.into())
361+
}

crates/forge/tests/it/fork.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
33
use crate::{
44
config::*,
5-
test_helpers::{filter::Filter, RE_PATH_SEPARATOR},
5+
test_helpers::{filter::Filter, PROJECT, RE_PATH_SEPARATOR},
66
};
77
use forge::result::SuiteResult;
8+
use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions};
89

910
/// Executes reverting fork test
1011
#[tokio::test(flavor = "multi_thread")]
@@ -36,9 +37,34 @@ async fn test_cheats_fork_revert() {
3637
/// Executes all non-reverting fork cheatcodes
3738
#[tokio::test(flavor = "multi_thread")]
3839
async fn test_cheats_fork() {
40+
let mut config = Config::with_root(PROJECT.root());
41+
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
42+
let runner = runner_with_config(config);
3943
let filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
4044
.exclude_tests(".*Revert");
41-
TestConfig::filter(filter).await.run().await;
45+
TestConfig::with_filter(runner.await, filter).run().await;
46+
}
47+
48+
/// Executes eth_getLogs cheatcode
49+
#[tokio::test(flavor = "multi_thread")]
50+
async fn test_get_logs_fork() {
51+
let mut config = Config::with_root(PROJECT.root());
52+
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
53+
let runner = runner_with_config(config);
54+
let filter = Filter::new("testEthGetLogs", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
55+
.exclude_tests(".*Revert");
56+
TestConfig::with_filter(runner.await, filter).run().await;
57+
}
58+
59+
/// Executes rpc cheatcode
60+
#[tokio::test(flavor = "multi_thread")]
61+
async fn test_rpc_fork() {
62+
let mut config = Config::with_root(PROJECT.root());
63+
config.fs_permissions = FsPermissions::new(vec![PathPermission::read("./fixtures")]);
64+
let runner = runner_with_config(config);
65+
let filter = Filter::new("testRpc", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}Fork"))
66+
.exclude_tests(".*Revert");
67+
TestConfig::with_filter(runner.await, filter).run().await;
4268
}
4369

4470
/// Tests that we can launch in forking mode

testdata/cheats/Fork2.t.sol

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity 0.8.18;
33

44
import "ds-test/test.sol";
5+
import "../logs/console.sol";
56
import "./Vm.sol";
67

78
struct MyStruct {
@@ -165,6 +166,67 @@ contract ForkTest is DSTest {
165166
// this will revert since `dummy` does not exists on the currently active fork
166167
string memory msg2 = dummy.hello();
167168
}
169+
170+
struct EthGetLogsJsonParseable {
171+
bytes32 blockHash;
172+
bytes blockNumber; // Should be uint256, but is returned from RPC in 0x... format
173+
bytes32 data; // Should be bytes, but in our particular example is bytes32
174+
address emitter;
175+
bytes logIndex; // Should be uint256, but is returned from RPC in 0x... format
176+
bool removed;
177+
bytes32[] topics;
178+
bytes32 transactionHash;
179+
bytes transactionIndex; // Should be uint256, but is returned from RPC in 0x... format
180+
}
181+
182+
function testEthGetLogs() public {
183+
vm.selectFork(mainnetFork);
184+
address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
185+
bytes32 withdrawalTopic = 0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65;
186+
uint256 blockNumber = 17623835;
187+
188+
string memory path = "fixtures/Rpc/eth_getLogs.json";
189+
string memory file = vm.readFile(path);
190+
bytes memory parsed = vm.parseJson(file);
191+
EthGetLogsJsonParseable[] memory fixtureLogs = abi.decode(parsed, (EthGetLogsJsonParseable[]));
192+
193+
bytes32[] memory topics = new bytes32[](1);
194+
topics[0] = withdrawalTopic;
195+
Vm.EthGetLogs[] memory logs = vm.eth_getLogs(blockNumber, blockNumber, weth, topics);
196+
assertEq(logs.length, 3);
197+
198+
for (uint256 i = 0; i < logs.length; i++) {
199+
Vm.EthGetLogs memory log = logs[i];
200+
assertEq(log.emitter, fixtureLogs[i].emitter);
201+
202+
string memory i_str;
203+
if (i == 0) i_str = "0";
204+
if (i == 1) i_str = "1";
205+
if (i == 2) i_str = "2";
206+
207+
assertEq(log.blockNumber, vm.parseJsonUint(file, string.concat("[", i_str, "].blockNumber")));
208+
assertEq(log.logIndex, vm.parseJsonUint(file, string.concat("[", i_str, "].logIndex")));
209+
assertEq(log.transactionIndex, vm.parseJsonUint(file, string.concat("[", i_str, "].transactionIndex")));
210+
211+
assertEq(log.blockHash, fixtureLogs[i].blockHash);
212+
assertEq(log.removed, fixtureLogs[i].removed);
213+
assertEq(log.transactionHash, fixtureLogs[i].transactionHash);
214+
215+
// In this specific example, the log.data is bytes32
216+
assertEq(bytes32(log.data), fixtureLogs[i].data);
217+
assertEq(log.topics.length, 2);
218+
assertEq(log.topics[0], withdrawalTopic);
219+
assertEq(log.topics[1], fixtureLogs[i].topics[1]);
220+
}
221+
}
222+
223+
function testRpc() public {
224+
vm.selectFork(mainnetFork);
225+
string memory path = "fixtures/Rpc/balance_params.json";
226+
string memory file = vm.readFile(path);
227+
bytes memory result = vm.rpc("eth_getBalance", file);
228+
assertEq(result, hex"65a221ccb194dc");
229+
}
168230
}
169231

170232
contract DummyContract {

testdata/cheats/Vm.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ interface Vm {
2424
string url;
2525
}
2626

27+
// Used in eth_getLogs
28+
struct EthGetLogs {
29+
address emitter;
30+
bytes32[] topics;
31+
bytes data;
32+
uint256 blockNumber;
33+
bytes32 transactionHash;
34+
uint256 transactionIndex;
35+
bytes32 blockHash;
36+
uint256 logIndex;
37+
bool removed;
38+
}
39+
2740
// Used in readDir
2841
struct DirEntry {
2942
string errorMessage;
@@ -559,6 +572,12 @@ interface Vm {
559572
/// Returns all rpc urls and their aliases as an array of structs
560573
function rpcUrlStructs() external returns (Rpc[] memory);
561574

575+
// Gets all the logs according to specified filter
576+
function eth_getLogs(uint256, uint256, address, bytes32[] memory) external returns (EthGetLogs[] memory);
577+
578+
// Generic rpc call function
579+
function rpc(string calldata, string calldata) external returns (bytes memory);
580+
562581
function parseJson(string calldata, string calldata) external returns (bytes memory);
563582

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

testdata/fixtures/Rpc/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Fixture Generation Instructions
2+
3+
### `eth_getLogs.json`
4+
5+
To generate this fixture, send a POST request to a Eth Mainnet (chainId = 1) RPC
6+
7+
```
8+
{
9+
"jsonrpc": "2.0",
10+
"method": "eth_getLogs",
11+
"id": "1",
12+
"params": [
13+
{
14+
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
15+
"fromBlock": "0x10CEB1B",
16+
"toBlock": "0x10CEB1B",
17+
"topics": [
18+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"
19+
]
20+
}
21+
]
22+
}
23+
```
24+
25+
Then you must change the `address` key to `emitter` because in Solidity, a struct's name cannot be `address` as that is a keyword.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["0x8D97689C9818892B700e27F316cc3E41e17fBeb9", "latest"]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
4+
"topics": [
5+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
6+
"0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"
7+
],
8+
"data": "0x0000000000000000000000000000000000000000000000000186faccfe3e2bcc",
9+
"blockNumber": "0x10ceb1b",
10+
"transactionHash": "0xa08f7b4aaa57cb2baec601ad96878d227ae3289a8dd48df98cce30c168588ce7",
11+
"transactionIndex": "0xc",
12+
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
13+
"logIndex": "0x42",
14+
"removed": false
15+
},
16+
{
17+
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
18+
"topics": [
19+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
20+
"0x0000000000000000000000002ec705d306b51e486b1bc0d6ebee708e0661add1"
21+
],
22+
"data": "0x000000000000000000000000000000000000000000000000004befaedcfaea00",
23+
"blockNumber": "0x10ceb1b",
24+
"transactionHash": "0x2cd5355bd917ec5c28194735ad539a4cb58e4b08815a038f6e2373290caeee1d",
25+
"transactionIndex": "0x11",
26+
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
27+
"logIndex": "0x56",
28+
"removed": false
29+
},
30+
{
31+
"emitter": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
32+
"topics": [
33+
"0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
34+
"0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d"
35+
],
36+
"data": "0x000000000000000000000000000000000000000000000000003432a29cd0ed22",
37+
"blockNumber": "0x10ceb1b",
38+
"transactionHash": "0x4e762d9a572084e0ec412ddf6c4e6d0b746b10e9714d4e786c13579e2e3c3187",
39+
"transactionIndex": "0x16",
40+
"blockHash": "0xe4299c95a140ddad351e9831cfb16c35cc0014e8cbd8465de2e5112847d70465",
41+
"logIndex": "0x68",
42+
"removed": false
43+
}
44+
]

0 commit comments

Comments
 (0)