Skip to content

feat(forge): add cheatcodes vm.recordLogs() and vm.getRecordedLogs() #2161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 6, 2022
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
3 changes: 3 additions & 0 deletions evm/src/executor/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub static CHEATCODE_ADDRESS: Address = H160([
ethers::contract::abigen!(
HEVM,
r#"[
struct Log {bytes32[] topics; bytes data;}
roll(uint256)
warp(uint256)
fee(uint256)
Expand Down Expand Up @@ -50,6 +51,8 @@ ethers::contract::abigen!(
expectRevert(bytes4)
record()
accesses(address)(bytes32[],bytes32[])
recordLogs()
getRecordedLogs()(Log[])
expectEmit(bool,bool,bool,bool)
expectEmit(bool,bool,bool,bool,address)
mockCall(address,bytes,bytes)
Expand Down
37 changes: 36 additions & 1 deletion evm/src/executor/inspector/cheatcodes/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::Cheatcodes;
use crate::abi::HEVMCalls;
use bytes::Bytes;
use ethers::{
abi::{self, AbiEncode, Token, Tokenize},
abi::{self, AbiEncode, RawLog, Token, Tokenizable, Tokenize},
types::{Address, H256, U256},
utils::keccak256,
};
Expand Down Expand Up @@ -104,6 +104,36 @@ fn accesses(state: &mut Cheatcodes, address: Address) -> Bytes {
}
}

#[derive(Clone, Debug, Default)]
pub struct RecordedLogs {
pub entries: Vec<RawLog>,
}

fn start_record_logs(state: &mut Cheatcodes) {
state.recorded_logs = Some(Default::default());
}

fn get_recorded_logs(state: &mut Cheatcodes) -> Bytes {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Imo this should clear the state.recorded_logs vec (see reasoning below)

Copy link
Contributor Author

@bentobox19 bentobox19 Jul 5, 2022

Choose a reason for hiding this comment

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

Will clone state.recorded_logs and then call start_record_logs() to reset them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can also use std::mem::take instead

if let Some(recorded_logs) = state.recorded_logs.replace(Default::default()) {
ethers::abi::encode(
&recorded_logs
.entries
.iter()
.map(|entry| {
Token::Tuple(vec![
entry.topics.clone().into_token(),
Token::Bytes(entry.data.clone()),
])
})
.collect::<Vec<Token>>()
.into_tokens(),
)
.into()
} else {
ethers::abi::encode(&[Token::Array(vec![])]).into()
}
}

pub fn apply<DB: Database>(
state: &mut Cheatcodes,
data: &mut EVMData<'_, DB>,
Expand Down Expand Up @@ -197,6 +227,11 @@ pub fn apply<DB: Database>(
Ok(Bytes::new())
}
HEVMCalls::Accesses(inner) => Ok(accesses(state, inner.0)),
HEVMCalls::RecordLogs(_) => {
start_record_logs(state);
Ok(Bytes::new())
}
HEVMCalls::GetRecordedLogs(_) => Ok(get_recorded_logs(state)),
HEVMCalls::SetNonce(inner) => {
// TODO: this is probably not a good long-term solution since it might mess up the gas
// calculations
Expand Down
12 changes: 11 additions & 1 deletion evm/src/executor/inspector/cheatcodes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Cheatcodes related to the execution environment.
mod env;
pub use env::{Prank, RecordAccess};
pub use env::{Prank, RecordAccess, RecordedLogs};
/// Assertion helpers (such as `expectEmit`)
mod expect;
pub use expect::{ExpectedCallData, ExpectedEmit, ExpectedRevert, MockCallDataContext};
Expand Down Expand Up @@ -74,6 +74,9 @@ pub struct Cheatcodes {
/// Recorded storage reads and writes
pub accesses: Option<RecordAccess>,

/// Recorded logs
pub recorded_logs: Option<RecordedLogs>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be better to have a boolean indicating whether we are recording or not so you can do something like:

vm.recordLogs();
// some calls (lets say call a, b and c)
vm.getRecordedLogs(); // gets logs for calls a, b and c
// perform call d and call e
vm.getRecordedLogs(); // gets logs for calls d and e

Additionally I think a call to vm.getRecordedLogs should only return logs between the call to getRecordedLogs and the previous call to recordLogs or getRecordedLogs to allow for the pattern above. Currently this is the behavior:

vm.recordLogs();
// some calls (lets say call a, b and c)
vm.getRecordedLogs(); // gets logs for calls a, b and c
// perform call d and call e
vm.getRecordedLogs(); // gets logs for  calls a, b, c, d and e

To discard logs for the first 3 calls you would have to call recordLogs again to reset the buffer


/// Mocked calls
pub mocked_calls: BTreeMap<Address, BTreeMap<MockCallDataContext, Bytes>>,

Expand Down Expand Up @@ -319,6 +322,13 @@ where
address,
);
}

// Stores this log if `recordLogs` has been called
if let Some(storage_recorded_logs) = &mut self.recorded_logs {
storage_recorded_logs
.entries
.push(RawLog { topics: topics.to_vec(), data: data.to_vec() });
}
}

fn call_end(
Expand Down
6 changes: 6 additions & 0 deletions testdata/cheats/Cheats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pragma solidity >=0.8.0;

interface Cheats {
// This allows us to getRecordedLogs()
struct Log {bytes32[] topics; bytes data;}
// Set block.timestamp (newTimestamp)
function warp(uint256) external;
// Set block.height (newHeight)
Expand Down Expand Up @@ -60,6 +62,10 @@ interface Cheats {
function record() external;
// Gets all accessed reads and write slot from a recording session, for a given address
function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes);
// Record all the transaction logs
function recordLogs() external;
// Gets all the recorded logs
function getRecordedLogs() external returns (Log[] memory);
// Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData).
// Call this function, then emit an event, then call a function. Internally after the call, we check if
// logs were emitted in the expected order with the expected topics and data (as specified by the booleans).
Expand Down
257 changes: 257 additions & 0 deletions testdata/cheats/RecordLogs.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0;

import "ds-test/test.sol";
import "./Cheats.sol";

contract Emitter {
event LogAnonymous(
bytes data
) anonymous;

event LogTopic0(
bytes data
);

event LogTopic1(
uint256 indexed topic1,
bytes data
);

event LogTopic12(
uint256 indexed topic1,
uint256 indexed topic2,
bytes data
);

event LogTopic123(
uint256 indexed topic1,
uint256 indexed topic2,
uint256 indexed topic3,
bytes data
);

function emitAnonymousEvent(
bytes memory data
) public {
emit LogAnonymous(data);
}

function emitEvent(
bytes memory data
) public {
emit LogTopic0(data);
}

function emitEvent(
uint256 topic1,
bytes memory data
) public {
emit LogTopic1(topic1, data);
}

function emitEvent(
uint256 topic1,
uint256 topic2,
bytes memory data
) public {
emit LogTopic12(topic1, topic2, data);
}

function emitEvent(
uint256 topic1,
uint256 topic2,
uint256 topic3,
bytes memory data
) public {
emit LogTopic123(topic1, topic2, topic3, data);
}
}

contract Emitterv2 {
Emitter emitter = new Emitter();

function emitEvent(
uint256 topic1,
uint256 topic2,
uint256 topic3,
bytes memory data
) public {
emitter.emitEvent(topic1, topic2, topic3, data);
}
}

contract RecordLogsTest is DSTest {
Cheats constant cheats = Cheats(HEVM_ADDRESS);
Emitter emitter;
bytes32 internal seedTestData = keccak256(abi.encodePacked("Some data"));

// Used on testRecordOnEmitDifferentDepths()
event LogTopic(
uint256 indexed topic1,
bytes data
);

function setUp() public {
emitter = new Emitter();
}

function generateTestData(uint8 n) internal returns (bytes memory) {
bytes memory output = new bytes(n);

for (uint8 i = 0; i < n; i++) {
output[i] = seedTestData[ i % 32 ];
if ( i % 32 == 31 ) {
seedTestData = keccak256(abi.encodePacked(seedTestData));
}
}

return output;
}

function testRecordOffGetsNothing() public {
emitter.emitEvent(1, 2, 3, generateTestData(48));
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 0);
}

function testRecordOnNoLogs() public {
cheats.recordLogs();
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 0);
}

function testRecordOnSingleLog() public {
bytes memory testData = "Event Data in String";

cheats.recordLogs();
emitter.emitEvent(1, 2, 3, testData);
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 1);
assertEq(entries[0].topics.length, 4);
assertEq(entries[0].topics[0], keccak256("LogTopic123(uint256,uint256,uint256,bytes)"));
assertEq(entries[0].topics[1], bytes32(uint256(1)));
assertEq(entries[0].topics[2], bytes32(uint256(2)));
assertEq(entries[0].topics[3], bytes32(uint256(3)));
assertEq(abi.decode(entries[0].data, (string)), string(testData));
}

// TODO
// This crashes on decoding!
// The application panicked (crashed).
// Message: index out of bounds: the len is 0 but the index is 0
// Location: <local-dir>/evm/src/trace/decoder.rs:299
function NOtestRecordOnAnonymousEvent() public {
bytes memory testData = generateTestData(48);

cheats.recordLogs();
emitter.emitAnonymousEvent(testData);
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 1);
}

function testRecordOnSingleLogTopic0() public {
bytes memory testData = generateTestData(48);

cheats.recordLogs();
emitter.emitEvent(testData);
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 1);
assertEq(entries[0].topics.length, 1);
assertEq(entries[0].topics[0], keccak256("LogTopic0(bytes)"));
// While not a proper string, this conversion allows the comparison.
assertEq(abi.decode(entries[0].data, (string)), string(testData));
}

function testEmitRecordEmit() public {
bytes memory testData0 = generateTestData(32);
emitter.emitEvent(1, 2, testData0);

cheats.recordLogs();
bytes memory testData1 = generateTestData(16);
emitter.emitEvent(3, testData1);
Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 1);
assertEq(entries[0].topics.length, 2);
assertEq(entries[0].topics[0], keccak256("LogTopic1(uint256,bytes)"));
assertEq(entries[0].topics[1], bytes32(uint256(3)));
assertEq(abi.decode(entries[0].data, (string)), string(testData1));
}

function testRecordOnEmitDifferentDepths() public {
cheats.recordLogs();

bytes memory testData0 = generateTestData(16);
emit LogTopic(1, testData0);

bytes memory testData1 = generateTestData(20);
emitter.emitEvent(2, 3, testData1);

bytes memory testData2 = generateTestData(24);
Emitterv2 emitter2 = new Emitterv2();
emitter2.emitEvent(4, 5, 6, testData2);

Cheats.Log[] memory entries = cheats.getRecordedLogs();

assertEq(entries.length, 3);

assertEq(entries[0].topics.length, 2);
assertEq(entries[0].topics[0], keccak256("LogTopic(uint256,bytes)"));
assertEq(entries[0].topics[1], bytes32(uint256(1)));
assertEq(abi.decode(entries[0].data, (string)), string(testData0));

assertEq(entries[1].topics.length, 3);
assertEq(entries[1].topics[0], keccak256("LogTopic12(uint256,uint256,bytes)"));
assertEq(entries[1].topics[1], bytes32(uint256(2)));
assertEq(entries[1].topics[2], bytes32(uint256(3)));
assertEq(abi.decode(entries[1].data, (string)), string(testData1));

assertEq(entries[2].topics.length, 4);
assertEq(entries[2].topics[0], keccak256("LogTopic123(uint256,uint256,uint256,bytes)"));
assertEq(entries[2].topics[1], bytes32(uint256(4)));
assertEq(entries[2].topics[2], bytes32(uint256(5)));
assertEq(entries[2].topics[3], bytes32(uint256(6)));
assertEq(abi.decode(entries[2].data, (string)), string(testData2));
}

function testRecordsConsumednAsRead() public {
Cheats.Log[] memory entries;

emitter.emitEvent(1, generateTestData(16));

// hit record now
cheats.recordLogs();

entries = cheats.getRecordedLogs();
assertEq(entries.length, 0);

// emit after calling .getRecordedLogs()
emitter.emitEvent(2, 3, generateTestData(24));

entries = cheats.getRecordedLogs();
assertEq(entries.length, 1);
assertEq(entries[0].topics.length, 3);

// let's emit two more!
emitter.emitEvent(4, 5, 6, generateTestData(20));
emitter.emitEvent(generateTestData(32));

entries = cheats.getRecordedLogs();
assertEq(entries.length, 2);
assertEq(entries[0].topics.length, 4);
assertEq(entries[1].topics.length, 1);

// the last one
emitter.emitEvent(7, 8, 9, generateTestData(24));

entries = cheats.getRecordedLogs();
assertEq(entries.length, 1);
assertEq(entries[0].topics.length, 4);
}
}