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 33 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
13 changes: 13 additions & 0 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ impl Cheatcode for resumeGasMeteringCall {
}
}

impl Cheatcode for lastCallGasCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
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, as we ensure `last_call_gas` is `Some` above.
.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
22 changes: 21 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,9 +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
}

// 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
// previous call depth's recorded accesses, if any
if let Some(recorded_account_diffs_stack) = &mut self.recorded_account_diffs_stack {
Expand Down
22 changes: 18 additions & 4 deletions crates/forge/tests/it/cheats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ use crate::{
use foundry_config::{fs_permissions::PathPermission, FsPermissions};
use foundry_test_utils::Filter;

/// Executes all cheat code tests but not fork cheat codes
/// Executes all cheat code tests but not fork cheat codes or tests that require isolation mode
#[tokio::test(flavor = "multi_thread")]
async fn test_cheats_local() {
let mut filter =
Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}*")).exclude_paths("Fork");
let mut filter = Filter::new(".*", ".*", &format!(".*cheats{RE_PATH_SEPARATOR}*"))
.exclude_paths("Fork")
.exclude_contracts("Isolated");

// Exclude FFI tests on Windows because no `echo`, and file tests that expect certain file paths
// Exclude FFI tests on Windows because no `echo`, and file tests that expect certain file path
if cfg!(windows) {
filter = filter.exclude_tests("(Ffi|File|Line|Root)");
}
Expand All @@ -24,3 +25,16 @@ async fn test_cheats_local() {

TestConfig::with_filter(runner, filter).run().await;
}

/// Executes subset of all cheat code tests in isolation mode
#[tokio::test(flavor = "multi_thread")]
async fn test_cheats_local_isolated() {
let filter = Filter::new(".*", ".*(Isolated)", &format!(".*cheats{RE_PATH_SEPARATOR}*"))
.exclude_paths("Fork");

let mut config = TEST_DATA_DEFAULT.config.clone();
config.isolate = true;
let runner = TEST_DATA_DEFAULT.runner_with_config(config);

TestConfig::with_filter(runner, filter).run().await;
}
8 changes: 7 additions & 1 deletion crates/forge/tests/it/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,18 @@ impl ForgeTestData {
config.prompt_timeout = 0;

let root = self.project.root();
let opts = self.evm_opts.clone();
let mut opts = self.evm_opts.clone();

if config.isolate {
opts.isolate = true;
}

let env = opts.local_evm_env();
let output = self.output.clone();
let artifact_ids = output.artifact_ids().map(|(id, _)| id).collect();
self.base_runner()
.with_cheats_config(CheatsConfig::new(&config, opts.clone(), Some(artifact_ids), None))
.enable_isolation(opts.isolate)
.sender(config.sender)
.with_test_options(self.test_opts.clone())
.build(root, output, env, opts.clone())
Expand Down
17 changes: 17 additions & 0 deletions crates/test-utils/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct Filter {
contract_regex: Regex,
path_regex: Regex,
exclude_tests: Option<Regex>,
exclude_contracts: Option<Regex>,
exclude_paths: Option<Regex>,
}

Expand All @@ -21,6 +22,7 @@ impl Filter {
path_regex: Regex::new(path_pattern)
.unwrap_or_else(|_| panic!("Failed to parse path pattern: `{path_pattern}`")),
exclude_tests: None,
exclude_contracts: None,
exclude_paths: None,
}
}
Expand All @@ -41,6 +43,14 @@ impl Filter {
self
}

/// All contracts to also exclude
///
/// This is a workaround since regex does not support negative look aheads
pub fn exclude_contracts(mut self, pattern: &str) -> Self {
self.exclude_contracts = Some(Regex::new(pattern).unwrap());
self
}

/// All paths to also exclude
///
/// This is a workaround since regex does not support negative look aheads
Expand All @@ -55,6 +65,7 @@ impl Filter {
contract_regex: Regex::new(".*").unwrap(),
path_regex: Regex::new(".*").unwrap(),
exclude_tests: None,
exclude_contracts: None,
exclude_paths: None,
}
}
Expand All @@ -71,6 +82,12 @@ impl TestFilter for Filter {
}

fn matches_contract(&self, contract_name: &str) -> bool {
if let Some(exclude) = &self.exclude_contracts {
if exclude.is_match(contract_name) {
return false;
}
}

self.contract_regex.is_match(contract_name)
}

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.

Loading
Loading