# Foundry के साथ Mainnet Fork Testing
Unit tests साबित करते हैं कि आपका logic अलगाव में काम करता है। Fork tests साबित करते हैं कि यह एक live chain के असली, बिखरे हुए state के खिलाफ काम करता है: असली Uniswap pools, असली Aave liquidity, और एक ऐसे token का exact storage जिसे आप control नहीं करते। Foundry इसे लगभग मुफ्त बना देता है। आप एक test को किसी RPC URL की ओर इशारा करते हैं, और EVM माँग पर live state पढ़ता है जबकि आपके transactions local रहते हैं। यह गाइड fork cheatcodes, whales को impersonate करना, block numbers के साथ time travel, RPC configuration और एक साफ CI setup को कवर करती है।
Fork tests क्यों मायने रखते हैं
External protocols को mock करना तेज है लेकिन यह आपसे झूठ बोलता है। एक mock किया हुआ Uniswap router कभी भी slippage पर वैसे revert नहीं होता जैसे असली होता है, और एक mock ERC20 fee-on-transfer व्यवहार या एक frozen blacklist को दोहरा नहीं सकता। Forking अनुमान को हटा देता है:
- आप deployed protocols के असली bytecode के खिलाफ integrate करते हैं।
- आप live storage पढ़ते हैं: balances, reserves, oracle prices, governance state।
- आप ऐसे integration bugs पकड़ते हैं जिन्हें mocks structurally दोहरा ही नहीं सकते।
लागत है हर cold storage slot के लिए एक network round trip। Foundry उन reads को cache करता है, इसलिए दूसरा run तेज होता है।
RPC configuration
कभी भी किसी test में API key को hardcode न करें। Foundry foundry.toml से named endpoints पढ़ता है और runtime पर environment variables को resolve करता है।
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
Secrets को एक .env file में रखें (और इसे .gitignore में जोड़ें):
MAINNET_RPC_URL=https://eth-mainnet.example.com/v3/your-key
ARBITRUM_RPC_URL=https://arb-mainnet.example.com/v3/your-key
अब vm.createFork("mainnet") alias को URL में resolve कर देता है। आप एक raw URL string भी pass कर सकते हैं, लेकिन alias secrets को आपके source tree से बाहर रखता है।
createFork बनाम createSelectFork
Foundry forks को ऐसे objects की तरह मानता है जिन्हें आप id से पकड़ सकते हैं और जिनके बीच switch कर सकते हैं।
vm.createFork(alias)एक fork बनाता है और उसकाuint256id लौटाता है, उसे activate किए बिना।
vm.createSelectFork(alias)एक fork बनाता है और तुरंत उसे active EVM बना देता है।
vm.selectFork(id)active fork को पहले से बनाए गए किसी fork में switch करता है।
यह दो-चरण वाला pattern तब चमकता है जब एक ही test एक से अधिक chain को छूता है, उदाहरण के लिए एक cross-chain bridge:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
contract MultiForkTest is Test {
uint256 internal mainnetFork;
uint256 internal arbitrumFork;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
mainnetFork = vm.createFork("mainnet");
arbitrumFork = vm.createFork("arbitrum");
}
function test_activeForkSwitches() public {
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
// Live mainnet पर USDC की supply शून्य से अधिक है।
assertGt(IERC20(USDC).balanceOf(USDC), 0);
vm.selectFork(arbitrumFork);
assertEq(vm.activeFork(), arbitrumFork);
}
}
हर fork अपना state रखता है, इसलिए आगे-पीछे switch करने से chains के बीच balances या storage leak नहीं होता।
rollFork के साथ एक block pin करना
एक चलते हुए "latest" block पर pin किया गया fork tests को flaky बना देता है: liquidity, prices और balances हर run के बीच बदलते रहते हैं। एक specific block number पर pin करें ताकि हर run एक जैसा state देखे।
आप creation के समय pin कर सकते हैं, vm.createSelectFork("mainnet", 20_000_000), या एक मौजूदा fork को vm.rollFork से आगे बढ़ा सकते हैं:
function test_pinAndRoll() public {
uint256 fork = vm.createSelectFork("mainnet", 20_000_000);
assertEq(block.number, 20_000_000);
// State परिवर्तन देखने के लिए fork को एक बाद के block तक आगे बढ़ाएँ।
vm.rollFork(20_100_000);
assertEq(block.number, 20_100_000);
}
एक non-active fork को roll करने के लिए vm.rollFork(forkId, blockNumber) भी है, और एक variant जो किसी specific transaction hash के block तक roll करता है, जो किसी incident को बिल्कुल वैसे ही on chain दोहराने के लिए उपयोगी है जैसे वह हुआ था।
Pinning एक performance फायदा भी है। एक pinned block पहले run के बाद disk पर cache हो जाता है, इसलिए बाद के runs network को पूरी तरह छोड़ देते हैं।
Accounts को impersonate करना
एक deposit test करने के लिए आपको tokens चाहिए। एक fork पर आपके पास किसी whale की private keys नहीं होतीं, लेकिन आपको उनकी जरूरत भी नहीं। vm.prank और vm.startPrank अगले call (या stopPrank तक हर call) को किसी भी address से आता हुआ दिखाते हैं।
function test_impersonateWhale() public {
vm.selectFork(mainnetFork);
address whale = 0x55FE002aefF02F77364de339a1292923A15844B8; // एक USDC holder
address recipient = address(0xBEEF);
uint256 amount = 1_000e6; // USDC में 6 decimals हैं
uint256 beforeBal = IERC20(USDC).balanceOf(recipient);
vm.startPrank(whale);
IERC20(USDC).transfer(recipient, amount);
vm.stopPrank();
assertEq(IERC20(USDC).balanceOf(recipient), beforeBal + amount);
}
अगर कोई सुविधाजनक holder मौजूद नहीं है, तो vm.deal सीधे एक balance को overwrite कर देता है। Native ETH के लिए, vm.deal(addr, 100 ether)। ERC20s के लिए, तीन-argument वाला रूप आपके लिए storage slot की गणना करके लिख देता है:
// Balance slot सीधे लिखकर recipient को 10,000 USDC दें।
deal(USDC, recipient, 10_000e6);
ध्यान दें कि tokens के लिए deal balances mapping के slot का अनुमान लगाकर काम करता है। यह standard layouts को संभाल लेता है, लेकिन exotic tokens (असामान्य storage वाले proxies, rebasing tokens) को एक manual vm.store की जरूरत हो सकती है।
Forks के बीच persistent contracts
जब आप selectFork करते हैं, तो test में deploy किए गए contracts मिट जाते हैं क्योंकि हर fork का अपना state होता है। अगर आपको चाहिए कि कोई helper contract fork switch के बाद भी बचा रहे, तो उसे persistent mark करें:
function test_persistentHelper() public {
MyHelper helper = new MyHelper();
vm.makePersistent(address(helper));
vm.selectFork(mainnetFork);
// यहाँ भी helper के पास code है।
vm.selectFork(arbitrumFork);
// यहाँ भी helper के पास code है।
}
Test contract खुद और cheatcode address default रूप से persistent होते हैं, यही वजह है कि switch के बाद आपके assertions काम करते रहते हैं।
एक realistic integration test
सब कुछ जोड़कर, यह एक ऐसे test का आकार है जो live state के खिलाफ एक vault deposit में असली USDC डालता है:
function test_depositRealUsdc() public {
vm.createSelectFork("mainnet", 20_000_000);
address user = makeAddr("user");
deal(USDC, user, 5_000e6);
Vault vault = new Vault(USDC);
vm.startPrank(user);
IERC20(USDC).approve(address(vault), 5_000e6);
uint256 shares = vault.deposit(5_000e6);
vm.stopPrank();
assertGt(shares, 0);
assertEq(IERC20(USDC).balanceOf(address(vault)), 5_000e6);
}
यह असली USDC contract, असली decimals, असली transfer semantics तक पहुँचता है। अगर USDC कभी user को blacklist करे या व्यवहार बदले, तो यह test उसे पकड़ लेगा।
चलाना और CI
Suite को ऐसी verbosity के साथ चलाएँ जो failure पर traces सामने लाए:
forge test --match-contract MultiForkTest -vvv
CI में, RPC URL को एक secret के रूप में set करें और fork cache को बार-बार होने वाले runs को तेज करने दें। एक न्यूनतम GitHub Actions step:
# Job environment में
export MAINNET_RPC_URL="${{ secrets.MAINNET_RPC_URL }}"
forge test --fork-block-number 20000000
CI के लिए दो practical सुझाव:
~/.foundry/cache को cache करें। Foundry वहाँ fetched RPC state store करता है, जिससे बाद के runs पर runtime और provider usage दोनों घटते हैं।एक dedicated fork-test job जो एक schedule पर चलता है (हर push पर नहीं) आपकी तेज unit suite को green रखता है जबकि live integrations का भी अभ्यास करता रहता है।
इसका practical अभ्यास करें
Fork tests के बारे में पढ़ना एक बात है; एक ऐसा test लिखना जो असली integration bug पकड़े, दूसरी बात है। app.solingo-blockchain.xyz पर आप browser में Foundry-style exercises चला सकते हैं, accounts को impersonate कर सकते हैं और अपने assertions को step by step verify कर सकते हैं। Testing track खोलें, live protocol state के खिलाफ fork करें, और ऐसे contracts ship करें जिन पर आप वाकई भरोसा करते हैं।