Skip to content

Commit 47c040b

Browse files
authored
fix(cheatcodes): rework gas metering pause/resume (#8736)
* fix(cheatcodes): rework gas metering pause/resume * Changes after review: simplify paused gas recording * Move check out of meter_gas * Fix clippy * Add unit test for 4523
1 parent 2b1f8d6 commit 47c040b

File tree

15 files changed

+132
-89
lines changed

15 files changed

+132
-89
lines changed

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,8 +2676,8 @@ pub fn transaction_build(
26762676
WithOtherFields::new(transaction)
26772677
}
26782678

2679-
/// Prove a storage key's existence or nonexistence in the account's storage
2680-
/// trie.
2679+
/// Prove a storage key's existence or nonexistence in the account's storage trie.
2680+
///
26812681
/// `storage_key` is the hash of the desired storage key, meaning
26822682
/// this will only work correctly under a secure trie.
26832683
/// `storage_key` == keccak(key)

crates/anvil/src/eth/otterscan/api.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ pub fn mentions_address(trace: LocalizedTransactionTrace, address: Address) -> O
3939
}
4040
}
4141

42-
/// Converts the list of traces for a transaction into the expected Otterscan format, as
43-
/// specified in the [`ots_traceTransaction`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_tracetransaction) spec
42+
/// Converts the list of traces for a transaction into the expected Otterscan format.
43+
///
44+
/// Follows format specified in the [`ots_traceTransaction`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_tracetransaction) spec.
4445
pub fn batch_build_ots_traces(traces: Vec<LocalizedTransactionTrace>) -> Vec<TraceEntry> {
4546
traces
4647
.into_iter()

crates/cheatcodes/src/evm.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,16 @@ impl Cheatcode for getRecordedLogsCall {
209209
impl Cheatcode for pauseGasMeteringCall {
210210
fn apply(&self, state: &mut Cheatcodes) -> Result {
211211
let Self {} = self;
212-
if state.gas_metering.is_none() {
213-
state.gas_metering = Some(None);
214-
}
212+
state.pause_gas_metering = true;
215213
Ok(Default::default())
216214
}
217215
}
218216

219217
impl Cheatcode for resumeGasMeteringCall {
220218
fn apply(&self, state: &mut Cheatcodes) -> Result {
221219
let Self {} = self;
222-
state.gas_metering = None;
220+
state.pause_gas_metering = false;
221+
state.paused_frame_gas = vec![];
223222
Ok(Default::default())
224223
}
225224
}

crates/cheatcodes/src/inspector.rs

Lines changed: 26 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -293,18 +293,11 @@ pub struct Cheatcodes {
293293
/// All recorded ETH `deal`s.
294294
pub eth_deals: Vec<DealRecord>,
295295

296-
/// Holds the stored gas info for when we pause gas metering. It is an `Option<Option<..>>`
297-
/// because the `call` callback in an `Inspector` doesn't get access to
298-
/// the `revm::Interpreter` which holds the `revm::Gas` struct that
299-
/// we need to copy. So we convert it to a `Some(None)` in `apply_cheatcode`, and once we have
300-
/// the interpreter, we copy the gas struct. Then each time there is an execution of an
301-
/// operation, we reset the gas.
302-
pub gas_metering: Option<Option<Gas>>,
303-
304-
/// Holds stored gas info for when we pause gas metering, and we're entering/inside
305-
/// CREATE / CREATE2 frames. This is needed to make gas meter pausing work correctly when
306-
/// paused and creating new contracts.
307-
pub gas_metering_create: Option<Option<Gas>>,
296+
/// If true then gas metering is paused.
297+
pub pause_gas_metering: bool,
298+
299+
/// Stores frames paused gas.
300+
pub paused_frame_gas: Vec<Gas>,
308301

309302
/// Mapping slots.
310303
pub mapping_slots: Option<HashMap<Address, MappingSlots>>,
@@ -353,8 +346,8 @@ impl Cheatcodes {
353346
context: Default::default(),
354347
serialized_jsons: Default::default(),
355348
eth_deals: Default::default(),
356-
gas_metering: Default::default(),
357-
gas_metering_create: Default::default(),
349+
pause_gas_metering: false,
350+
paused_frame_gas: Default::default(),
358351
mapping_slots: Default::default(),
359352
pc: Default::default(),
360353
breakpoints: Default::default(),
@@ -940,7 +933,7 @@ impl Cheatcodes {
940933

941934
impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
942935
#[inline]
943-
fn initialize_interp(&mut self, _interpreter: &mut Interpreter, ecx: &mut EvmContext<DB>) {
936+
fn initialize_interp(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<DB>) {
944937
// When the first interpreter is initialized we've circumvented the balance and gas checks,
945938
// so we apply our actual block data with the correct fees and all.
946939
if let Some(block) = self.block.take() {
@@ -949,14 +942,19 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
949942
if let Some(gas_price) = self.gas_price.take() {
950943
ecx.env.tx.gas_price = gas_price;
951944
}
945+
946+
// Record gas for current frame if gas metering is paused.
947+
if self.pause_gas_metering {
948+
self.paused_frame_gas.push(interpreter.gas);
949+
}
952950
}
953951

954952
#[inline]
955953
fn step(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<DB>) {
956954
self.pc = interpreter.program_counter();
957955

958956
// `pauseGasMetering`: reset interpreter gas.
959-
if self.gas_metering.is_some() {
957+
if self.pause_gas_metering {
960958
self.meter_gas(interpreter);
961959
}
962960

@@ -1343,68 +1341,20 @@ impl<DB: DatabaseExt> InspectorExt<DB> for Cheatcodes {
13431341
impl Cheatcodes {
13441342
#[cold]
13451343
fn meter_gas(&mut self, interpreter: &mut Interpreter) {
1346-
match &self.gas_metering {
1347-
None => {}
1348-
// Need to store gas metering.
1349-
Some(None) => self.gas_metering = Some(Some(interpreter.gas)),
1350-
Some(Some(gas)) => {
1351-
match interpreter.current_opcode() {
1352-
opcode::CREATE | opcode::CREATE2 => {
1353-
// Set we're about to enter CREATE frame to meter its gas on first opcode
1354-
// inside it.
1355-
self.gas_metering_create = Some(None)
1356-
}
1357-
opcode::STOP => {
1358-
// Reset gas to value recorded when paused.
1359-
interpreter.gas = *gas;
1360-
self.gas_metering = None;
1361-
self.gas_metering_create = None;
1362-
}
1363-
opcode::RETURN | opcode::SELFDESTRUCT | opcode::REVERT => {
1364-
match &self.gas_metering_create {
1365-
None | Some(None) => {
1366-
// If we are ending current execution frame, we want to reset
1367-
// interpreter gas to the value of gas spent during frame, so only
1368-
// the consumed gas is erased.
1369-
// ref: https://github.com/bluealloy/revm/blob/2cb991091d32330cfe085320891737186947ce5a/crates/revm/src/evm_impl.rs#L190
1370-
//
1371-
// It would be nice if we had access to the interpreter in
1372-
// `call_end`, as we could just do this there instead.
1373-
interpreter.gas = Gas::new(interpreter.gas.spent());
1374-
1375-
// Make sure CREATE gas metering is resetted.
1376-
self.gas_metering_create = None
1377-
}
1378-
Some(Some(gas)) => {
1379-
// If this was CREATE frame, set correct gas limit. This is needed
1380-
// because CREATE opcodes deduct additional gas for code storage,
1381-
// and deducted amount is compared to gas limit. If we set this to
1382-
// 0, the CREATE would fail with out of gas.
1383-
//
1384-
// If we however set gas limit to the limit of outer frame, it would
1385-
// cause a panic after erasing gas cost post-create. Reason for this
1386-
// is pre-create REVM records `gas_limit - (gas_limit / 64)` as gas
1387-
// used, and erases costs by `remaining` gas post-create.
1388-
// gas used ref: https://github.com/bluealloy/revm/blob/2cb991091d32330cfe085320891737186947ce5a/crates/revm/src/instructions/host.rs#L254-L258
1389-
// post-create erase ref: https://github.com/bluealloy/revm/blob/2cb991091d32330cfe085320891737186947ce5a/crates/revm/src/instructions/host.rs#L279
1390-
interpreter.gas = Gas::new(gas.limit());
1391-
1392-
// Reset CREATE gas metering because we're about to exit its frame.
1393-
self.gas_metering_create = None
1394-
}
1395-
}
1396-
}
1397-
_ => {
1398-
// If just starting with CREATE opcodes, record its inner frame gas.
1399-
if self.gas_metering_create == Some(None) {
1400-
self.gas_metering_create = Some(Some(interpreter.gas))
1401-
}
1344+
if let Some(paused_gas) = self.paused_frame_gas.last() {
1345+
// Keep gas constant if paused.
1346+
interpreter.gas = *paused_gas;
1347+
} else {
1348+
// Record frame paused gas.
1349+
self.paused_frame_gas.push(interpreter.gas);
1350+
}
14021351

1403-
// Don't monitor gas changes, keep it constant.
1404-
interpreter.gas = *gas;
1405-
}
1406-
}
1352+
// Remove recorded gas if we exit frame.
1353+
match interpreter.current_opcode() {
1354+
opcode::STOP | opcode::RETURN | opcode::REVERT | opcode::SELFDESTRUCT => {
1355+
self.paused_frame_gas.pop();
14071356
}
1357+
_ => {}
14081358
}
14091359
}
14101360

crates/common/src/contracts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ unsafe fn count_different_bytes(a: &[u8], b: &[u8]) -> usize {
341341
sum
342342
}
343343

344+
/// Returns contract name for a given contract identifier.
345+
///
344346
/// Artifact/Contract identifier can take the following form:
345347
/// `<artifact file name>:<contract name>`, the `artifact file name` is the name of the json file of
346348
/// the contract's artifact and the contract name is the name of the solidity contract, like

crates/common/src/evm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use serde::Serialize;
1818
pub type Breakpoints = FxHashMap<char, (Address, usize)>;
1919

2020
/// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy.
21+
///
2122
/// All vars are opt-in, their default values are expected to be set by the
2223
/// [`foundry_config::Config`], and are always present ([`foundry_config::Config::default`])
2324
///

crates/common/src/provider/runtime_transport.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub enum RuntimeTransportError {
6262
InvalidJwt(String),
6363
}
6464

65+
/// Runtime transport that only connects on first request.
66+
///
6567
/// A runtime transport is a custom [alloy_transport::Transport] that only connects when the *first*
6668
/// request is made. When the first request is made, it will connect to the runtime using either an
6769
/// HTTP WebSocket, or IPC transport depending on the URL used.

crates/doc/src/parser/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ pub use item::{ParseItem, ParseSource};
2323
mod comment;
2424
pub use comment::{Comment, CommentTag, Comments, CommentsRef};
2525

26-
/// The documentation parser. This type implements a [Visitor] trait. While walking the parse tree,
27-
/// [Parser] will collect relevant source items and corresponding doc comments. The resulting
28-
/// [ParseItem]s can be accessed by calling [Parser::items].
26+
/// The documentation parser. This type implements a [Visitor] trait.
27+
///
28+
/// While walking the parse tree, [Parser] will collect relevant source items and corresponding
29+
/// doc comments. The resulting [ParseItem]s can be accessed by calling [Parser::items].
2930
#[derive(Debug, Default)]
3031
pub struct Parser {
3132
/// Initial comments from solang parser.

crates/doc/src/preprocessor/contract_inheritance.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{collections::HashMap, path::PathBuf};
77
pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inheritance");
88

99
/// The contract inheritance preprocessor.
10+
///
1011
/// It matches the documents with inner [`ParseSource::Contract`](crate::ParseSource) elements,
1112
/// iterates over their [Base](solang_parser::pt::Base)s and attempts
1213
/// to link them with the paths of the other contract documents.

crates/evm/core/src/backend/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub use snapshot::{BackendSnapshot, RevertSnapshotAction, StateSnapshot};
5050
type ForkDB = CacheDB<SharedBackend>;
5151

5252
/// Represents a numeric `ForkId` valid only for the existence of the `Backend`.
53+
///
5354
/// The difference between `ForkId` and `LocalForkId` is that `ForkId` tracks pairs of `endpoint +
5455
/// block` which can be reused by multiple tests, whereas the `LocalForkId` is unique within a test
5556
pub type LocalForkId = U256;

crates/evm/evm/src/executors/invariant/shrink.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub(crate) fn shrink_sequence(
137137
}
138138

139139
/// Checks if the given call sequence breaks the invariant.
140+
///
140141
/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
141142
/// persisted failures.
142143
/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was

crates/evm/fuzz/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ impl FuzzedCases {
299299
}
300300

301301
/// Fixtures to be used for fuzz tests.
302+
///
302303
/// The key represents name of the fuzzed parameter, value holds possible fuzzed values.
303304
/// For example, for a fixture function declared as
304305
/// `function fixture_sender() external returns (address[] memory senders)`

crates/evm/fuzz/src/strategies/param.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
1414
}
1515

1616
/// Given a parameter type and configured fixtures for param name, returns a strategy for generating
17-
/// values for that type. Fixtures can be currently generated for uint, int, address, bytes and
18-
/// string types and are defined for parameter name.
17+
/// values for that type.
1918
///
19+
/// Fixtures can be currently generated for uint, int, address, bytes and
20+
/// string types and are defined for parameter name.
2021
/// For example, fixtures for parameter `owner` of type `address` can be defined in a function with
2122
/// a `function fixture_owner() public returns (address[] memory)` signature.
2223
///

crates/fmt/src/visit.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ pub trait Visitor {
383383
}
384384
}
385385

386+
/// Visitable trait for [`solang_parser::pt`] types.
387+
///
386388
/// All [`solang_parser::pt`] types, such as [Statement], should implement the [Visitable] trait
387389
/// that accepts a trait [Visitor] implementation, which has various callback handles for Solidity
388390
/// Parse Tree nodes.

crates/forge/tests/cli/test_cmd.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,3 +1286,83 @@ contract ATest is Test {
12861286
...
12871287
"#]]);
12881288
});
1289+
1290+
// see https://github.com/foundry-rs/foundry/issues/5564
1291+
forgetest_init!(repro_5564, |prj, cmd| {
1292+
prj.wipe_contracts();
1293+
1294+
prj.add_test(
1295+
"ATest.t.sol",
1296+
r#"
1297+
import {Test} from "forge-std/Test.sol";
1298+
contract ATest is Test {
1299+
error MyError();
1300+
function testSelfMeteringRevert() public {
1301+
vm.pauseGasMetering();
1302+
vm.expectRevert(MyError.selector);
1303+
this.selfReverts();
1304+
}
1305+
function selfReverts() external {
1306+
vm.resumeGasMetering();
1307+
revert MyError();
1308+
}
1309+
}
1310+
"#,
1311+
)
1312+
.unwrap();
1313+
1314+
cmd.args(["test"]).with_no_redact().assert_success().stdout_eq(str![[r#"
1315+
...
1316+
[PASS] testSelfMeteringRevert() (gas: 3299)
1317+
...
1318+
"#]]);
1319+
});
1320+
1321+
// see https://github.com/foundry-rs/foundry/issues/4523
1322+
forgetest_init!(repro_4523, |prj, cmd| {
1323+
prj.wipe_contracts();
1324+
1325+
prj.add_test(
1326+
"ATest.t.sol",
1327+
r#"
1328+
import "forge-std/Test.sol";
1329+
contract ATest is Test {
1330+
mapping(uint => bytes32) map;
1331+
1332+
function test_GasMeter () public {
1333+
vm.pauseGasMetering();
1334+
for (uint i = 0; i < 10000; i++) {
1335+
map[i] = keccak256(abi.encode(i));
1336+
}
1337+
vm.resumeGasMetering();
1338+
1339+
for (uint i = 0; i < 10000; i++) {
1340+
map[i] = keccak256(abi.encode(i));
1341+
}
1342+
}
1343+
1344+
function test_GasLeft () public {
1345+
for (uint i = 0; i < 10000; i++) {
1346+
map[i] = keccak256(abi.encode(i));
1347+
}
1348+
1349+
uint start = gasleft();
1350+
for (uint i = 0; i < 10000; i++) {
1351+
map[i] = keccak256(abi.encode(i));
1352+
}
1353+
console2.log("Gas cost:", start - gasleft());
1354+
}
1355+
}
1356+
"#,
1357+
)
1358+
.unwrap();
1359+
1360+
cmd.args(["test", "-vvvv"]).with_no_redact().assert_success().stdout_eq(str![[r#"
1361+
...
1362+
Logs:
1363+
Gas cost: 5754479
1364+
...
1365+
[PASS] test_GasMeter() (gas: 5757137)
1366+
...
1367+
"#]]);
1368+
});

0 commit comments

Comments
 (0)