Skip to content

Commit

Permalink
Add expectCall(address,uint256,uint64,bytes) cheatcode
Browse files Browse the repository at this point in the history
  • Loading branch information
clabby committed Feb 23, 2023
1 parent e049b0d commit e09e9a8
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 15 deletions.
1 change: 1 addition & 0 deletions evm/src/executor/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ ethers::contract::abigen!(
clearMockedCalls()
expectCall(address,bytes)
expectCall(address,uint256,bytes)
expectCall(address,uint256,uint64,bytes)
getCode(string)
getDeployedCode(string)
label(address,string)
Expand Down
36 changes: 26 additions & 10 deletions evm/src/executor/inspector/cheatcodes/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ pub struct ExpectedCallData {
pub calldata: Bytes,
/// The expected value sent in the call
pub value: Option<U256>,
/// The expected gas supplied to the call
pub gas: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -264,19 +266,33 @@ pub fn apply<DB: DatabaseExt>(
Ok(Bytes::new())
}
HEVMCalls::ExpectCall0(inner) => {
state
.expected_calls
.entry(inner.0)
.or_default()
.push(ExpectedCallData { calldata: inner.1.to_vec().into(), value: None });
state.expected_calls.entry(inner.0).or_default().push(ExpectedCallData {
calldata: inner.1.to_vec().into(),
value: None,
gas: None,
});
Ok(Bytes::new())
}
HEVMCalls::ExpectCall1(inner) => {
state
.expected_calls
.entry(inner.0)
.or_default()
.push(ExpectedCallData { calldata: inner.2.to_vec().into(), value: Some(inner.1) });
state.expected_calls.entry(inner.0).or_default().push(ExpectedCallData {
calldata: inner.2.to_vec().into(),
value: Some(inner.1),
gas: None,
});
Ok(Bytes::new())
}
HEVMCalls::ExpectCall2(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: Some(inner.2 + positive_value_cost_stipend),
});
Ok(Bytes::new())
}
HEVMCalls::MockCall0(inner) => {
Expand Down
8 changes: 5 additions & 3 deletions evm/src/executor/inspector/cheatcodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,8 @@ where
if let Some(found_match) = expecteds.iter().position(|expected| {
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.value.map(|value| value == call.transfer.value).unwrap_or(true) &&
expected.gas.map(|gas| gas == call.gas_limit).unwrap_or(true)
}) {
expecteds.remove(found_match);
}
Expand Down Expand Up @@ -584,10 +585,11 @@ 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].value.map(|v| format!(" and value {v}")).unwrap_or_default(),
expecteds[0].gas.map(|g| format!(" and gas {g}")).unwrap_or_default(),
)
.encode()
.into(),
Expand Down
2 changes: 2 additions & 0 deletions forge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ interface Hevm {
function expectCall(address,bytes calldata) external;
// Expect a call to an address with the specified msg.value and calldata
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;
// Fetches the contract bytecode from its artifact file
function getCode(string calldata) external returns (bytes memory);
// Label an address in test traces
Expand Down
4 changes: 2 additions & 2 deletions testdata/cache/solidity-files-cache.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,8 @@
}
},
"cheats/ExpectCall.t.sol": {
"lastModificationDate": 1661330493212,
"contentHash": "c6164b1cfe68bc3bfd2f7171f47fbc75",
"lastModificationDate": 1677141892508,
"contentHash": "b15f2d663c79b0e205cbf458f81540d6",
"sourceName": "cheats/ExpectCall.t.sol",
"solcConfig": {
"settings": {
Expand Down
3 changes: 3 additions & 0 deletions testdata/cheats/Cheats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ interface Cheats {
// Expect a call to an address with the specified msg.value and calldata
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;

// Gets the bytecode from an artifact file. Takes in the relative path to the json file
function getCode(string calldata) external returns (bytes memory);

Expand Down
32 changes: 32 additions & 0 deletions testdata/cheats/ExpectCall.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ contract NestedContract {
return inner.numberA() + inner.numberB();
}

function forwardPay() public payable returns (uint256) {
return inner.pay{ gas: 50_000, value: 1 }(1);
}

function addHardGasLimit() public view returns (uint256) {
return inner.add{ gas: 50_000 }(1, 1);
}

function hello() public pure returns (string memory) {
return "hi";
}
Expand Down Expand Up @@ -104,4 +112,28 @@ contract ExpectCallTest is DSTest {
cheats.expectCall(address(target), 3, abi.encodeWithSelector(target.pay.selector));
target.pay{value: 3}(100);
}

function testExpectCallWithValueAndGas() public {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);

cheats.expectCall(address(inner), 1, 50_000, abi.encodeWithSelector(inner.pay.selector, 1));
target.forwardPay{ value: 1 }();
}

function testExpectCallWithNoValueAndGas() public {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);

cheats.expectCall(address(inner), 0, 50_000, abi.encodeWithSelector(inner.add.selector, 1, 1));
target.addHardGasLimit();
}

function testFailExpectCallWithNoValueAndWrongGas() public {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);

cheats.expectCall(address(inner), 0, 25_000, abi.encodeWithSelector(inner.add.selector, 1, 1));
target.addHardGasLimit();
}
}

0 comments on commit e09e9a8

Please sign in to comment.