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): add vm.lastCallGas cheatcode #7573

Merged
merged 35 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a737fb9
add `gasUsed and lastGasUsed methods to Vm
zerosnacks Apr 5, 2024
92d0f82
reorder
zerosnacks Apr 5, 2024
b9a0ed7
basic sketching of idea, planning to use Gas struct to deliver as muc…
zerosnacks Apr 5, 2024
2f72c83
add transformation
zerosnacks Apr 5, 2024
3cd8f13
to prevent recording gas by default, only enable after recordGas is e…
zerosnacks Apr 5, 2024
cc485cf
update struct layout, implementation builds, now connecting to cheatc…
zerosnacks Apr 5, 2024
06f38f5
fix cheatcodes
zerosnacks Apr 5, 2024
208c1e7
refactor to use simple u64 as Gas struct doesnt have a default
zerosnacks Apr 5, 2024
cb9d668
change from Gas struct to simple u64 as I ran into issues with cache …
zerosnacks Apr 5, 2024
98b9975
it appears cheatcodes are resolved before the actual function calls a…
zerosnacks Apr 5, 2024
4cc57d2
still not working
zerosnacks Apr 5, 2024
d68a0de
finally works, stupid me didnt realize i had to cross call frames
zerosnacks Apr 5, 2024
c63b29e
emit gas record
zerosnacks Apr 5, 2024
7ebbcd0
test convenient single field access
zerosnacks Apr 5, 2024
b846537
add gas record struct back
zerosnacks Apr 5, 2024
b2b1c4d
pass down isolate bool, only enable gas tracing if enabled
zerosnacks Apr 5, 2024
1736f2b
raise error if cheatcode is used outside of isolation mode
zerosnacks Apr 5, 2024
61644f9
mark as view
zerosnacks Apr 5, 2024
eea3102
show gas refund and memory expansion
zerosnacks Apr 5, 2024
179e450
improve example
zerosnacks Apr 5, 2024
49d33c8
add isolation test, currently does not run as expected
zerosnacks Apr 5, 2024
05a7a63
fix fmt
zerosnacks Apr 5, 2024
d1f8809
avoid formatting changes
zerosnacks Apr 5, 2024
52161dc
avoid commiting formatting changes, editor now configured correctly
zerosnacks Apr 5, 2024
710f971
lastGasUsed -> lastCallGas
zerosnacks Apr 8, 2024
368fe05
small name fix
zerosnacks Apr 8, 2024
9a01330
remove separate isolation profile, just configure on the runner
zerosnacks Apr 8, 2024
e1d19df
fix forge fmt
zerosnacks Apr 8, 2024
cbc713d
note on why path should never happen
zerosnacks Apr 8, 2024
0f83251
remove separate isolated param, inherit from config
zerosnacks Apr 8, 2024
0292b2c
add support for non-isolation mode
zerosnacks Apr 8, 2024
86ca22d
remove isolate standalone, create additional entry in cheats and docu…
zerosnacks Apr 8, 2024
999faec
improve tests, use asserts and add option to exclude contracts from t…
zerosnacks Apr 8, 2024
79a9782
typo, no need to define path exclusion of forks in isolated tests as …
zerosnacks Apr 8, 2024
e57066b
fix merge conflicts
zerosnacks Apr 8, 2024
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
51 changes: 51 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

1 change: 1 addition & 0 deletions crates/cheatcodes/spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl Cheatcodes<'static> {
Vm::ChainInfo::STRUCT.clone(),
Vm::AccountAccess::STRUCT.clone(),
Vm::StorageAccess::STRUCT.clone(),
Vm::Gas::STRUCT.clone(),
]),
enums: Cow::Owned(vec![
Vm::CallerMode::ENUM.clone(),
Expand Down
52 changes: 36 additions & 16 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ interface Vm {
address emitter;
}

struct Gas {
// The gas limit of the call.
uint64 gasLimit;
// The total gas used.
uint64 gasTotalUsed;
// The amount of gas used for memory expansion.
uint64 gasMemoryUsed;
// The amount of gas refunded.
int64 gasRefunded;
// The amount of gas remaining.
uint64 gasRemaining;
}

/// An RPC URL and its alias. Returned by `rpcUrlStructs`.
struct Rpc {
/// The alias of the RPC URL.
Expand Down Expand Up @@ -169,6 +182,22 @@ interface Vm {
uint256 chainId;
}

/// The storage accessed during an `AccountAccess`.
struct StorageAccess {
/// The account whose storage was accessed.
address account;
/// The slot that was accessed.
bytes32 slot;
/// If the access was a write.
bool isWrite;
/// The previous value of the slot.
bytes32 previousValue;
/// The new value of the slot.
bytes32 newValue;
/// If the access was reverted.
bool reverted;
}

/// The result of a `stopAndReturnStateDiff` call.
struct AccountAccess {
/// The chain and fork the access occurred.
Expand Down Expand Up @@ -207,22 +236,6 @@ interface Vm {
uint64 depth;
}

/// The storage accessed during an `AccountAccess`.
struct StorageAccess {
/// The account whose storage was accessed.
address account;
/// The slot that was accessed.
bytes32 slot;
/// If the access was a write.
bool isWrite;
/// The previous value of the slot.
bytes32 previousValue;
/// The new value of the slot.
bytes32 newValue;
/// If the access was reverted.
bool reverted;
}

// ======== EVM ========

/// Gets the address for a given private key.
Expand Down Expand Up @@ -594,6 +607,7 @@ interface Vm {
function getRecordedLogs() external returns (Log[] memory logs);

// -------- Gas Metering --------

// It's recommend to use the `noGasMetering` modifier included with forge-std, instead of
// using these functions directly.

Expand All @@ -605,6 +619,12 @@ interface Vm {
#[cheatcode(group = Evm, safety = Safe)]
function resumeGasMetering() external;

// -------- Gas Measurement --------

/// Gets the gas used in the last call.
#[cheatcode(group = Evm, safety = Safe)]
function lastCallGas() external view returns (Gas memory gas);

// ======== Test Assertions and Utilities ========

/// If the condition is false, discard this run's fuzz inputs and generate new ones.
Expand Down
4 changes: 4 additions & 0 deletions crates/cheatcodes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use std::{
pub struct CheatsConfig {
/// Whether the FFI cheatcode is enabled.
pub ffi: bool,
/// Whether the isolation of calls is enabled.
pub isolate: bool,
/// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
pub always_use_create_2_factory: bool,
/// Sets a timeout for vm.prompt cheatcodes
Expand Down Expand Up @@ -70,6 +72,7 @@ impl CheatsConfig {

Self {
ffi: evm_opts.ffi,
isolate: evm_opts.isolate,
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
prompt_timeout: Duration::from_secs(config.prompt_timeout),
rpc_storage_caching: config.rpc_storage_caching.clone(),
Expand Down Expand Up @@ -188,6 +191,7 @@ impl Default for CheatsConfig {
fn default() -> Self {
Self {
ffi: false,
isolate: false,
always_use_create_2_factory: false,
prompt_timeout: Duration::from_secs(120),
rpc_storage_caching: Default::default(),
Expand Down
18 changes: 18 additions & 0 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,24 @@ impl Cheatcode for resumeGasMeteringCall {
}
}

impl Cheatcode for lastCallGasCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
ensure!(
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
state.config.isolate,
"`lastCallGas` is only available in isolated mode (`--isolate`)"
);
ensure!(state.last_call_gas.is_some(), "`lastCallGas` is only available after a call");

Ok(state
.last_call_gas
.as_ref()
// This should never happen, but we need to handle it to satisfy the compiler.
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
.expect("`lastCallGas` is only available after a call")
.abi_encode())
}
}

impl Cheatcode for chainIdCall {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { newChainId } = self;
Expand Down
24 changes: 23 additions & 1 deletion crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ pub struct Cheatcodes {
/// Recorded logs
pub recorded_logs: Option<Vec<crate::Vm::Log>>,

/// Cache of the amount of gas used in previous call.
/// This is used by the `lastCallGas` cheatcode.
pub last_call_gas: Option<crate::Vm::Gas>,

/// Mocked calls
// **Note**: inner must a BTreeMap because of special `Ord` impl for `MockCallDataContext`
pub mocked_calls: HashMap<Address, BTreeMap<MockCallDataContext, MockCallReturnData>>,
Expand Down Expand Up @@ -1033,7 +1037,25 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
// Exit early for calls to cheatcodes as other logic is not relevant for cheatcode
// invocations
if cheatcode_call {
return outcome;
return outcome
}

if self.config.isolate {
// Record the gas usage of the call, this allows the `lastCallGas` cheatcode to
// retrieve the gas usage of the last call.
let gas = outcome.result.gas;
self.last_call_gas = Some(crate::Vm::Gas {
// The gas limit of the call.
gasLimit: gas.limit(),
// The total gas used.
gasTotalUsed: gas.spend(),
// The amount of gas used for memory expansion.
gasMemoryUsed: gas.memory(),
// The amount of gas refunded.
gasRefunded: gas.refunded(),
// The amount of gas remaining.
gasRemaining: gas.remaining(),
})
}

// If `startStateDiffRecording` has been called, update the `reverted` status of the
Expand Down
13 changes: 13 additions & 0 deletions crates/forge/tests/it/isolate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Isolation tests.

use crate::{config::*, test_helpers::TEST_DATA_DEFAULT};
use foundry_test_utils::Filter;

#[tokio::test(flavor = "multi_thread")]
async fn test_isolate_record_gas() {
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
let mut config = TEST_DATA_DEFAULT.config.clone();
config.isolate = true;
let runner = TEST_DATA_DEFAULT.runner_with_config(config);
let filter = Filter::new(".*", ".*", ".*isolation/RecordGas");
TestConfig::with_filter(runner, filter).run().await;
}
1 change: 1 addition & 0 deletions crates/forge/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ mod fs;
mod fuzz;
mod inline;
mod invariant;
mod isolate;
mod repros;
mod spec;
1 change: 1 addition & 0 deletions crates/forge/tests/it/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ impl ForgeTestData {
.with_cheats_config(CheatsConfig::new(&config, opts.clone(), Some(artifact_ids), None))
.sender(config.sender)
.with_test_options(self.test_opts.clone())
.enable_isolation(config.isolate)
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
.build(root, output, env, opts.clone())
.unwrap()
}
Expand Down
2 changes: 2 additions & 0 deletions testdata/cheats/Vm.sol

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

87 changes: 87 additions & 0 deletions testdata/default/isolation/RecordGas.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";

contract Target {
uint256 public slot0;

function expandMemory() public pure returns (uint256) {
uint256[] memory arr = new uint256[](1000);

for (uint256 i = 0; i < arr.length; i++) {
arr[i] = i;
}

return arr.length;
}

function set(uint256 value) public {
slot0 = value;
}

function reset() public {
slot0 = 0;
}
}

contract RecordGasTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);
Target public target;

function testRevertNoCachedLastCallGas() public {
vm.expectRevert();
Vm.Gas memory record = vm.lastCallGas();
}

function testRecordLastCallGas() public {
target = new Target();

_performCall();
_logGasRecord();

_performCall();
_logGasRecord();

_performCall();
_logGasRecord();
}

function testRecordGasMemory() public {
target = new Target();
target.expandMemory();
_logGasRecord();
}

function testRecordGasRefund() public {
target = new Target();
target.set(1);
target.reset();
_logGasRecord();
}

function testRecordGasSingleField() public {
_performCall();
_logGasTotalUsed();
}

function _performCall() internal returns (bool success) {
(success, ) = address(0).call("");
}

function _logGasTotalUsed() internal {
uint256 gasTotalUsed = vm.lastCallGas().gasTotalUsed;
emit log_named_uint("gasTotalUsed", gasTotalUsed);
}

function _logGasRecord() internal {
Vm.Gas memory record = vm.lastCallGas();
emit log_named_uint("gasLimit", record.gasLimit);
emit log_named_uint("gasTotalUsed", record.gasTotalUsed);
emit log_named_uint("gasMemoryUsed", record.gasMemoryUsed);
emit log_named_int("gasRefunded", record.gasRefunded);
emit log_named_uint("gasRemaining", record.gasRemaining);
emit log_string("");
}
}
Loading