Skip to content
5 changes: 3 additions & 2 deletions src/kontrol/kdist/cheatcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ All cheat code calls which take place while `expectRevert` is active are ignored
<expectedRevert>
<isRevertExpected> true </isRevertExpected>
...
</expectedRevert>
<wordStack> _ : ACCTTO : _ : _ : _ : RETSTART : RETWIDTH : _WS </wordStack>
[priority(35)]
```

Expand All @@ -579,12 +579,13 @@ WThe `#checkRevert` will be used to compare the status code of the execution and
rule [foundry.set.expectrevert.1]:
<k> #next [ _OP:CallOp ] ~> (.K => #checkRevert ~> #updateRevertOutput RETSTART RETWIDTH) ~> #execute ... </k>
<callDepth> CD </callDepth>
<wordStack> _ : _ : _ : _ : _ : RETSTART : RETWIDTH : _WS </wordStack>
<wordStack> _ : ACCTTO : _ : _ : _ : RETSTART : RETWIDTH : _WS </wordStack>
<expectedRevert>
<isRevertExpected> true </isRevertExpected>
<expectedDepth> CD </expectedDepth>
...
</expectedRevert>
requires ACCTTO =/=Int #address(FoundryCheat)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this check also have to be added to the rule [foundry.set.expectrevert.2] below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think it's possible/likely to delegate a call to the cheatcode address?

Copy link
Contributor

@anvacaru anvacaru Apr 11, 2025

Choose a reason for hiding this comment

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

I looked into this and wrote two tests, one for delegatecall and one for staticcall calling a cheatcode.

The assertion at the end fails for delegatecall, whereas it does not fail for staticcall.

    function testDelegatecallCheatcodeReverts() external {
        (bool success, ) = VM_ADDRESS.delegatecall(
            abi.encodeWithSignature("roll(uint256)", 1337)
        );
        assertTrue(success);
        assertEq(block.number, 1337);
    }

    function testStaticcallcallCheatcode() external view {
        (bool success, ) = VM_ADDRESS.staticcall(
            abi.encodeWithSignature("roll(uint256)", 1337)
        );
        assertTrue(success);
        assertEq(block.number, 1337);
    }

So maybe it's worth adding the rule to cover staticcall for now. Next, in another issue/PR, we should check that Kontrol has the same behavior and ensure that delegating into VM_ADDRESS does not execute the cheat code.

@palinatolmach what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it sounds good! Let's add a rule for staticcall here, and ensure that Kontrol behaves similarly wrt delegatecall as a follow-up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do it!
I also tried the following tests with Foundry:

function testIncrementAsNotOwner_2() public {
        vm.expectRevert(Unauthorized.selector);
        (bool success, ) = VM_ADDRESS.delegatecall(
            abi.encodeWithSignature("roll(uint256)", 1337)
        );
        upOnly.increment();
    }

And this test is passing when it shouldn't, since the function does not revert as expected. On the other hand, the following test fails:

function testIncrementAsNotOwner_3() public {
        vm.expectRevert(Unauthorized.selector);
        (bool success, ) = VM_ADDRESS.delegatecall(
            abi.encodeWithSignature("roll(uint256)", 1337)
        );
        vm.prank(address(0));
        upOnly.increment();
    }

It seems that delegatecall to VM_ADDRESS clears the expectRevert cheatcode.

[priority(32)]

rule [foundry.set.expectrevert.2]:
Expand Down
1 change: 0 additions & 1 deletion src/tests/integration/test-data/foundry-prove-skip
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ MockCallRevertTest.testMockCallRevertResetsMockCall()
MockCallRevertTest.testMockCallRevertWithCall()
MockCallRevertTest.testMockCallEmptyAccount()
OwnerUpOnlyTest.testFailIncrementAsNotOwner()
OwnerUpOnlyTest.testIncrementAsNotOwner()
OwnerUpOnlyTest.testIncrementAsOwner()
PlainPrankTest.testFail_startPrank_existingAlready()
PrankTest.testAddAsOwner(uint256)
Expand Down
1 change: 0 additions & 1 deletion src/tests/integration/test-data/foundry-prove-skip-legacy
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ MockFunctionTest.test_mock_function_all_args()
MockFunctionTest.test_mock_function_concrete_args()
NestedStructsTest.prove_fourfold_nested_struct(((((uint8,uint256),bytes32)[],bytes32)))
OwnerUpOnlyTest.testFailIncrementAsNotOwner()
OwnerUpOnlyTest.testIncrementAsNotOwner()
OwnerUpOnlyTest.testIncrementAsOwner()
PlainPrankTest.testFail_startPrank_existingAlready()
PlainPrankTest.testFail_startPrank_internalCall()
Expand Down
Loading
Loading