---
description: Write effective invariant tests with handler patterns and ghost variables.
---

## Invariant Testing

Invariant testing verifies properties that should always hold true, regardless of the sequence of actions taken. Forge runs random sequences of function calls and checks invariants after each call.

### Basic invariant test

```solidity [test/Vault.invariant.t.sol]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";

contract VaultInvariantTest is Test {
    Vault vault;

    function setUp() public {
        vault = new Vault();
        targetContract(address(vault)); // [!code hl]
    }

    function invariant_SolvencyCheck() public view { // [!code hl]
        assertGe(
            address(vault).balance,
            vault.totalDeposits()
        );
    }
}
```

Run invariant tests:

```bash
$ forge test --match-contract VaultInvariantTest
```

### After-invariant hook

Define `afterInvariant()` when you need to check a property once after each
invariant run, rather than after every call in the run:

```solidity [test/Vault.invariant.t.sol]
contract VaultInvariantTest is Test {
    Vault vault;
    VaultHandler handler;

    function setUp() public {
        vault = new Vault();
        handler = new VaultHandler(vault);
        targetContract(address(handler));
    }

    function invariant_SolvencyCheck() public view {
        assertGe(address(vault).balance, vault.totalDeposits());
    }

    function afterInvariant() public view { // [!code hl]
        assertEq( // [!code hl]
            address(vault).balance, // [!code hl]
            handler.ghost_depositSum() - handler.ghost_withdrawSum() // [!code hl]
        ); // [!code hl]
    } // [!code hl]
}
```

Forge calls the hook against the final state of a run after its invariant checks
pass. A revert or failed assertion in the hook fails the invariant test and
reports the run's call sequence. The hook must have the exact signature
`afterInvariant()` and a test contract can define only one. State changes made
by the hook are not persisted.

Use an `invariant_*` function for properties that must hold throughout a call
sequence. Use `afterInvariant()` for aggregate or end-of-run checks, especially
when evaluating the property after every call would be unnecessarily expensive.

### Handler pattern

Handlers wrap target contracts to constrain inputs and track state:

:::code-group

```solidity [test/handlers/VaultHandler.sol]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../../src/Vault.sol";

contract VaultHandler is Test {
    Vault public vault;
    
    uint256 public ghost_depositSum; // [!code hl]
    uint256 public ghost_withdrawSum; // [!code hl]
    
    address[] public actors;
    address internal currentActor;
    
    modifier useActor(uint256 actorSeed) {
        currentActor = actors[bound(actorSeed, 0, actors.length - 1)];
        vm.startPrank(currentActor);
        _;
        vm.stopPrank();
    }

    constructor(Vault _vault) {
        vault = _vault;
        for (uint256 i = 0; i < 10; i++) {
            actors.push(makeAddr(string(abi.encodePacked("actor", i))));
            vm.deal(actors[i], 100 ether);
        }
    }

    function deposit(uint256 amount, uint256 actorSeed) external useActor(actorSeed) {
        amount = bound(amount, 0.01 ether, 10 ether); // [!code hl]
        
        vault.deposit{value: amount}();
        ghost_depositSum += amount; // [!code hl]
    }

    function withdraw(uint256 amount, uint256 actorSeed) external useActor(actorSeed) {
        uint256 balance = vault.balanceOf(currentActor);
        if (balance == 0) return;
        
        amount = bound(amount, 1, balance); // [!code hl]
        
        vault.withdraw(amount);
        ghost_withdrawSum += amount; // [!code hl]
    }
}
```

```solidity [test/Vault.invariant.t.sol]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";
import {VaultHandler} from "./handlers/VaultHandler.sol"; // [!code hl]

contract VaultInvariantTest is Test {
    Vault vault;
    VaultHandler handler; // [!code hl]

    function setUp() public {
        vault = new Vault();
        handler = new VaultHandler(vault); // [!code hl]
        
        // Only target the handler, not the vault directly
        targetContract(address(handler)); // [!code hl]
    }

    function invariant_ConservationOfDeposits() public view {
        assertEq(
            address(vault).balance,
            handler.ghost_depositSum() - handler.ghost_withdrawSum() // [!code hl]
        );
    }

    function invariant_SolvencyCheck() public view {
        assertGe(
            address(vault).balance,
            vault.totalDeposits()
        );
    }
}
```

:::

### Ghost variables

Ghost variables track cumulative state that isn't stored on-chain:

```solidity [test/handlers/TokenHandler.sol]
contract TokenHandler is Test {
    Token public token;
    
    // Track all mints and burns
    uint256 public ghost_mintedSum; // [!code hl]
    uint256 public ghost_burnedSum; // [!code hl]
    
    // Track per-address deltas
    mapping(address => int256) public ghost_balanceDeltas; // [!code hl]

    function mint(address to, uint256 amount) external {
        amount = bound(amount, 1, 1000 ether);
        
        token.mint(to, amount);
        
        ghost_mintedSum += amount; // [!code hl]
        ghost_balanceDeltas[to] += int256(amount); // [!code hl]
    }

    function burn(address from, uint256 amount) external {
        uint256 balance = token.balanceOf(from);
        if (balance == 0) return;
        
        amount = bound(amount, 1, balance);
        
        vm.prank(from);
        token.burn(amount);
        
        ghost_burnedSum += amount; // [!code hl]
        ghost_balanceDeltas[from] -= int256(amount); // [!code hl]
    }
}

contract TokenInvariantTest is Test {
    function invariant_TotalSupplyMatchesGhosts() public view {
        assertEq(
            token.totalSupply(),
            handler.ghost_mintedSum() - handler.ghost_burnedSum() // [!code hl]
        );
    }
}
```

### Configuring invariant runs

```toml [foundry.toml]
[invariant]
runs = 256           # Number of test runs
depth = 100          # Calls per run
fail_on_revert = false  # Don't fail on handler reverts
shrink_run_limit = 5000 # Attempts to shrink failing sequence
```

### Advanced invariant campaigns

Foundry v1.7.0 added several options that make deep invariant campaigns both faster and more expressive.

#### `check_interval`

Use `check_interval` to trade precision for speed on deep campaigns:

```toml [foundry.toml]
[invariant]
depth = 1000
check_interval = 10
```

This changes when Foundry evaluates the invariant:

- `0` only checks the last call in each run
- `1` checks every call
- `N` checks every `N` calls and always the last call

This is useful when an invariant is expensive to evaluate, but it can miss bugs that break and then restore the invariant between checks.

#### Time-based campaigns

Use `max_time_delay` and `max_block_delay` when bugs depend on elapsed time or block movement between calls:

```toml [foundry.toml]
[invariant]
max_time_delay = 86400 # 1 day, in seconds
max_block_delay = 1000
```

Foundry will fuzz both the call sequence and the time or block distance between calls. This is especially useful for vesting, auctions, TWAPs, cooldowns, and any logic that depends on `block.timestamp` or `block.number`.

#### Optimization mode

If an invariant function returns `int256` instead of asserting, Foundry switches from "find a failure" mode to "maximize this value" mode:

```solidity
function invariant_optimize_maxUtilization() public view returns (int256) {
    return int256(vault.totalBorrows()) - int256(vault.totalIdle());
}
```

This is a good fit for worst-case slippage, maximum imbalance, largest rounding error, or any other metric you want the fuzzer to push upward.

For optimization-mode campaigns, use a fixed `seed` when you want reproducible results across runs.

### Targeting specific functions

```solidity
function setUp() public {
    vault = new Vault();
    handler = new VaultHandler(vault);
    
    targetContract(address(handler));
    
    // Only call these functions
    bytes4[] memory selectors = new bytes4[](2);
    selectors[0] = VaultHandler.deposit.selector; // [!code hl]
    selectors[1] = VaultHandler.withdraw.selector; // [!code hl]
    targetSelector(FuzzSelector({ // [!code hl]
        addr: address(handler), // [!code hl]
        selectors: selectors // [!code hl]
    })); // [!code hl]
}
```

### Excluding functions

```solidity
function setUp() public {
    targetContract(address(handler));
    
    // Exclude specific functions
    excludeSelector(FuzzSelector({ // [!code hl]
        addr: address(handler), // [!code hl]
        selectors: toSelectors(VaultHandler.debugFunction.selector) // [!code hl]
    })); // [!code hl]
}

function toSelectors(bytes4 selector) internal pure returns (bytes4[] memory) {
    bytes4[] memory selectors = new bytes4[](1);
    selectors[0] = selector;
    return selectors;
}
```

### Call summary

Add a summary function to understand test coverage:

```solidity
contract VaultHandler is Test {
    // Call counters
    mapping(bytes4 => uint256) public calls; // [!code hl]

    function deposit(uint256 amount) external {
        calls[this.deposit.selector]++; // [!code hl]
        // ...
    }

    function withdraw(uint256 amount) external {
        calls[this.withdraw.selector]++; // [!code hl]
        // ...
    }

    function callSummary() external view { // [!code hl]
        console.log("deposit calls:", calls[this.deposit.selector]); // [!code hl]
        console.log("withdraw calls:", calls[this.withdraw.selector]); // [!code hl]
    } // [!code hl]
}

contract VaultInvariantTest is Test {
    function invariant_CallSummary() public view {
        handler.callSummary(); // [!code hl]
    }
}
```

### Multi-contract invariants

Test invariants across multiple contracts:

:::code-group

```solidity [test/handlers/SystemHandler.sol]
contract SystemHandler is Test {
    Vault vault; // [!code hl]
    Token token; // [!code hl]
    Oracle oracle; // [!code hl]

    function depositAndStake(uint256 amount, uint256 actorSeed) external useActor(actorSeed) {
        amount = bound(amount, 1 ether, 100 ether);
        
        token.approve(address(vault), amount); // [!code hl]
        vault.depositAndStake(amount); // [!code hl]
        
        ghost_stakedSum += amount;
    }

    function updatePrice(uint256 newPrice) external {
        newPrice = bound(newPrice, 0.1 ether, 100 ether);
        oracle.setPrice(newPrice); // [!code hl]
    }
}
```

```solidity [test/System.invariant.t.sol]
contract SystemInvariantTest is Test {
    // Cross-contract invariant: vault can always cover withdrawals
    function invariant_VaultSolvency() public view {
        uint256 vaultValue = vault.totalStaked() * oracle.price() / 1e18; // [!code hl]
        assertGe(vaultValue, vault.totalLiabilities()); // [!code hl]
    }
}
```

:::

### Common invariants to test

| Invariant | Description |
|-----------|-------------|
| Conservation | Sum of inputs equals sum of outputs |
| Solvency | Contract can cover all liabilities |
| Monotonicity | Value only increases/decreases |
| Bounds | Value stays within expected range |
| Access control | Only authorized users can call functions |
| State consistency | Related state variables stay in sync |

### Debugging failed invariants

When an invariant fails, Forge shows the call sequence:

```bash
$ forge test --match-test invariant_Solvency -vvvv
```

The output shows each call that led to the failure, helping you reproduce and fix the bug.

### Best practices

| Practice | Description |
|----------|-------------|
| Use handlers | Constrain inputs to valid ranges |
| Track with ghosts | Verify cumulative state matches on-chain state |
| Bound inputs | Use `bound(){:solidity}` instead of `vm.assume(){:solidity}` |
| Multiple actors | Test with various users, not just one |
| Start simple | Begin with basic invariants, add complexity |
| Log call counts | Verify all functions are being exercised |
