Настройка форка mainnet для тестирования
Мок Uniswap пула для тестирования свопов — это имитация интерфейса без имитации экономики. Реальный пул Uniswap V3 имеет конкретную ликвидность в конкретных тиках, конкретную историю свопов, конкретный fee growth. Тест, который проходит против мока, может падать против реального пула — потому что tick spacing не тот, или liquidity в нужном диапазоне оказалась нулевой.
Форк mainnet решает это: берём реальное состояние всех контрактов на конкретном блоке и запускаем тесты против него локально.
Как это работает технически
Hardhat и Foundry реализуют форкинг через JSON-RPC: при обращении к адресу или storage slot, которого нет в локальном кэше, делают eth_getStorageAt / eth_getCode к удалённому узлу (Alchemy, Infura, QuickNode) и кэшируют ответ. Последующие обращения к тому же slot идут из кэша.
Это значит, что первый прогон тест-сьюта будет медленным (много запросов к RPC), а последующие — быстрыми (всё в дисковом кэше). Foundry кэширует в ~/.foundry/cache/, Hardhat — в cache/hardhat-network-fork/.
Настройка в Hardhat
// hardhat.config.ts
networks: {
hardhat: {
forking: {
url: process.env.ALCHEMY_MAINNET_URL!,
blockNumber: 19750000, // фиксируем блок
enabled: true,
},
chainId: 1, // важно: chainId должен совпадать с forked chain
}
}
Фиксация blockNumber обязательна для детерминированных тестов. Без неё каждый запуск берёт разный блок, и тесты могут падать или проходить в зависимости от текущего состояния пулов.
Для тестов, которым нужны свежие данные (например, проверка оракула Chainlink), используем hardhat_reset с новым blockNumber прямо в тесте:
await network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: process.env.ALCHEMY_MAINNET_URL,
blockNumber: 19800000,
}
}]
});
Настройка в Foundry
# foundry.toml
[profile.default]
fork_url = "${ALCHEMY_MAINNET_URL}"
fork_block_number = 19750000
Или в тесте через vm.createFork() / vm.selectFork() для переключения между форками в одном тест-файле — удобно при тестировании cross-chain логики:
uint256 mainnetFork = vm.createFork(vm.envString("ALCHEMY_MAINNET_URL"), 19750000);
uint256 polygonFork = vm.createFork(vm.envString("ALCHEMY_POLYGON_URL"), 55000000);
vm.selectFork(mainnetFork);
// тест на Ethereum
vm.selectFork(polygonFork);
// тест на Polygon
Манипуляция состоянием форка
Форк даёт реальные данные, но для тестов нужно часто их изменять: дать аккаунту токены, изменить параметры протокола, перемотать время.
Дать ETH аккаунту:
await network.provider.send("hardhat_setBalance", [
address, ethers.toQuantity(ethers.parseEther("100"))
]);
Дать ERC-20 токены — нужно найти storage slot балансового маппинга. Для большинства токенов это slot 0 или slot 1. Утилита hardhat-storage-layout или Foundry cast storage помогает найти нужный slot:
// для USDC балансы в slot 9
const slot = ethers.keccak256(
ethers.concat([ethers.zeroPadValue(address, 32), ethers.zeroPadValue("0x09", 32)])
);
await network.provider.send("hardhat_setStorageAt", [
USDC_ADDRESS, slot, ethers.toBeHex(amount, 32)
]);
Impersonate account — действовать от имени любого адреса, включая multisig или DAO:
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [WHALE_ADDRESS]
});
const whale = await ethers.getSigner(WHALE_ADDRESS);
// теперь можно вызывать контракты от имени whale
Перемотать время:
await time.increase(86400 * 7); // +7 дней
await time.setNextBlockTimestamp(specificTimestamp);
Что тестировать через форк
Взаимодействие с AMM. Swap через Uniswap V3 с реальной ликвидностью, проверка slippage, price impact для крупных объёмов. Мок не воспроизведёт ситуацию, когда liquidity в нужном tick range исчезла.
Flash loan атаки. Берём flash loan через Aave V3 (реальный контракт), пробуем манипулировать ценой, проверяем что защита в нашем контракте работает.
Интеграция с Chainlink. Проверяем, что контракт корректно читает price feed и обрабатывает случай устаревших данных (staleness check).
Работа с реальными токенами. USDT без return value, stETH с rebasing, USDC с blacklist — все особенности реальных токенов присутствуют в форке автоматически.
RPC и кэш
Хороший RPC критичен. Alchemy и QuickNode дают архивные узлы с историей всех storage slots. Infura бесплатный план ограничен и может давать rate limit при большом тест-сьюте.
Для CI рекомендуем кэшировать Foundry/Hardhat cache между запусками — это экономит 70-80% времени:
- uses: actions/cache@v3
with:
path: ~/.foundry/cache
key: foundry-fork-${{ env.FORK_BLOCK_NUMBER }}
Настройка среды с форком занимает 1 рабочий день. Стоимость рассчитывается индивидуально.







