From 873a59c758e449678a5977bd077ad44e8332dc20 Mon Sep 17 00:00:00 2001 From: clabby Date: Thu, 23 Feb 2023 12:57:02 -0500 Subject: [PATCH] Add `expectCallMinGas(address,uint256,uint64,bytes)` cheatcode --- evm/src/executor/abi/mod.rs | 1 + .../executor/inspector/cheatcodes/expect.rs | 20 ++++++++++++++++ evm/src/executor/inspector/cheatcodes/mod.rs | 9 +++++-- forge/README.md | 2 ++ testdata/cache/solidity-files-cache.json | 4 ++-- testdata/cheats/Cheats.sol | 3 +++ testdata/cheats/ExpectCall.t.sol | 24 +++++++++++++++++++ 7 files changed, 59 insertions(+), 4 deletions(-) diff --git a/evm/src/executor/abi/mod.rs b/evm/src/executor/abi/mod.rs index d97c450b603a..330b8140f467 100644 --- a/evm/src/executor/abi/mod.rs +++ b/evm/src/executor/abi/mod.rs @@ -82,6 +82,7 @@ ethers::contract::abigen!( expectCall(address,bytes) expectCall(address,uint256,bytes) expectCall(address,uint256,uint64,bytes) + expectCallMinGas(address,uint256,uint64,bytes) getCode(string) getDeployedCode(string) label(address,string) diff --git a/evm/src/executor/inspector/cheatcodes/expect.rs b/evm/src/executor/inspector/cheatcodes/expect.rs index 2af91458410c..dce458dd73e9 100644 --- a/evm/src/executor/inspector/cheatcodes/expect.rs +++ b/evm/src/executor/inspector/cheatcodes/expect.rs @@ -208,6 +208,8 @@ pub struct ExpectedCallData { pub value: Option, /// The expected gas supplied to the call pub gas: Option, + /// The expected *miniumum* gas supplied to the call + pub min_gas: Option, } #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -270,6 +272,7 @@ pub fn apply( calldata: inner.1.to_vec().into(), value: None, gas: None, + min_gas: None, }); Ok(Bytes::new()) } @@ -278,6 +281,7 @@ pub fn apply( calldata: inner.2.to_vec().into(), value: Some(inner.1), gas: None, + min_gas: None, }); Ok(Bytes::new()) } @@ -292,6 +296,22 @@ pub fn apply( calldata: inner.3.to_vec().into(), value: Some(value), gas: Some(inner.2 + positive_value_cost_stipend), + min_gas: None, + }); + Ok(Bytes::new()) + } + HEVMCalls::ExpectCallMinGas(inner) => { + let value = inner.1; + + // If the value of the transaction is non-zero, the EVM adds a call stipend of 2300 gas + // to ensure that the basic fallback function can be called. + let positive_value_cost_stipend = if value > U256::zero() { 2300 } else { 0 }; + + state.expected_calls.entry(inner.0).or_default().push(ExpectedCallData { + calldata: inner.3.to_vec().into(), + value: Some(value), + gas: None, + min_gas: Some(inner.2 + positive_value_cost_stipend), }); Ok(Bytes::new()) } diff --git a/evm/src/executor/inspector/cheatcodes/mod.rs b/evm/src/executor/inspector/cheatcodes/mod.rs index 1736aa8749b6..3299a951533f 100644 --- a/evm/src/executor/inspector/cheatcodes/mod.rs +++ b/evm/src/executor/inspector/cheatcodes/mod.rs @@ -403,7 +403,8 @@ where expected.calldata.len() <= call.input.len() && expected.calldata == call.input[..expected.calldata.len()] && expected.value.map(|value| value == call.transfer.value).unwrap_or(true) && - expected.gas.map(|gas| gas == call.gas_limit).unwrap_or(true) + expected.gas.map(|gas| gas == call.gas_limit).unwrap_or(true) && + expected.min_gas.map(|min_gas| min_gas <= call.gas_limit).unwrap_or(true) }) { expecteds.remove(found_match); } @@ -585,11 +586,15 @@ where Return::Revert, remaining_gas, format!( - "Expected a call to {:?} with data {}{}{}, but got none", + "Expected a call to {:?} with data {}{}{}{}, but got none", address, ethers::types::Bytes::from(expecteds[0].calldata.clone()), expecteds[0].value.map(|v| format!(" and value {v}")).unwrap_or_default(), expecteds[0].gas.map(|g| format!(" and gas {g}")).unwrap_or_default(), + expecteds[0] + .min_gas + .map(|g| format!(" and minimum gas {g}")) + .unwrap_or_default(), ) .encode() .into(), diff --git a/forge/README.md b/forge/README.md index 727c815a7c20..57e47555fdf9 100644 --- a/forge/README.md +++ b/forge/README.md @@ -319,6 +319,8 @@ interface Hevm { function expectCall(address,uint256,bytes calldata) external; // Expect a call to an address with the specified msg.value, gas, and calldata. function expectCall(address, uint256, uint64, bytes calldata) external; + // Expect a call to an address with the specified msg.value and calldata, and a *minimum* amount of gas. + function expectCallMinGas(address, uint256, uint64, bytes calldata) external; // Fetches the contract bytecode from its artifact file function getCode(string calldata) external returns (bytes memory); // Label an address in test traces diff --git a/testdata/cache/solidity-files-cache.json b/testdata/cache/solidity-files-cache.json index 87e58309fc5f..8e393e2789f2 100644 --- a/testdata/cache/solidity-files-cache.json +++ b/testdata/cache/solidity-files-cache.json @@ -509,8 +509,8 @@ } }, "cheats/ExpectCall.t.sol": { - "lastModificationDate": 1677141892508, - "contentHash": "b15f2d663c79b0e205cbf458f81540d6", + "lastModificationDate": 1677174707886, + "contentHash": "c91741f7cf3a014c0359d8a4c046bbf6", "sourceName": "cheats/ExpectCall.t.sol", "solcConfig": { "settings": { diff --git a/testdata/cheats/Cheats.sol b/testdata/cheats/Cheats.sol index 6beccd77506d..e5b39af1ff66 100644 --- a/testdata/cheats/Cheats.sol +++ b/testdata/cheats/Cheats.sol @@ -199,6 +199,9 @@ interface Cheats { // Expect a call to an address with the specified msg.value, gas, and calldata. function expectCall(address, uint256, uint64, bytes calldata) external; + // Expect a call to an address with the specified msg.value and calldata, and a *minimum* amount of gas. + function expectCallMinGas(address, uint256, uint64, bytes calldata) external; + // Gets the bytecode from an artifact file. Takes in the relative path to the json file function getCode(string calldata) external returns (bytes memory); diff --git a/testdata/cheats/ExpectCall.t.sol b/testdata/cheats/ExpectCall.t.sol index 40aba6b6b176..b9330b8db979 100644 --- a/testdata/cheats/ExpectCall.t.sol +++ b/testdata/cheats/ExpectCall.t.sol @@ -136,4 +136,28 @@ contract ExpectCallTest is DSTest { cheats.expectCall(address(inner), 0, 25_000, abi.encodeWithSelector(inner.add.selector, 1, 1)); target.addHardGasLimit(); } + + function testExpectCallWithValueAndMinGas() public { + Contract inner = new Contract(); + NestedContract target = new NestedContract(inner); + + cheats.expectCallMinGas(address(inner), 1, 50_000, abi.encodeWithSelector(inner.pay.selector, 1)); + target.forwardPay{value: 1}(); + } + + function testExpectCallWithNoValueAndMinGas() public { + Contract inner = new Contract(); + NestedContract target = new NestedContract(inner); + + cheats.expectCallMinGas(address(inner), 0, 25_000, abi.encodeWithSelector(inner.add.selector, 1, 1)); + target.addHardGasLimit(); + } + + function testFailExpectCallWithNoValueAndWrongMinGas() public { + Contract inner = new Contract(); + NestedContract target = new NestedContract(inner); + + cheats.expectCallMinGas(address(inner), 0, 50_001, abi.encodeWithSelector(inner.add.selector, 1, 1)); + target.addHardGasLimit(); + } }