Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
Практическое введение в Foundry: установка, структура проекта, unit-тесты, читкоды vm, фаззинг и инварианты, forge, cast и anvil. С рабочим примером.
В этой статье
Foundry - набор инструментов для разработки смарт-контрактов, где тесты пишутся на самом Solidity. Он быстрый и удобный, поэтому стал стандартом во многих командах. Эта статья - практическое введение: от установки до фаззинг-теста. Теория тестирования - в статье тестирование смарт-контрактов, язык - в обзоре Solidity.
Что входит в Foundry
| Инструмент | Для чего |
|---|---|
forge | сборка, тесты, развёртывание, отчёты о газе |
cast | команды для общения с сетью из терминала |
anvil | локальный блокчейн для разработки |
chisel | интерактивная консоль Solidity |
Установка и проект
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init escrow-demo
cd escrow-demoСтруктура проекта:
src/ исходный код контрактов
test/ тесты (файлы *.t.sol)
script/ скрипты развёртывания
lib/ зависимости (forge-std и др.)
foundry.toml настройки: версия компилятора, оптимизаторПример настроек, близких к тем, что использует наш контракт:
[profile.default]
solc = "0.8.24"
evm_version = "paris"
optimizer = true
optimizer_runs = 200
bytecode_hash = "none"Эти параметры делают сборку воспроизводимой - это важно для проверки контракта.
Контракт для примера
Возьмём упрощённую версию расчёта долей из эскроу (разбор математики):
// src/Splitter.sol
library Splitter {
uint256 internal constant BPS = 10_000;
uint256 internal constant PLATFORM_FEE_BPS = 100; // 1%
function split(uint256 amount, uint256 arbiterFeeBps, uint256 buyerShareBps)
internal
pure
returns (uint256 buyer, uint256 seller, uint256 arbiter, uint256 platform)
{
platform = (amount * PLATFORM_FEE_BPS) / BPS;
uint256 net = amount - platform;
arbiter = (net * arbiterFeeBps) / BPS;
uint256 pot = net - arbiter;
buyer = (pot * buyerShareBps) / BPS;
seller = pot - buyer;
}
}Первый тест
// test/Splitter.t.sol
import {Test} from "forge-std/Test.sol";
import {Splitter} from "../src/Splitter.sol";
contract SplitterTest is Test {
function test_completedDeal() public {
// 1000 USDC (6 знаков), гарант 2%, всё продавцу
(uint256 b, uint256 s, uint256 a, uint256 p) = Splitter.split(1_000_000_000, 200, 0);
assertEq(p, 10_000_000); // 10 USDC платформе
assertEq(a, 19_800_000); // 19,80 гаранту
assertEq(b, 0);
assertEq(s, 970_200_000); // 970,20 продавцу
}
}Запуск:
forge test -vvvФлаги -v показывают подробности: чем больше v, тем больше деталей (логи, трассировки).
Фаззинг: тысячи случайных входов
Вместо проверки нескольких чисел сформулируем свойство: сумма частей всегда равна целому.
function testFuzz_partsEqualWhole(uint96 amount, uint16 arbBps, uint16 buyerBps) public {
arbBps = uint16(bound(arbBps, 0, 3000)); // ограничим разумными значениями
buyerBps = uint16(bound(buyerBps, 0, 10_000));
(uint256 b, uint256 s, uint256 a, uint256 p) = Splitter.split(amount, arbBps, buyerBps);
assertEq(b + s + a + p, amount);
}Foundry подставит сотни случайных значений (по умолчанию 256) и при ошибке покажет минимальный контрпример. bound аккуратно приводит значение в нужный диапазон, а vm.assume(cond) отбрасывает неподходящие.
Такие тесты ловят то, о чём вы не подумали: сумма 0, максимум uint96, граничные доли.
Читкоды vm
Тестовый контракт наследует Test, а значит получает объект vm с «читами», управляющими окружением.
| Чит | Что делает |
|---|---|
vm.prank(addr) | следующий вызов выполнится от addr |
vm.startPrank(addr) / vm.stopPrank() | то же для серии вызовов |
vm.expectRevert(селектор) | следующий вызов обязан откатиться с этой ошибкой |
vm.warp(время) | устанавливает block.timestamp |
vm.roll(блок) | устанавливает номер блока |
deal(addr, сумма) | выдаёт ETH на адрес (есть вариант для токенов) |
vm.expectEmit(...) | проверяет событие |
makeAddr("alice") | создаёт тестовый адрес |
Примеры проверок для эскроу
function test_strangerCannotConfirm() public {
uint256 id = _createActiveDeal();
vm.prank(makeAddr("stranger"));
vm.expectRevert(Escrow.NotAuthorized.selector);
escrow.confirmDelivery(id);
}
function test_arbiterTimeoutSplitsEvenly() public {
uint256 id = _createDisputedDeal();
vm.warp(block.timestamp + 30 days + 1);
vm.prank(buyer);
escrow.claimArbiterTimeout(id);
// проверяем: гарант 0, покупатель и продавец поровну
}Так проверяются правила из контроля доступа и жизненного цикла сделки: кто и когда может действовать, и что происходит по таймауту.
Инварианты
Инвариантный тест проверяет свойство, которое должно быть верно после любой последовательности вызовов. Foundry случайно вызывает функции контракта и после каждого шага проверяет условие:
function invariant_solvency() public view {
assertGe(token.balanceOf(address(escrow)), escrow.reserved(address(token)));
}Это тот самый инвариант платёжеспособности: баланс контракта не меньше обязательств (пояснение).
Газ и покрытие
forge test --gas-report # таблица расхода газа по функциям
forge snapshot # сохраняет эталонные значения газа
forge coverage # какие строки покрыты тестамиРезультаты помогают оптимизировать газ и видеть непроверенные ветки кода.
Локальная сеть и cast
anvil # блокчейн на localhost:8545, chain id 31337
cast sig "transfer(address,uint256)" # 0xa9059cbb - селектор
cast keccak "hello" # хеш keccak256
cast call <адрес> "balanceOf(address)(uint256)" <кошелёк> --rpc-url http://127.0.0.1:8545anvil создаёт тестовые аккаунты с ETH, на них удобно отрабатывать весь поток сделки локально, не тратя реальных денег. Селекторы и хеши - тема статей про ABI и хеш-функции.
Форк реальной сети
Тесты можно запускать на копии реальной сети (только чтение состояния, ничего не отправляется):
forge test --fork-url $RPC_URLТак проверяют взаимодействие с настоящими токенами вроде USDC - например, как они ведут себя при блокировке адреса.
Советы
- Тестируйте не только «счастливый путь», но и отказы, и граничные значения.
- Пишите по фаззинг-тесту на каждую арифметическую функцию.
- Один тест - одна идея, понятные имена:
test_..._reverts_when_.... - Запускайте тесты в CI при каждом изменении.
- Тесты не заменяют аудит, а дополняют его.
Что почитать дальше
Узнайте, как проверяют контракты независимые эксперты: аудиты и баг-баунти. Или как сократить расход газа: оптимизация газа.
Читайте также
- 3 · Под капотом
Тестирование смарт-контрактов: unit, fuzz, инварианты и статический анализ
Как проверяют контракты с деньгами: юнит-тесты, фаззинг, инварианты платёжеспособности, «злые» моки токенов, Slither и Mythril на примере эскроу с Foundry.
5 мин чтения - 3 · Под капотом
Обзор языка Solidity: типы, функции, ошибки и события на примерах
Короткий практический тур по Solidity для тех, кто умеет читать код: структура контракта, типы, видимость, модификаторы, ошибки и события. Примеры из эскроу.
4 мин чтения
Упоминается в статьях
- Словарь терминов: блокчейн, кошельки, смарт-контракты и эскроу от A до Я
- Обзор языка Solidity: типы, функции, ошибки и события на примерах
- ABI: как вызов функции превращается в байты - селекторы, calldata и события
- ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
- Целочисленная математика и округление в смарт-контрактах
- Раскладка хранилища и упаковка переменных в Solidity
- Оптимизация газа в Solidity: 12 приёмов и когда они уместны
- Абстракция аккаунтов (ERC-4337): умные кошельки и спонсирование газа
- Тестирование смарт-контрактов: unit, fuzz, инварианты и статический анализ
- Аудиты и баг-баунти: как проверяют смарт-контракты и как читать отчёт
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

