8000 Allow `targetContract(address(this));` to perform Invariant Testing on Scaffolded Contracts · Issue #506 · a16z/halmos · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow targetContract(address(this)); to perform Invariant Testing on Scaffolded Contracts #506

New issue

Have a question about this project? Sign up for a free Git 8000 Hub 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

Open
GalloDaSballo opened this issue May 5, 2025 · 3 comments
Labels
enhancement New feature or request

Comments

@GalloDaSballo
Copy link

Is your feature request related to a problem? Please describe.
Per this merged foundry feature: foundry-rs/foundry#10253

We expect targetContract(address(this)); to cause the fuzzer to use every function exposed by this as a handler

And we expect all invariants to be specified on this

Describe the solution you'd like

An extremely common way of writing invariant tests uses Target Function, explicit functions that tell the fuzzer how to explore state

Source:
https://github.com/Recon-Fuzz/create-chimera-app (Recon)
https://github.com/perimetersec/arachne (PerimeterSec)
https://github.com/euler-xyz/euler-vault-kit/tree/master/src (Enigma Dark)
https://github.com/CodeHawks-Contests/2025-02-gamma/blob/main/test/fuzzing/echidna/CryticToFoundry.sol (Guardian Audits)

As it stands targetFunctions doesn't allow passing address(this)

This prevents these testing suites from being easily reusable with Halmos

Describe alternatives you've considered
This is an additional feature that makes Halmos competitive with Foundry, Echidna and Medusa

I'm unaware of any alternative that makes Halmos as powerful

Additional context
You can attempt running all tools on: https://github.com/Recon-Fuzz/create-chimera-app

Which is a template that is compatible with all of them
However, Halmos Invariant Testing will fail due to a lack of compatibility with targetContract(address(this));

@0xkarmacoma
Copy link
Collaborator

Testing with:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "forge-std/Test.sol";

contract InvariantTest is Test {
    bool fooCalled;

    function setUp() public {
        targetContract(address(this));
    }

    function foo() public {
        fooCalled = true;
    }

    function invariant_this() public {
        assertFalse(fooCalled);
    }
}

On foundry 1.0.0-rc:

Ran 1 test for test/Invariant.t.sol:FuzzBytes4
[FAIL: failed to set up invariant testing environment: No contracts to fuzz.] invariant_this() (runs: 0, calls: 0, reverts: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 14.84ms (3.71ms CPU time)

But on foundry 1.1.0-stable:

Ran 1 test for test/Invariant.t.sol:FuzzBytes4
[FAIL: assertion failed]
	[Sequence] (original: 2, shrunk: 1)
		sender=0x0c34F849a9eb89b5DaEa4D41Fc497B8fBeDcd325 addr=[test/Invariant.t.sol:FuzzBytes4]0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 calldata=foo() args=[]
 invariant_this() (runs: 0, calls: 0, reverts: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.54ms (1.83ms CPU time)

Ran 1 test suite in 400.16ms (3.54ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

With current halmos, we mirrored the behavior of foundry 1.0.0-rc:

Running 1 tests for test/Invariant.t.sol:FuzzBytes4
[ERROR] invariant_this()
ERROR    HalmosException: No target contracts available. A targetSelector() must be specified if the test contract is set as a target. 

@0xkarmacoma
Copy link
Collaborator

FYI the behavior is a little weird, and may cause some issues. Take this test for example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "forge-std/Test.sol";

contract InvariantTest is Test {
    bool fooCalled;
    uint256 invariantCalledNum;
    uint256 setUpCalledNum;

    function setUp() public {
        targetContract(address(this));
    }

    function foo() public {
        fooCalled = true;
    }

    function invariant_foo_called() public view {
        assert(!fooCalled);
    }

    function invariant_considered_target() public {
        // this is evaluated for invariant violations, but also considered a target state-changing function
        invariantCalledNum++;
        assertLt(invariantCalledNum, 10);
    }

    function invariant_setUp_considered_target() public {
        setUpCalledNum++;
        assertLt(setUpCalledNum, 10);
    }
}
$ ft --mc InvariantTest

╭---------------+-----------------------------------+-------+---------+----------╮
| Contract      | Selector                          | Calls | Reverts | Discards |
+================================================================================+
| InvariantTest | foo                               | 6     | 0       | 0        |
|---------------+-----------------------------------+-------+---------+----------|
| InvariantTest | invariant_considered_target       | 3     | 0       | 0        |
|---------------+-----------------------------------+-------+---------+----------|
| InvariantTest | invariant_setUp_considered_target | 9     | 0       | 0        |
|---------------+-----------------------------------+-------+---------+----------|
| InvariantTest | setUp                             | 3     | 0       | 0        |
╰---------------+-----------------------------------+-------+---------+----------╯

Suite result: FAILED. 0 passed; 3 failed; 0 skipped; finished in 27.48ms (34.19ms CPU time)

Ran 1 test suite in 389.21ms (27.48ms CPU time): 0 tests passed, 3 failed, 0 skipped (3 total tests)

Failing tests:
Encountered 3 failing tests in test/Invariant.t.sol:InvariantTest
[FAIL: assertion failed: 10 >= 10]
	[Sequence] (original: 55, shrunk: 9)
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
		... calldata=invariant_considered_target() args=[]
 invariant_considered_target() (runs: 0, calls: 0, reverts: 7)
[FAIL: panic: assertion failed (0x01)]
	[Sequence] (original: 3, shrunk: 1)
		... calldata=foo() args=[]
 invariant_foo_called() (runs: 0, calls: 0, reverts: 0)
[FAIL: assertion failed: 10 >= 10]
	[Sequence] (original: 21, shrunk: 9)
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
		... calldata=invariant_setUp_considered_target() args=[]
 invariant_setUp_considered_target() (runs: 0, calls: 0, reverts: 0)

This shows that the target functions are not just foo(), but also setUp() and the invariant_ function themselves (!)

I think the special functions like setUp() and invariant_ should be excluded from target functions. Can you confirm what behavior you would expect from this @GalloDaSballo?

@GalloDaSballo
Copy link
Author

Thank you for digging deeper @0xkarmacoma

From my POV we'd want to exclude exclusively the setUp function which is supposed to be the constructor of the test contract

Every other function can have storage changes, that can influence other functions, meaning they should probably be included

If we'd wanted an invariant to be stateless, we'd probably resort to something like this:
https://github.com/Recon-Fuzz/create-chimera-app/blob/97036ad908633de6e59d576765a1b08b9e1ba6ff/test/recon/targets/DoomsdayTargets.sol#L22

If you remove the invariant_ you'd force developers to make all invariants be stateless, which is ok from my POV, but less flexible than not

If doing so makes Halmos competitively faster, then that's a tradeoff worth taking, whereas if there's no difference it's worth keeping both options open

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

No branches or pull requests

2 participants
0