# 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:
---
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
totalAssets = sum of all deposits - sum of withdrawalstotalAssets >= convertToAssets(totalSupply)totalAssets never decreases without withdrawalshare 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
- Conservation (assets in = out)
- Solvency (can always pay out)
- Monotonicity (values don't decrease unexpectedly)
- All user actions covered
- Multiple actors
- Realistic bounds
- Minimum: 256 runs × 50 depth
- Recommended: 1000 runs × 100 depth
- Total deposits/withdrawals
- Max values
- State transitions
# .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 कर देती है। 🧪