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

Test function early returns after vm.expectRevert #3437

Open
2 tasks done
0xbok opened this issue Oct 3, 2022 · 13 comments · Fixed by #4945
Open
2 tasks done

Test function early returns after vm.expectRevert #3437

0xbok opened this issue Oct 3, 2022 · 13 comments · Fixed by #4945
Labels
A-cheatcodes Area: cheatcodes C-forge Command: forge T-bug Type: bug T-likely-breaking Type: requires changes that can be breaking
Milestone

Comments

@0xbok
Copy link

0xbok commented Oct 3, 2022

Component

Forge

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge 0.2.0 (d7733ee 2022-10-03T00:06:06.841223Z)

What command(s) is the bug in?

forge test

Operating System

macOS (Apple Silicon)

Describe the bug

pragma solidity 0.8.4;

import "forge-std/Test.sol";

contract ContractTest is Test {
    function setUp() public {}

    function rever() internal {
        revert();
    }

    function testExample() public {
        vm.expectRevert();
        rever();

        rever();
        console.log("Does not print");
    }
}

Running forge test -vvv passes this test and also doesn't print "Does not print" on console. In summary, after vm.expectRevert() the test just executes the call and does not run the remaining test.

@0xbok 0xbok added the T-bug Type: bug label Oct 3, 2022
@mattsse
Copy link
Member

mattsse commented Oct 3, 2022

@DaniPopes there's something off here, do you have the bandwidth to look into this?

@DaniPopes
Copy link
Member

This has something to do with reverting and expecting on the test contract itself since this works as expected:

// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.4;

import "forge-std/Test.sol";

contract CustomContract {
    error CustomError();

    function rever() external {
        revert CustomError();
    }
}

contract ContractTest is Test {
    CustomContract internal c;

    function setUp() public {
        c = new CustomContract();
    }

    function testExample() public {
        // vm.expectRevert(CustomContract.CustomError.selector);
        vm.expectRevert();
        c.rever();

        c.rever();
        console.log("Does not print");
    }
}

Outputs:

$ forge test -vvv
[...]
[FAIL. Reason: CustomError()] testExample() (gas: 8795)
Traces:
  [8795] ContractTest::testExample() 
    ├─ [0] VM::expectRevert() 
    │   └─  ()
    ├─ [158] CustomContract::rever() 
    │   └─ ← "CustomError()"
    ├─ [158] CustomContract::rever() 
    │   └─ ← "CustomError()"
    └─ ← "CustomError()"

Test result: FAILED. 0 passed; 1 failed; finished in 244.88µs

Failing tests:
Encountered 1 failing test in test/a.t.sol:ContractTest
[FAIL. Reason: CustomError()] testExample() (gas: 8795)

Encountered a total of 1 failing tests, 0 tests succeeded

@0xbok
Copy link
Author

0xbok commented Oct 4, 2022

This has something to do with reverting and expecting on the test contract

makes sense as that’s how I discovered this bug. I have an abstract contract that I’m testing by inheriting it from my test contract.

@DaniPopes
Copy link
Member

I think what's happening is that the vm.expectRevert call makes the test expect a revert, so when the test contract itself reverts, stopping the execution of the test, it also succeeds since we expected a revert.
You can create a mock contract that just inherits the abstract contract and deploy and test with external calls on that

@mds1
Copy link
Collaborator

mds1 commented Oct 4, 2022

Does this happen (i.e. the test erroneously passes) if the reason the test contract reverts is due to a solidity panic like overflow or divide by zero? I don't see why you'd intentionally revert in a test contract like this, but I can imagine unintentional reverts due to math errors and want to make sure that doesn't cause tests to incorrectly pass.

I think what's happening is that the vm.expectRevert call makes the test expect a revert, so when the test contract itself reverts, stopping the execution of the test, it also succeeds since we expected a revert.

Hmm this seems odd though because my understanding is that expectRevert only expects reverts for calls made from the test contract, I don't think it should count reverts within the test contract, that feels like a bug

@0xbok
Copy link
Author

0xbok commented Oct 4, 2022

I don't see why you'd intentionally revert in a test contract like this

I have an abstract contract that I’m testing by inheriting it from my test contract. The abstract contract has a function that can revert. The PoC that I showed in this issue is just a toy example replicating this behavior. But I'll be moving to @DaniPopes's workaround by creating a mock contract which just calls the abstract contract.

@mds1
Copy link
Collaborator

mds1 commented Oct 4, 2022

Got it, makes sense! I do still think we should verify the below, to make sure users can't have tests that are passing when you'd normally expect them to fail due to a revert in the test contract

Does this happen (i.e. the test erroneously passes) if the reason the test contract reverts is due to a solidity panic like overflow or divide by zero? I can imagine unintentional reverts due to math errors and want to make sure that doesn't cause tests to incorrectly pass.

@0xbok
Copy link
Author

0xbok commented Oct 4, 2022

Yes, overflow passes as well:

pragma solidity 0.8.4;

import "forge-std/Test.sol";

contract ContractTest is Test {
    function setUp() public {}

    function rever(uint8 a) internal returns (uint8) {
        return a*2;
    }

    function testExample() public {
        vm.expectRevert();
        rever(254);

        rever(254);
        console.log("print");
    }
}

@mds1
Copy link
Collaborator

mds1 commented Oct 4, 2022

Thanks for testing! So IMO this is a bug that we should fix, cc @mattsse. According to the docs, expectRevert looks for a revert in the next call. In that example, there is never a call (just jumps to internal functions), so that test should fail from the overflow in rever(uint8)

@brockelmore
Copy link
Member

brockelmore commented Dec 2, 2022

So I looked into this. It is due to this line:

if data.journaled_state.depth() <= expected_revert.depth {
(and the create_end).

We evaluate based on <=, I dont know why we do <= instead of ==. I think the only possible case where <= is used is this exact case? So the answer may be just to switch to ==. But it is sort of hard to reason about. Switching this line to == makes the following tests pass/fail as expected:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";


contract ContractTest is Test {
    function setUp() public {}

    function rever() public {
        revert();
    }

    function delayed_rever() public {
        ContractTest(address(this)).rever();
    }

    function testExample() public {
        vm.expectRevert();
        rever();

        rever();
        console.log("Does not print");
    }

    function testExample2() public {
        vm.expectRevert();
        ContractTest(address(this)).rever();
    }

    function testExample3() public {
        vm.expectRevert();
        ContractTest(address(this)).delayed_rever();
    }
}
Running 3 tests for test/Counter.t.sol:ContractTest
[FAIL. Reason: EvmError: Revert] testExample() (gas: 3068)
Traces:
  [121] ContractTest::setUp()
    └─  ()

  [3068] ContractTest::testExample()
    ├─ [0] VM::expectRevert()
       └─  ()
    └─  "EvmError: Revert"

[PASS] testExample2() (gas: 3541)
Traces:
  [3541] ContractTest::testExample2()
    ├─ [0] VM::expectRevert()
       └─  ()
    ├─ [151] ContractTest::rever()
       └─  "EvmError: Revert"
    └─  ()

[PASS] testExample3() (gas: 4011)
Traces:
  [4011] ContractTest::testExample3()
    ├─ [0] VM::expectRevert()
       └─  ()
    ├─ [643] ContractTest::delayed_rever()
       ├─ [151] ContractTest::rever()
          └─  "EvmError: Revert"
       └─  "EvmError: Revert"
    └─  ()

The early return is due to the way expectRevert works, it just changes the status code of the return, so any revert/return will always stop current call context.

@0xbok
Copy link
Author

0xbok commented Dec 3, 2022

@brockelmore the PR has been closed, so is this an intended behaviour now, or is there a different fix being discussed?

@brockelmore
Copy link
Member

Leaving this issue open because I would like a better solution that what currently is happening:

pragma solidity 0.8.4;

import "forge-std/Test.sol";

contract ContractTest is Test {
    function rever() internal {
        revert();
    }

    // currently "intended" behavior (was not supposed to be supported but has been for too long on accident)
    function testExample() public {
        vm.expectRevert();
        rever();
        // anything after this will *NOT* execute. We cannot continue execution after a revert in the same call 
        // its a footgun that is probably just gonna be here for a little while until we can find a better solution
    }
    
}

Too many people rely on the above pattern already that we probably need to have a broader discussion and if we break the above pattern we let people know its going to break ahead of time. If you can avoid the above pattern, I would

@frangio
Copy link

frangio commented Apr 10, 2023

The problem with using vm.expectRevert before an internall call is that, if the internal call does not revert, execution will continue and silently consume the next revert, which was not at all the developer intention. This seems highly problematic to me, I'd think it should be fixed as soon as possible.

espendk added a commit to mangrovedao/mangrove-core that referenced this issue Nov 30, 2023
vm.expertRevert has surprising behavior when used before non-external calls: Anything after the call will not execute.

See this GitHub issue and comment for more details: foundry-rs/foundry#3437 (comment)

Expecting reverts for non-external calls is therefore only safe if the call is last in the test.

This commit fixes two tests in TickAndBinTest which assumed that multiple reverts could be expected within a single test. The tests are split into one test per expected revert.

The commit also fixes an error in one of the test cases which this change uncovered:
The test assumed that it was possible to pass a too small `exp` to `TickLib.tickFromNormalizedRatio`. However, MAX_RATIO_EXP is zero and the exp parameter is uint,
so it is not possible pass an exp value that is smaller than MAX_RATIO_EXP, since uint(MAX_RATIO_EXP - 1) = uint(-1) = type(uint)max.
@zerosnacks zerosnacks added this to the v1.0.0 milestone Jul 26, 2024
@zerosnacks zerosnacks added the T-likely-breaking Type: requires changes that can be breaking label Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-cheatcodes Area: cheatcodes C-forge Command: forge T-bug Type: bug T-likely-breaking Type: requires changes that can be breaking
Projects
Status: Todo
8 participants