Skip to content
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

feat(forge): readCallers cheatcode #4884

Merged
merged 22 commits into from
Jun 6, 2023

Conversation

xeno097
Copy link
Contributor

@xeno097 xeno097 commented May 5, 2023

Motivation

#4875

Solution

Implements a readPCallers cheat code that allows to read the current msg.sender and tx.origin addresses that will be used to send transactions in later calls

@xeno097 xeno097 changed the title Read prank cheatcode feat(forge): readPrank cheatcode May 5, 2023
@xeno097 xeno097 changed the title feat(forge): readPrank cheatcode feat(forge): readPrank cheatcode May 5, 2023
@Evalir Evalir requested review from mattsse and Evalir May 5, 2023 15:31
Copy link
Member

@Evalir Evalir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lgtm. Looks like it needs linting.

Would appreciate some followup PRs for the book and forge-std! Deferring to Matt for final approval & merge.

@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

This lgtm. Looks like it needs linting.

Would appreciate some followup PRs for the book and forge-std! Deferring to Matt for final approval & merge.

On it 😉

@mds1
Copy link
Collaborator

mds1 commented May 5, 2023

Do we need the bool? In other words, do we actually care if a prank is active, or do we just care about what the msg.sender and tx.origin will be in the next call? I suspect it's the latter, meaning we can simplify this interface. If there is an active prank, you return the prank addresses, if there isn't you return the default sender addresses.

@mds1
Copy link
Collaborator

mds1 commented May 5, 2023

Given that use case, we'd probably want to rename this to something more generic like readCallers.

Another consideration: how does this cheatcode behave in scripts, where instead of pranks you have broadcasts, but which have similar semantics of changing the msg.sender?

@Evalir Evalir self-requested a review May 5, 2023 15:49
@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

Do we need the bool? In other words, do we actually care if a prank is active, or do we just care about what the msg.sender and tx.origin will be in the next call? I suspect it's the latter, meaning we can simplify this interface. If there is an active prank, you return the prank addresses, if there isn't you return the default sender addresses.

Following what @PaulRBerg described in #4875, I preferred using a bool instead of returning the current caller and origin because in case the user wants to call any of the prank cheat codes he would only need to check the boolean instead of having to manually compare the pranked address and the original caller.
Also, nothing stops the user from calling vm.prank() with the test contract address, in that case, it would be ambiguous if there is an actual prank active or not. Calling vm.stopPrank or any other prank cheat codes could revert the test in that case. This would be an example for this scenario:

contract CounterTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
        counter.setNumber(0);
    }

    function testIncrement() public {
        vm.prank(address(this));

        (bool isActive, address newSender, ) = vm.readPrank();

        counter.increment();
        assertEq(counter.number(), 1);
    }
}

@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

Given that use case, we'd probably want to rename this to something more generic like readCallers.

Another consideration: how does this cheatcode behave in scripts, where instead of pranks you have broadcasts, but which have similar semantics of changing the msg.sender?

I honestly hadn't thought about it, thank you for pointing it out. Maybe we should implement a new cheat code for that scenario considering that with broadcast you alter only the msg.sender, I think it would be cleaner this way.

@mds1
Copy link
Collaborator

mds1 commented May 5, 2023

Following what @PaulRBerg described in #4875, I preferred using a bool instead of returning the current caller and origin because in case the user wants to call any of the prank cheat codes he would only need to check the boolean instead of having to manually compare the pranked address and the original caller.
Also, nothing stops the user from calling vm.prank() with the test contract address, in that case, it would be ambiguous if there is an actual prank active or not. Calling vm.stopPrank or any other prank cheat codes could revert the test in that case. This would be an example for this scenario:

Hmm, based on his description of this cheat:

It would be helpful to have a cheatcode for reading the current prank. The use case is testing utilities that don't have knowledge of when the current prank was started, but which require tweaking the msg.sender to perform a particular action in the contracts.

It's not clear what the details of the use case are, so it's hard to say which UX makes more sense. Note that you can also change the sender used in tests from the CLI with the --sender flag. Therefore just having "no active prank" does not mean the next caller is the default sender, and you'd need to use msg.sender to get the address. The use case I can think of for this cheat is something like getting nonces or state of an account, and in all those cases, you wouldn't actually care if a prank is active or not, and the data you actually want is "what are the next sender/origin addresses used"

For the script scenario, I think this cheat would have to (1) detect that we're in a script environment and revert, or (2) return the address that would be used for the next vm.broadcast() call for both the msg.sender and tx.origin

@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

Following what @PaulRBerg described in #4875, I preferred using a bool instead of returning the current caller and origin because in case the user wants to call any of the prank cheat codes he would only need to check the boolean instead of having to manually compare the pranked address and the original caller.
Also, nothing stops the user from calling vm.prank() with the test contract address, in that case, it would be ambiguous if there is an actual prank active or not. Calling vm.stopPrank or any other prank cheat codes could revert the test in that case. This would be an example for this scenario:

Hmm, based on his description of this cheat:

It would be helpful to have a cheatcode for reading the current prank. The use case is testing utilities that don't have knowledge of when the current prank was started, but which require tweaking the msg.sender to perform a particular action in the contracts.

It's not clear what the details of the use case are, so it's hard to say which UX makes more sense. Note that you can also change the sender used in tests from the CLI with the --sender flag. Therefore just having "no active prank" does not mean the next caller is the default sender, and you'd need to use msg.sender to get the address. The use case I can think of for this cheat is something like getting nonces or state of an account, and in all those cases, you wouldn't actually care if a prank is active or not, and the data you actually is what are the next sender/origin addresses used

For the script scenario, I think this cheat would have to (1) detect that we're in a script environment and revert, or (2) return the address that would be used for the next vm.broadcast() call for both the msg.sender and tx.origin

Hmm, maybe we should wait for him to provide a practical scenario where this cheat code would be used to narrow down the implementation and not diverge from his expected behavior.

@PaulRBerg
Copy link
Contributor

PaulRBerg commented May 5, 2023

we should wait for him to provide a practical scenario

Consider this function:

// src/Foo.sol
contract Foo {
     function doSomethingUseful() external onlyOwner {
         // --- snip --- //
    }
}

And imagine that your code base has hundreds of test files (as our code base does). Now, the use case is to be able to write a subroutine that calls the function above, like this:

// test/MyTest.sol
contract Utils {
    function usefulSubroutine() internal {
        address initialPrank = vm.readPrank();
        changePrank(users.admin);
        foo.doSomethingUseful();
        changePrank(initialPrank);
    }
}

The goal is to perform a call only an admin can perform, then switch back to whatever prank was before.

@mds1
Copy link
Collaborator

mds1 commented May 5, 2023

The goal is to perform a call only an admin can perform, then switch back to whatever prank was before.

So for your use case, either approach would work. With the bool version of cheatcode, you'd:

  • check the current prank
  • if there is no prank, use a startPrank(admin) then stopPrank()
  • if there is a prank, use a startPrank(admin) then startPrank(prevPrankAddr)

With the no bool version, you'd:

  • check the current callers
  • do startPrank(admin) then startPrank(prevCallerAddr)

One downside of the second is that now you technically are in a prank state even though you weren't before, but functionality wise everything should be the same. This could be mitigated by forge detecting when a startPrank call matches the default senders and instead acting as a stop prank, but that feels dirty

I think I've convinced myself that bool approach is better for that reason, but that we should change the behavior so it returns the default (or configured via --sender) msg.sender/tx.origin when there is no prank, instead of the zero address

@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

we should wait for him to provide a practical scenario

Consider this function:

// src/Foo.sol
contract Foo {
     function doSomethingUseful() external onlyOwner {
         // --- snip --- //
    }
}

And imagine that your code base has hundreds of test files (as our code base does). Now, the use case is to be able to write a subroutine that calls the function above, like this:

// test/MyTest.sol
contract Utils {
    function usefulSubroutine() internal {
        address initialPrank = vm.readPrank();
        changePrank(users.admin);
        foo.doSomethingUseful();
        changePrank(initialPrank);
    }
}

The goal is to perform a call only an admin can perform, then switch back to whatever prank was before.

Cool thanks for shedding some light on this. Please @mds1 tell me what should I do next. Do we keep the current implementation or change something?

@xeno097
Copy link
Contributor Author

xeno097 commented May 5, 2023

The goal is to perform a call only an admin can perform, then switch back to whatever prank was before.

So for your use case, either approach would work. With the bool version of cheatcode, you'd:

  • check the current prank
  • if there is no prank, use a startPrank(admin) then stopPrank()
  • if there is a prank, use a startPrank(admin) then startPrank(prevPrankAddr)

With the no bool version, you'd:

  • check the current callers
  • do startPrank(admin) then startPrank(prevCallerAddr)

One downside of the second is that now you technically are in a prank state even though you weren't before, but functionality wise everything should be the same. This could be mitigated by forge detecting when a startPrank call matches the default senders and instead acting as a stop prank, but that feels dirty

I think I've convinced myself that bool approach is better for that reason, but that we should change the behavior so it returns the default (or configured via --sender) msg.sender/tx.origin when there is no prank, instead of the zero address

Nice I'll do that. thank you!

@xeno097
Copy link
Contributor Author

xeno097 commented May 7, 2023

The goal is to perform a call only an admin can perform, then switch back to whatever prank was before.

So for your use case, either approach would work. With the bool version of cheatcode, you'd:

  • check the current prank
  • if there is no prank, use a startPrank(admin) then stopPrank()
  • if there is a prank, use a startPrank(admin) then startPrank(prevPrankAddr)

With the no bool version, you'd:

  • check the current callers
  • do startPrank(admin) then startPrank(prevCallerAddr)

One downside of the second is that now you technically are in a prank state even though you weren't before, but functionality wise everything should be the same. This could be mitigated by forge detecting when a startPrank call matches the default senders and instead acting as a stop prank, but that feels dirty

I think I've convinced myself that bool approach is better for that reason, but that we should change the behavior so it returns the default (or configured via --sender) msg.sender/tx.origin when there is no prank, instead of the zero address

Done! Sorry for the delayed changes

@mds1
Copy link
Collaborator

mds1 commented May 8, 2023

@xeno097 what approach did you go with for how this cheat behaves in scripts? I don't see any tests using this cheat in a script env so I'm not sure it's currently handled. Quoting from above I think the options are:

For the script scenario, I think this cheat would have to (1) detect that we're in a script environment and revert, or (2) return the address that would be used for the next vm.broadcast() call for both the msg.sender and tx.origin

@xeno097
Copy link
Contributor Author

xeno097 commented May 8, 2023

@xeno097 what approach did you go with for how this cheat behaves in scripts? I don't see any tests using this cheat in a script env so I'm not sure it's currently handled. Quoting from above I think the options are:

For the script scenario, I think this cheat would have to (1) detect that we're in a script environment and revert, or (2) return the address that would be used for the next vm.broadcast() call for both the msg.sender and tx.origin

Sorry, my bad. Of the 2 options, I prefer the revert one because I think it is cleaner. Maybe later we can add an equivalent cheat code for the broadcasting scenario called readBroadcast to keep things nice and separated. What do you think @mds1?

@xeno097 xeno097 changed the title feat(forge): readPrank cheatcode feat(forge): readCallers cheatcode May 18, 2023
@mattsse
Copy link
Member

mattsse commented May 20, 2023

I haven't followed the recent discussion here closely, but it looks like this is ready for review?

@mds1
Copy link
Collaborator

mds1 commented May 22, 2023

This is not ready for review yet, it needs to be updated to one of the too versions described in #4884 (comment)

@wighawag @PaulRBerg just want to sanity check we're all on board with that approach, and see if either of you have a preference on which of those two we use?

@PaulRBerg
Copy link
Contributor

I'd go with the first option described in #4884 (comment), i.e. the one which uses this enum:

enum CallerMode {
  None
  Broadcast
  StartBroadcast
  Prank
  StartPrank
}

It's more explicit and specific; whereas isRecurring could easily be confused to mean something else.

@xeno097
Copy link
Contributor Author

xeno097 commented May 22, 2023

I'd go with the first option described in #4884 (comment), i.e. the one which uses this enum:

enum CallerMode {
  None
  Broadcast
  StartBroadcast
  Prank
  StartPrank
}

It's more explicit and specific; whereas isRecurring could easily be confused to mean something else.

Nice! I had already updated the implementation with this option last week. Let's see if @wighawag has any comment about this if not I think this is ready for review @mds1 @mattsse

@wighawag
Copy link

This is good for me, I also prefer the enum version. thanks!

Copy link
Member

@Evalir Evalir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good! Sorry for the massive delay @xeno097 .

cc @mattsse @mds1 for final approve/merge

Can we please follow up with a forge-std/book PR?

ci failure is also unrelated

Cheats constant cheats = Cheats(HEVM_ADDRESS);

function testReadCallersWithNoActivePrankOrBroadcast() public {
// Arrange
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove these? :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup np 😅

@mds1
Copy link
Collaborator

mds1 commented Jun 2, 2023

I won't be able to look at this until sometime monday, but cc'ing @wighawag @PaulRBerg in case you have the bandwidth the foundryup -P 4844 and test this PR

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test+code lgtm!

any objections @mds1 ?

Comment on lines +6 to +12
enum CallerMode {
None,
Broadcast,
RecurrentBroadcast,
Prank,
RecurrentPrank
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this we'd need to move to forge-std?

Copy link
Collaborator

@mds1 mds1 Jun 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea @xeno097 once this is merged do you mind opening a PR to forge-std to add these to VmSafe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep no problem! I think I also need to add a new page to the fpundry book with documentation for the cheatcode right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please that would be great! 🙏

Copy link
Collaborator

@mds1 mds1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm (assuming CI failures are unrelated, deferring to @mattsse / @Evalir there). Thanks a lot @xeno097!

@Evalir
Copy link
Member

Evalir commented Jun 6, 2023

yup ci failure is unrelated. merging!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants