-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Expect revert from address #5430
Conversation
Thanks! I haven't reviewed but just two questions on the behavior:
|
Hey @mds1 about this
The current implementation does not care who originated the revert, the only thing that matters if the contract at address error SomeError();
contract B {
function reverts() external pure {
revert SomeError();
}
function doesNotRevert() external pure {}
}
contract A {
B b;
constructor(B _b) {
b = _b;
}
function revertFromB() external view {
b.reverts();
}
function revertFromA() external view {
try b.doesNotRevert() {
revert SomeError();
} catch {}
}
function revertRethrowingRevertFromB() external view {
try b.reverts() {} catch {
revert SomeError();
}
}
} In About
Currently |
Yep this is a great point and was my concern there too, so I'm good with the current behavior of "does not care who originated the revert". Thanks for the clear example
Makes sense, I'm also ok with this as long as @Evalir is also. Only comment would be to make sure we document this difference in the book |
Nice 🔥 I'll make it as clear as possible! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hefty PR so will review again a tad later—but I have some comments we can begin tackling. I think this is moving in the right direction! let's just make sure to document things properly
}; | ||
} | ||
|
||
fn stringify(data: &[u8]) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Address
or H160
has an utility to convert from a hex string? we should use this instead of having a custom func
.unwrap_or_else(|| format!("0x{}", hex::encode(data))) | ||
} | ||
|
||
fn actual_error_from_bytes(data: Bytes) -> Bytes { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use decode_revert
from utils here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with decode_revert
is that it parses the error to human readable string what I want is to preserve the error and then have the hex string version of it to compare it with the revert data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, wdyt about renaming this to something a bit more suitable like get_raw_err
?
} | ||
// Empty revert data | ||
else if address_reverts.contains("0x") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this only checks if the 0x
string exists, not if it's exactly 0x
which would mean invalid data. Was this how we did it previously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the logic without the address the data is compared as bytes and the .is_empty
is called. Here I use 0x
because the stringified version of an empty byte data is 0x
but if you think that there is a better way to achieve this I'd be welcome.
Also to add to the context of why I did it this way if I use bytes as the datatype for the parsed hashset clippy complains that I'm using a type with interior mutability as a key to get a value from the hash set so between 1) use #[allow(clippy::mutable_key_type)]
and 2) convert Bytes
to another type to get rid of the interior mutability. I chose the second one.
|
||
data | ||
} | ||
|
||
#[instrument(skip_all, fields(expected_revert, status, retdata = hex::encode(&retdata)))] | ||
pub fn handle_expect_revert( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's now add some docs now that this is a top lvl function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup np
retdata: Bytes, | ||
) -> Result<(Option<Address>, Bytes)> { | ||
trace!("handle expect revert"); | ||
|
||
ensure!(!matches!(status, return_ok!()), "Call did not revert as expected"); | ||
if let Some(expected_revert_address) = expected_revert_address { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to make this a little more rusty let's use match here—instead of having the revert_with_data
case be a fallthrough case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌🏼
let stringify = |data: &[u8]| { | ||
String::decode(data) | ||
.ok() | ||
.or_else(|| std::str::from_utf8(data).ok().map(ToOwned::to_owned)) | ||
.unwrap_or_else(|| format!("0x{}", hex::encode(data))) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah right, the stringify function is this extracted to an outside func?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, previously it was a closure defined inside the handle_expect_revert
so I exported it into its own thing and reused it in both expect_revert_...
functions.
@@ -124,6 +124,9 @@ pub struct Cheatcodes { | |||
/// Expected revert information | |||
pub expected_revert: Option<ExpectedRevert>, | |||
|
|||
/// Tracks the reverts that happen during evm execution if a there is an expected revert |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add a tad more docs on how this works—to document the type properly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oky 👌🏼
if self.expected_revert.is_some() && status == InstructionResult::Revert { | ||
self.revert_per_address | ||
.entry(b160_to_h160(call.contract)) | ||
.or_insert_with(HashSet::new) | ||
.insert(retdata.clone().into()); | ||
} | ||
|
||
// Handle expected reverts | ||
if let Some(expected_revert) = &self.expected_revert { | ||
if data.journaled_state.depth() <= expected_revert.depth { | ||
let expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); | ||
let reverts_by_address = std::mem::take(&mut self.revert_per_address); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so if I understand correctly here, we're:
- saving the last revert seen by address
- then, taking out the hashmap from memory and passing it to
handle_expect_revert
I'm unsure if this is the correct behavior—do we really wanna do std::mem::take
? as this will reset the hashmap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to reset the hashmap because after entering that if statement the only way to exit is after the handle_revert
func is called and that function will either be successful because the revert matched or not which will cause a revert in the evm. In both cases, we don't need the tracked reverts by address anymore so I think it is safe to reset the data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so do we need a hashmap in this case? because it then seems that this map can't hold more than 1 revert, or else we have a bug I believe because we're discarding reverts that we shouldn't. In this case we can probably just have something like an Option<ExpectedRevertWithAddress
> right?
// Handle expected reverts | ||
if let Some(expected_revert) = &self.expected_revert { | ||
if data.journaled_state.depth() <= expected_revert.depth { | ||
let expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); | ||
let reverts_by_address = std::mem::take(&mut self.revert_per_address); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
function expectRevert(bytes calldata, address) external; | ||
|
||
function expectRevert(bytes4, address) external; | ||
|
||
function expectRevert(address) external; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add some docs here 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some more nits, looking good!
data | ||
} | ||
|
||
/// Verifies that a revert occurred during EVM execution and that matches the user provided data if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beautiful
if self.expected_revert.is_some() && status == InstructionResult::Revert { | ||
self.revert_per_address | ||
.entry(b160_to_h160(call.contract)) | ||
.or_insert_with(HashSet::new) | ||
.insert(retdata.clone().into()); | ||
} | ||
|
||
// Handle expected reverts | ||
if let Some(expected_revert) = &self.expected_revert { | ||
if data.journaled_state.depth() <= expected_revert.depth { | ||
let expected_revert = std::mem::take(&mut self.expected_revert).unwrap(); | ||
let reverts_by_address = std::mem::take(&mut self.revert_per_address); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so do we need a hashmap in this case? because it then seems that this map can't hold more than 1 revert, or else we have a bug I believe because we're discarding reverts that we shouldn't. In this case we can probably just have something like an Option<ExpectedRevertWithAddress
> right?
.unwrap_or_else(|| format!("0x{}", hex::encode(data))) | ||
} | ||
|
||
fn actual_error_from_bytes(data: Bytes) -> Bytes { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, wdyt about renaming this to something a bit more suitable like get_raw_err
?
@Evalir @xeno097 One thing I'm realizing is that when using this version of So really I think we'll have two versions of
|
I agree this can be extremely useful. So to define a bit ux:
Should the order of the call of Please let me know what you think and if there is something else we should consider for this @Evalir @mds1 |
If it currently matters for |
@xeno097 should i give this another review round or should i wait? lmk, would love to get this over the line, just wanna make sure we contain the complexity of all of these variants of |
Hey @Evalir please wait. I'll be implementing the latest changes and updated logic over the weekend 🙏🏼 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not quite sure of the implementation 😅 it's getting a bit complicated and I'm really worried about the complexity of the expectRevert
cheatcode as a whole. Can't we slim this down somehow?
// If the first expected revert has already been matched it means that all the expected | ||
// reverts have been matched and we can safely clear the queque. | ||
else { | ||
state.matched_all_expected_reverts_with_address = true; | ||
state.expected_reverts_with_address.clear(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't really understand this—we're only really matching one event? So why not use an Option
here? The vecdequeue can only hold at most one event. because we're clearing it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are matching multiple reverts because the user can call expectRevert
with an address multiple times. So every time a revert happens it is compared against the first revert in the deque (because order matters).
If the revert matches with the first one then it is moved at the end of the queue, if not it is put in front once again until a matching revert is found. Following this cycle until the end of the call we could have the following scenarios:
- The first element of the deque has been matched which means that all the other reverts after it have been matched too. This is because at some point all the reverts that have been matched have been moved at the end of the deque and the first one that initially wasn't matched is brought back to the front.
- The first element of the dequeue has still not been matched which means that the expected revert was not thrown during EVM execution.
/// Tracks if all the expected reverts associated to an address have been matched to alter the | ||
/// root call return data from a revert to a simple return | ||
pub matched_all_expected_reverts_with_address: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we need a vecdequeue
here, can't we just map over it any see if all have been matched with an .any()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used that boolean because I want to explicitly distinguish the scenario where the queue was empty from the start from the one where all the reverts have been matched and the queue was cleared. I can't simply check if the deque is empty because if the user did not use expectRevert
with an address, modifying the return value here would be wrong.
|
||
// If `expected_reverts_with_address` is not empty then we did not match all the | ||
// expected reverts with an address | ||
if let Some(pending_expected_revert) = self.expected_reverts_with_address.pop_front() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: I don't really think we need a VecDequeue
here if we're only checking a single event. It's b etter to use an Option
😅 it's making the whole code a bit more complicated than it needs to be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm taking only the first element of the deque because it is the first one that did not match but there could be more after it.
@xeno097 will review this soon, sorry! could you please rebase? we recently flattened all crates into |
650c62e
to
32386ea
Compare
Guys, being completely honest this PR has gotten huge and it changes a lot of very sensitive things when it comes to
Sorry if this is a lot of work or derails the implementation, but this gets a tad hard to review with all the comments and we really do not wanna break this section of the code |
fde4eb3
to
72bd64d
Compare
@xeno097 just to understand—should i review this PR or are we gonna open two new ones splitting this feature? |
Hey @Evalir you can review this PR as it implements the cheat code as initially designed, I'll then open a new PR with the updated implementation to enable support for the new use case mentioned here #5430 (comment) |
Hey all—will close this as stale for now, we can come back to this later. We're undergoing a huge cheatcode alloy migration now and it'll be come a pain to migrate this—it will be better to start this from scratch once we're done @xeno097 |
Hey @Evalir no problem I'll pick this up once the migration is completed! |
reopening just so we don't lose it, because we still want this |
gm @xeno097 — we can come back to this as cheatcodes have been refactored. |
Hi @xeno097, thanks for your PR! Would be great to get this merged. Would you be interested in updating the PR? If not I can pick it up from here as well |
Hey @zerosnacks unfortunately at the moment I'm unable to continue working on this PR, I'd be pleased if you completed it for me thanks 🔥 |
Superseded by #8770 |
Motivation
As requested in #5299, this pr adds the following
expectRevert
cheat code overloads to verify that the specified address reverted:Solution
Track the reverts that happen during contract execution and store them in
reverts_by_address
.The process to check if an address reverted is as follows:
reverts_by_address
for the selected address:expected_revert
isNone
) it is a success.expected_revert_address
and if it is then it is a success, otherwise it is a failure.Note: This new logic does not enforce that the current call should end with a revert (but it stays the same for the old expectRevert only with data). The only thing that matters is that anywhere in the call stack
expected_revert_address
reverted. For example, if we have 2 contractsA
andB
, andB
callsA
which reverts, even ifB
catches the error but does not revert, if we are expectingA
to revert then the assertion will be successful.Ps: I'm super sorry for the super delayed pr 🙏🏼
Closes: #5299