Skip to content
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

feat(forge): account access cheatcode accounts for extcode* and balance opcodes #6545

Merged
merged 3 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ interface Vm {
SelfDestruct,
/// Synthetic access indicating the current context has resumed after a previous sub-context (AccountAccess).
Resume,
/// The account's codesize was read.
Extcodesize,
/// The account's code was copied.
Extcodecopy,
/// The account's codehash was read.
Extcodehash,
}

/// An Ethereum log. Returned by `getRecordedLogs`.
Expand Down
50 changes: 50 additions & 0 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,56 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
data.journaled_state.depth(),
);
}
// Record account accesses via the EXT family of opcodes
opcode::EXTCODECOPY | opcode::EXTCODESIZE | opcode::EXTCODEHASH => {
let kind = match interpreter.current_opcode() {
opcode::EXTCODECOPY => crate::Vm::AccountAccessKind::Extcodecopy,
opcode::EXTCODESIZE => crate::Vm::AccountAccessKind::Extcodesize,
opcode::EXTCODEHASH => crate::Vm::AccountAccessKind::Extcodehash,
_ => unreachable!(),
};
let address = Address::from_word(B256::from(try_or_continue!(interpreter
.stack()
.peek(0))));
let balance;
let initialized;
if let Ok((acc, _)) = data.journaled_state.load_account(address, data.db) {
initialized = acc.info.exists();
balance = acc.info.balance;
} else {
initialized = false;
balance = U256::ZERO;
}
let account_access = crate::Vm::AccountAccess {
chainInfo: crate::Vm::ChainInfo {
forkId: data.db.active_fork_id().unwrap_or_default(),
chainId: U256::from(data.env.cfg.chain_id),
},
accessor: interpreter.contract().address,
account: address,
kind,
initialized,
oldBalance: balance,
newBalance: balance,
value: U256::ZERO,
data: vec![],
reverted: false,
deployedCode: vec![],
storageAccesses: vec![],
};
let access = AccountAccess {
access: account_access,
// use current depth; EXT* opcodes are not creating new contexts
depth: data.journaled_state.depth(),
};
// Record the EXT* call as an account access at the current depth
// (future storage accesses will be recorded in a new "Resume" context)
if let Some(last) = recorded_account_diffs_stack.last_mut() {
last.push(access);
} else {
recorded_account_diffs_stack.push(vec![access]);
}
}
_ => (),
}
}
Expand Down
80 changes: 70 additions & 10 deletions testdata/cheats/RecordAccountAccesses.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ contract NestedRunner {
}
}

/// Helper contract that uses all three EXT* opcodes on a given address
contract ExtChecker {
function checkExts(address a) external {
assembly {
let x := extcodesize(a)
let y := extcodehash(a)
extcodecopy(a, x, y, 0)
// sstore to check that storage accesses are correctly stored in a new access with a "resume" context
sstore(0, 1)
}
}
}

/// @notice Helper contract that writes to storage in a nested call
contract NestedStorer {
mapping(bytes32 key => uint256 value) slots;
Expand Down Expand Up @@ -196,13 +209,15 @@ contract RecordAccountAccessesTest is DSTest {
Create2or create2or;
StorageAccessor test1;
StorageAccessor test2;
ExtChecker extChecker;

function setUp() public {
runner = new NestedRunner();
nestedStorer = new NestedStorer();
create2or = new Create2or();
test1 = new StorageAccessor();
test2 = new StorageAccessor();
extChecker = new ExtChecker();
}

function testStorageAccessDelegateCall() public {
Expand All @@ -211,7 +226,7 @@ contract RecordAccountAccessesTest is DSTest {

cheats.startStateDiffRecording();
address(proxy).call(abi.encodeCall(StorageAccessor.read, bytes32(uint256(1234))));
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());

assertEq(called.length, 2, "incorrect length");

Expand Down Expand Up @@ -246,7 +261,7 @@ contract RecordAccountAccessesTest is DSTest {
two.write(bytes32(uint256(5678)), bytes32(uint256(123469)));
two.write(bytes32(uint256(5678)), bytes32(uint256(1234)));

Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 4, "incorrect length");

assertEq(called[0].storageAccesses.length, 1, "incorrect storage length");
Expand Down Expand Up @@ -317,7 +332,7 @@ contract RecordAccountAccessesTest is DSTest {
// contract calls to self in constructor
SelfCaller caller = new SelfCaller{value: 2 ether}("hello2 world2");

Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 6);
assertEq(
called[0],
Expand Down Expand Up @@ -430,7 +445,7 @@ contract RecordAccountAccessesTest is DSTest {
uint256 initBalance = address(this).balance;
cheats.startStateDiffRecording();
try this.revertingCall{value: 1 ether}(address(1234), "") {} catch {}
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 2);
assertEq(
called[0],
Expand Down Expand Up @@ -485,7 +500,7 @@ contract RecordAccountAccessesTest is DSTest {
/// @param shouldRevert Whether the first call should revert
function runNested(bool shouldRevert, bool expectFirstCall) public {
try runner.run{value: 1 ether}(shouldRevert) {} catch {}
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 7 + toUint(expectFirstCall), "incorrect length");

uint256 startingIndex = toUint(expectFirstCall);
Expand Down Expand Up @@ -737,7 +752,7 @@ contract RecordAccountAccessesTest is DSTest {
function testNestedStorage() public {
cheats.startStateDiffRecording();
nestedStorer.run();
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 3, "incorrect account access length");

assertEq(called[0].storageAccesses.length, 2, "incorrect run storage length");
Expand Down Expand Up @@ -858,7 +873,7 @@ contract RecordAccountAccessesTest is DSTest {
bytes memory creationCode = abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(true));
address hypotheticalStorer = deriveCreate2Address(address(create2or), bytes32(0), keccak256(creationCode));

Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 3, "incorrect account access length");
assertEq(toUint(called[0].kind), toUint(Vm.AccountAccessKind.Create), "incorrect kind");
assertEq(toUint(called[1].kind), toUint(Vm.AccountAccessKind.Call), "incorrect kind");
Expand Down Expand Up @@ -967,7 +982,7 @@ contract RecordAccountAccessesTest is DSTest {
try create2or.create2(bytes32(0), creationCode) {} catch {}
address hypotheticalAddress = deriveCreate2Address(address(create2or), bytes32(0), keccak256(creationCode));

Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 3, "incorrect length");
assertEq(
called[1],
Expand Down Expand Up @@ -1013,7 +1028,7 @@ contract RecordAccountAccessesTest is DSTest {
this.startRecordingFromLowerDepth();
address a = address(new SelfDestructor{value: 1 ether}(address(this)));
address b = address(new SelfDestructor{value: 1 ether}(address(bytes20("doesn't exist yet"))));
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 5, "incorrect length");
assertEq(
called[1],
Expand Down Expand Up @@ -1093,12 +1108,57 @@ contract RecordAccountAccessesTest is DSTest {
StorageAccessor a = new StorageAccessor();

cheats.stopBroadcast();
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
Vm.AccountAccess[] memory called = filterExtcodesizeForLegacyTests(cheats.stopAndReturnStateDiff());
assertEq(called.length, 1, "incorrect length");
assertEq(toUint(called[0].kind), toUint(Vm.AccountAccessKind.Create));
assertEq(called[0].account, address(a));
}

/// @notice Test that EXT* opcodes are recorded as account accesses
function testExtOpcodes() public {
cheats.startStateDiffRecording();
extChecker.checkExts(address(1234));
Vm.AccountAccess[] memory called = cheats.stopAndReturnStateDiff();
assertEq(called.length, 6, "incorrect length");
// initial solidity extcodesize check for calling extChecker
assertEq(toUint(called[0].kind), toUint(Vm.AccountAccessKind.Extcodesize));
// call to extChecker
assertEq(toUint(called[1].kind), toUint(Vm.AccountAccessKind.Call));
// extChecker checks
assertEq(toUint(called[2].kind), toUint(Vm.AccountAccessKind.Extcodesize));
assertEq(toUint(called[3].kind), toUint(Vm.AccountAccessKind.Extcodehash));
assertEq(toUint(called[4].kind), toUint(Vm.AccountAccessKind.Extcodecopy));
// resume of extChecker to hold SSTORE access
assertEq(toUint(called[5].kind), toUint(Vm.AccountAccessKind.Resume));
assertEq(called[5].storageAccesses.length, 1, "incorrect length");
}

/**
* @notice Filter out extcodesize account accesses for legacy tests written before
* EXT* opcodes were supported.
*/
function filterExtcodesizeForLegacyTests(Vm.AccountAccess[] memory inArr)
internal
pure
returns (Vm.AccountAccess[] memory out)
{
// allocate max length for out array
out = new Vm.AccountAccess[](inArr.length);
// track end size
uint256 size;
for (uint256 i = 0; i < inArr.length; ++i) {
// only append if not extcodesize
if (inArr[i].kind != Vm.AccountAccessKind.Extcodesize) {
out[size] = inArr[i];
++size;
}
}
// manually truncate out array
assembly {
mstore(out, size)
}
}

function startRecordingFromLowerDepth() external {
cheats.startStateDiffRecording();
assembly {
Expand Down
2 changes: 1 addition & 1 deletion testdata/cheats/Vm.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.