Skip to content

Commit

Permalink
feat(forge): Expect Revert cheatcode (gakonst#239)
Browse files Browse the repository at this point in the history
* add expectRevert

* fmt

* clippy

* update readme

* typos

* feat: bump ethers for new ethabi types (gakonst#238)

ref: gakonst#700

* custom revert test

* use ethabi to decode

* PR recs

* fmt

Co-authored-by: Georgios Konstantopoulos <[email protected]>
  • Loading branch information
brockelmore and gakonst authored Dec 17, 2021
1 parent 4ec2c63 commit 694801f
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 5 deletions.
68 changes: 66 additions & 2 deletions evm-adapters/src/sputnik/cheatcodes/cheatcode_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ pub static CONSOLE_ADDRESS: Lazy<Address> = Lazy::new(|| {
Address::from_slice(&hex::decode("000000000000000000636F6e736F6c652e6c6f67").unwrap())
});

/// For certain cheatcodes, we may internally change the status of the call, i.e. in
/// `expectRevert`. Solidity will see a successful call and attempt to abi.decode for the called
/// function. Therefore, we need to populate the return with dummy bytes such that the decode
/// doesn't fail
pub static DUMMY_OUTPUT: [u8; 320] = [0u8; 320];

/// Hooks on live EVM execution and forwards everything else to a Sputnik [`Handler`].
///
/// It allows:
Expand Down Expand Up @@ -402,6 +408,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> CheatcodeStackExecutor<'a, 'b, B, P>
_ => vec![],
};
}
HEVMCalls::ExpectRevert(inner) => {
if self.state().expected_revert.is_some() {
return evm_error(
"You must call another function prior to expecting a second revert.",
)
} else {
self.state_mut().expected_revert = Some(inner.0.to_vec());
}
}
HEVMCalls::Deal(inner) => {
let who = inner.0;
let value = inner.1;
Expand Down Expand Up @@ -740,12 +755,15 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a
// to the state.
// NB: This is very similar to how Optimism's custom intercept logic to "predeploys" work
// (e.g. with the StateManager)

let expected_revert = self.state_mut().expected_revert.take();

if code_address == *CHEATCODE_ADDRESS {
self.apply_cheatcode(input, transfer, target_gas)
} else if code_address == *CONSOLE_ADDRESS {
self.console_log(input)
} else {
self.call_inner(
let res = self.call_inner(
code_address,
transfer,
input,
Expand All @@ -754,7 +772,53 @@ impl<'a, 'b, B: Backend, P: PrecompileSet> Handler for CheatcodeStackExecutor<'a
true,
true,
context,
)
);

if let Some(expected_revert) = expected_revert {
let final_res = match res {
Capture::Exit((ExitReason::Revert(_e), data)) => {
if data.len() >= 4 && data[0..4] == [8, 195, 121, 160] {
// its a revert string
let decoded_data =
ethers::abi::decode(&[ethers::abi::ParamType::Bytes], &data[4..])
.expect("String error code, but not actual string");
let decoded_data = decoded_data[0]
.clone()
.into_bytes()
.expect("Can never fail because it is bytes");
if decoded_data == *expected_revert {
return Capture::Exit((
ExitReason::Succeed(ExitSucceed::Returned),
DUMMY_OUTPUT.to_vec(),
))
} else {
return evm_error(&*format!(
"Error != expected error: '{}' != '{}'",
String::from_utf8_lossy(&decoded_data[..]),
String::from_utf8_lossy(&expected_revert)
))
}
}

if data == *expected_revert {
Capture::Exit((
ExitReason::Succeed(ExitSucceed::Returned),
DUMMY_OUTPUT.to_vec(),
))
} else {
evm_error(&*format!(
"Error data != expected error data: 0x{} != 0x{}",
hex::encode(data),
hex::encode(expected_revert)
))
}
}
_ => evm_error("Expected revert call did not revert"),
};
final_res
} else {
res
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use ethers::types::{H160, H256, U256};
pub struct MemoryStackStateOwned<'config, B> {
pub backend: B,
pub substate: MemoryStackSubstate<'config>,
pub expected_revert: Option<Vec<u8>>,
}

impl<'config, B: Backend> MemoryStackStateOwned<'config, B> {
Expand All @@ -25,7 +26,7 @@ impl<'config, B: Backend> MemoryStackStateOwned<'config, B> {

impl<'config, B: Backend> MemoryStackStateOwned<'config, B> {
pub fn new(metadata: StackSubstateMetadata<'config>, backend: B) -> Self {
Self { backend, substate: MemoryStackSubstate::new(metadata) }
Self { backend, substate: MemoryStackSubstate::new(metadata), expected_revert: None }
}
}

Expand Down
1 change: 1 addition & 0 deletions evm-adapters/src/sputnik/cheatcodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ethers::contract::abigen!(
prank(address,address,bytes)(bool,bytes)
deal(address,uint256)
etch(address,bytes)
expectRevert(bytes)
]"#,
);
pub use hevm_mod::HEVMCalls;
Expand Down
77 changes: 76 additions & 1 deletion evm-adapters/testdata/CheatCodes.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Taken from:
// https://github.com/dapphub/dapptools/blob/e41b6cd9119bbd494aba1236838b859f2136696b/src/dapp-tests/pass/cheatCodes.sol
pragma solidity ^0.6.6;
pragma solidity ^0.8.4;
pragma experimental ABIEncoderV2;

import "./DsTest.sol";
Expand All @@ -26,6 +26,8 @@ interface Hevm {
function deal(address, uint256) external;
// Sets an address' code, (who, newCode)
function etch(address, bytes calldata) external;
// Expects an error on next call
function expectRevert(bytes calldata) external;
}

contract HasStorage {
Expand Down Expand Up @@ -209,6 +211,40 @@ contract CheatCodes is DSTest {
assertEq(string(newCode), string(n_code));
}

function testExpectRevert() public {
ExpectRevert target = new ExpectRevert();
hevm.expectRevert("Value too large");
target.stringErr(101);
target.stringErr(99);
}

function testExpectCustomRevert() public {
ExpectRevert target = new ExpectRevert();
bytes memory data = abi.encodePacked(bytes4(keccak256("InputTooLarge()")));
hevm.expectRevert(data);
target.customErr(101);
target.customErr(99);
}

function testCalleeExpectRevert() public {
ExpectRevert target = new ExpectRevert();
hevm.expectRevert("Value too largeCallee");
target.stringErrCall(101);
target.stringErrCall(99);
}

function testFailExpectRevert() public {
ExpectRevert target = new ExpectRevert();
hevm.expectRevert("Value too large");
target.stringErr2(101);
}

function testFailExpectRevert2() public {
ExpectRevert target = new ExpectRevert();
hevm.expectRevert("Value too large");
target.stringErr(99);
}

function getCode(address who) internal returns (bytes memory o_code) {
assembly {
// retrieve the size of the code, this needs assembly
Expand All @@ -226,6 +262,45 @@ contract CheatCodes is DSTest {
}
}


error InputTooLarge();
contract ExpectRevert {
function stringErrCall(uint256 a) public returns (uint256) {
ExpectRevertCallee callee = new ExpectRevertCallee();
uint256 amount = callee.stringErr(a);
return amount;
}

function stringErr(uint256 a) public returns (uint256) {
require(a < 100, "Value too large");
return a;
}

function stringErr2(uint256 a) public returns (uint256) {
require(a < 100, "Value too large2");
return a;
}

function customErr(uint256 a) public returns (uint256) {
if (a > 99) {
revert InputTooLarge();
}
return a;
}
}

contract ExpectRevertCallee {
function stringErr(uint256 a) public returns (uint256) {
require(a < 100, "Value too largeCallee");
return a;
}

function stringErr2(uint256 a) public returns (uint256) {
require(a < 100, "Value too large2Callee");
return a;
}
}

contract Prank is DSTest {
function checksOriginAndSender(string calldata input) external payable returns (string memory) {
string memory expectedInput = "And his name is JOHN CENA!";
Expand Down
53 changes: 52 additions & 1 deletion forge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,22 @@ which implements the following methods:

- `function prank(address from, address to, bytes calldata) (bool success,bytes retdata)`:
Performs a smart contract call as another address
- `function expectRevert(bytes calldata expectedError)`:
Tells the evm to expect that the next call reverts with specified error bytes.

The below example uses the `warp` cheatcode to override the timestamp:
The below example uses the `warp` cheatcode to override the timestamp & `expectRevert` to expect a specific revert string:

```solidity
interface Vm {
function warp(uint256 x) external;
function expectRevert(bytes calldata) external;
}
contract Foo {
function bar(uint256 a) public returns (uint256) {
require(a < 100, "My expected revert string");
return a;
}
}
contract MyTest {
Expand All @@ -151,6 +161,47 @@ contract MyTest {
vm.warp(100);
require(block.timestamp == 100);
}
function testBarExpectedRevert() public {
vm.expectRevert("My expected revert string");
// This would fail *if* we didnt expect revert. Since we expect the revert,
// it doesn't, unless the revert string is wrong.
foo.bar(101);
}
function testFailBar() public {
// this call would revert, causing this test to pass
foo.bar(101);
}
}
```

A full interface for all cheatcodes is here:
```solidity
interface Vm {
// Set block.timestamp (newTimestamp)
function warp(uint256) external;
// Set block.height (newHeight)
function roll(uint256) external;
// Loads a storage slot from an address (who, slot)
function load(address,bytes32) external returns (bytes32);
// Stores a value to an address' storage slot, (who, slot, value)
function store(address,bytes32,bytes32) external;
// Signs data, (privateKey, digest) => (r, v, s)
function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32);
// Gets address for a given private key, (privateKey) => (address)
function addr(uint256) external returns (address);
// Performs a foreign function call via terminal, (stringInputs) => (result)
function ffi(string[] calldata) external returns (bytes memory);
// Calls another contract with a specified `msg.sender`, (newSender, contract, input) => (success, returnData)
function prank(address, address, bytes calldata) external payable returns (bool, bytes memory);
// Sets an address' balance, (who, newBalance)
function deal(address, uint256) external;
// Sets an address' code, (who, newCode)
function etch(address, bytes calldata) external;
// Expects an error on next call
function expectRevert(bytes calldata) external;
}
```

## Future Features
Expand Down

0 comments on commit 694801f

Please sign in to comment.