Tutorial·10 min का पठन·Solingo द्वारा

Foundry के साथ Invariant Testing — Zero से Production तक

Fuzzing bugs ढूंढता है। Invariants safety prove करते हैं। यहाँ सही तरीके से कैसे लिखें।

# Foundry के साथ Invariant Testing — Zero से Production तक

Unit tests specific scenarios check करते हैं। Fuzz tests random inputs try करते हैं। Invariant tests system properties verify करते हैं — चाहे कुछ भी हो जाए।

Testing के तीन Level

Unit Test:

"transfer() should decrease sender balance"

Fuzz Test:

"transfer() should work for any valid amount"

Invariant Test:

"total supply should NEVER change during transfers"

Invariants सबसे powerful हैं — ये mathematical guarantees हैं।

---

क्या है Invariant?

Invariant = Property जो हमेशा सच होनी चाहिए, चाहे कोई भी sequence of actions हो।

Examples

ERC20:

  • Sum of balances = total supply
  • Balance never negative
  • Transfer decreases sender, increases receiver by same amount

AMM:

  • k = x * y (constant product)
  • Reserves never zero
  • LP token supply = sqrt(reserve0 * reserve1)

Lending Protocol:

  • Total borrowed ≤ total supplied
  • Collateral value > debt value (when healthy)
  • Interest monotonically increasing

---

Basic Invariant Test

Simple ERC20

contract Token {

mapping(address => uint) public balanceOf;

uint public totalSupply;

function mint(address to, uint amount) external {

balanceOf[to] += amount;

totalSupply += amount;

}

function transfer(address to, uint amount) external {

balanceOf[msg.sender] -= amount;

balanceOf[to] += amount;

}

}

Invariant: Sum of Balances = Total Supply

contract TokenInvariantTest is Test {

Token token;

Handler handler;

function setUp() public {

token = new Token();

handler = new Handler(token);

// Tell Foundry to target the handler

targetContract(address(handler));

}

function invariant_totalSupplyEqualsBalances() public {

uint sumBalances = handler.sumBalances();

assertEq(token.totalSupply(), sumBalances);

}

}

Handler

contract Handler is Test {

Token token;

address[] public actors;

mapping(address => uint) public balances;

constructor(Token _token) {

token = _token;

}

function mint(uint actorSeed, uint amount) public {

address actor = _getActor(actorSeed);

// Bound inputs to reasonable range

amount = bound(amount, 0, 1e30);

token.mint(actor, amount);

balances[actor] += amount;

}

function transfer(uint fromSeed, uint toSeed, uint amount) public {

address from = _getActor(fromSeed);

address to = _getActor(toSeed);

amount = bound(amount, 0, balances[from]);

vm.prank(from);

token.transfer(to, amount);

balances[from] -= amount;

balances[to] += amount;

}

function sumBalances() public view returns (uint) {

uint sum;

for (uint i = 0; i < actors.length; i++) {

sum += balances[actors[i]];

}

return sum;

}

function _getActor(uint seed) internal returns (address) {

uint index = bound(seed, 0, 9);

if (index >= actors.length) {

actors.push(makeAddr(string(abi.encode(index))));

}

return actors[index];

}

}

Run Invariant Tests

forge test --match-test invariant -vvv

Foundry automatically:

  • Random function sequences generate करता है
  • Handler के through execute करता है
  • हर call के बाद invariant check करता है
  • Failure पर shrinking करता है (minimal repro)
  • ---

    Handler Pattern

    Handlers critical हैं। ये:

    • Inputs को reasonable bounds में रखते हैं
    • Ghost variables track करते हैं
    • Multiple actors simulate करते हैं

    Pattern Structure

    contract Handler {
    

    TargetContract target;

    // Ghost variables (for tracking)

    uint public ghost_sumDeposits;

    uint public ghost_sumWithdrawals;

    mapping(bytes32 => uint) public calls;

    function handlerFunction(uint seed) public {

    // 1. Bound inputs

    uint amount = bound(seed, 1, 1e18);

    // 2. Setup

    address user = _getRandomUser();

    // 3. Execute

    vm.prank(user);

    target.deposit{value: amount}();

    // 4. Track in ghost variables

    ghost_sumDeposits += amount;

    calls["deposit"]++;

    }

    }

    ---

    Real Example: ERC4626 Vault

    contract Vault is ERC4626 {
    

    constructor(ERC20 _asset) ERC4626(_asset, "Vault", "vTKN") {}

    }

    Invariants

  • Conservation: totalAssets = sum of all deposits - sum of withdrawals
  • Solvency: totalAssets >= convertToAssets(totalSupply)
  • Monotonicity: totalAssets never decreases without withdrawal
  • Share Price: share price never decreases (assuming no losses)
  • Handler Implementation

    contract VaultHandler is Test {
    

    Vault vault;

    ERC20 asset;

    address[] public actors;

    mapping(address => uint) public depositedAssets;

    uint public ghost_depositSum;

    uint public ghost_withdrawSum;

    uint public ghost_sharesSum;

    constructor(Vault _vault, ERC20 _asset) {

    vault = _vault;

    asset = _asset;

    }

    function deposit(uint actorSeed, uint assets) public {

    address actor = _getActor(actorSeed);

    assets = bound(assets, 1, 1e30);

    // Give actor tokens

    asset.mint(actor, assets);

    // Deposit

    vm.startPrank(actor);

    asset.approve(address(vault), assets);

    uint shares = vault.deposit(assets, actor);

    vm.stopPrank();

    // Track

    depositedAssets[actor] += assets;

    ghost_depositSum += assets;

    ghost_sharesSum += shares;

    }

    function withdraw(uint actorSeed, uint shares) public {

    address actor = _getActor(actorSeed);

    uint maxShares = vault.balanceOf(actor);

    if (maxShares == 0) return; // Skip if no shares

    shares = bound(shares, 1, maxShares);

    vm.prank(actor);

    uint assets = vault.redeem(shares, actor, actor);

    // Track

    depositedAssets[actor] -= assets;

    ghost_withdrawSum += assets;

    ghost_sharesSum -= shares;

    }

    function _getActor(uint seed) internal returns (address) {

    uint index = bound(seed, 0, 9);

    if (index >= actors.length) {

    actors.push(makeAddr(string(abi.encode("actor", index))));

    }

    return actors[index];

    }

    }

    Invariant Tests

    contract VaultInvariantTest is Test {
    

    Vault vault;

    ERC20 asset;

    VaultHandler handler;

    function setUp() public {

    asset = new ERC20("Asset", "AST");

    vault = new Vault(asset);

    handler = new VaultHandler(vault, asset);

    targetContract(address(handler));

    }

    // Invariant 1: Conservation

    function invariant_conservation() public {

    assertEq(

    vault.totalAssets(),

    handler.ghost_depositSum() - handler.ghost_withdrawSum()

    );

    }

    // Invariant 2: Solvency

    function invariant_solvency() public {

    uint totalAssets = vault.totalAssets();

    uint totalSupply = vault.totalSupply();

    uint redeemable = vault.convertToAssets(totalSupply);

    assertGe(totalAssets, redeemable);

    }

    // Invariant 3: Share Price Never Decreases

    uint lastSharePrice;

    function invariant_sharePriceMonotonic() public {

    if (vault.totalSupply() == 0) return;

    uint sharePrice = vault.convertToAssets(1e18);

    assertGe(sharePrice, lastSharePrice);

    lastSharePrice = sharePrice;

    }

    // Invariant 4: Shares Sum Matches Total Supply

    function invariant_shareAccounting() public {

    assertEq(vault.totalSupply(), handler.ghost_sharesSum());

    }

    }

    ---

    Advanced: Configuration

    Runs और Depth

    # foundry.toml
    

    [invariant]

    runs = 256 # Number of sequences

    depth = 15 # Calls per sequence

    fail_on_revert = false # Continue on revert

    call_override = false # Use handler selectors only

    Runs: ज्यादा runs = better coverage (slower)

    Depth: ज्यादा depth = longer sequences (complex states)

    Target Specific Functions

    function targetSelectors() public view returns (FuzzSelector[] memory) {
    

    FuzzSelector[] memory selectors = new FuzzSelector[](2);

    selectors[0] = FuzzSelector({

    addr: address(handler),

    selectors: _getDepositSelector()

    });

    selectors[1] = FuzzSelector({

    addr: address(handler),

    selectors: _getWithdrawSelector()

    });

    return selectors;

    }

    function _getDepositSelector() internal pure returns (bytes4[] memory) {

    bytes4[] memory selectors = new bytes4[](1);

    selectors[0] = Handler.deposit.selector;

    return selectors;

    }

    ---

    Debugging Failures

    जब invariant fail होता है:

    forge test --match-test invariant -vvvv

    Shrinking

    Foundry automatically minimal failing case ढूंढता है:

    [FAIL] invariant_totalSupply()
    
    

    Failing sequence:

    1. mint(actor=0x123, amount=1000)

    2. transfer(from=0x123, to=0x456, amount=500)

    3. burn(actor=0x123, amount=600) // ← Bug! Burned more than balance

    Call Summary

    function invariant_callSummary() public view {
    

    console.log("---");

    console.log("Deposit calls:", handler.calls("deposit"));

    console.log("Withdraw calls:", handler.calls("withdraw"));

    console.log("---");

    }

    Run करने पर:

    ---
    

    Deposit calls: 145

    Withdraw calls: 89

    ---

    ---

    Ghost Variables

    Contract state के अलावा tracking के लिए।

    contract Handler {
    

    // Ghost: Track max ever deposited

    uint public ghost_maxDeposit;

    function deposit(uint amount) public {

    // ...

    if (amount > ghost_maxDeposit) {

    ghost_maxDeposit = amount;

    }

    }

    }

    // Invariant: Max deposit < 1M

    function invariant_maxDepositBound() public {

    assertLt(handler.ghost_maxDeposit(), 1e6);

    }

    ---

    Production Checklist

    ✅ Before Mainnet

  • Core Invariants Defined
  • - Conservation (assets in = out)

    - Solvency (can always pay out)

    - Monotonicity (values don't decrease unexpectedly)

  • Handler Comprehensive
  • - All user actions covered

    - Multiple actors

    - Realistic bounds

  • Runs + Depth High
  • - Minimum: 256 runs × 50 depth

    - Recommended: 1000 runs × 100 depth

  • Ghost Variables Track Critical Metrics
  • - Total deposits/withdrawals

    - Max values

    - State transitions

  • CI Integration
  • # .github/workflows/test.yml

    - name: Invariant Tests

    run: forge test --match-test invariant

    ---

    Conclusion

    Invariant testing सबसे powerful testing technique है। Unit tests specific bugs catch करते हैं। Invariants entire classes of bugs eliminate करते हैं।

    Key Takeaways:

    • Invariants = mathematical guarantees
    • Handlers = bounded, realistic actions
    • Ghost variables = track off-chain state
    • Shrinking = minimal reproduction
    • High runs × depth = thorough testing

    Production DeFi protocol में invariant tests mandatory होने चाहिए। ये bugs catch करते हैं जो manual testing miss कर देती है। 🧪

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें