Разработка контрактов на Foundry
Контракт написан, но пока это просто текст. Его надо собрать, обстрелять тестами, отладить по трейсу и задеплоить. Раньше для этого тянули тяжёлый JavaScript-тулинг, теперь есть Foundry, написанный на Rust быстрый набор инструментов, где тесты для Solidity пишут на самом Solidity. Тебе как человеку с Rust-бэкграундом он ляжет особенно удобно.
Идея
Foundry это три утилиты:
forgeсобирает контракты, гоняет тесты и fuzzing, деплоит.castэто швейцарский нож для запросов и транзакций в сеть прямо из терминала.anvilэто локальный узел-песочница, аналог regtest из урока 23, только для Ethereum.
Главная фишка: тесты пишут на Solidity. Не надо переключаться между языком контракта и языком тестов, весь цикл в одном.
forge init bank && cd bank # каркас проекта
forge build # компиляция в байткод EVM
forge test # прогон тестов
forge init кладёт в каркас шаблонный контракт Counter, он нам не нужен. Выкинь его и положи на его место собранный контракт Bank из урока 27:
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
# скопируй листинг Bank из прошлого урока в src/Bank.sol
forge build # должен собраться без ошибок
Теперь у проекта есть что тестировать, и импорт ../src/Bank.sol в тесте ниже сойдётся с реальным файлом.
Тесты на Solidity
Тест это контракт, наследующий Test из стандартной библиотеки forge. Метод setUp готовит стенд перед каждым тестом, методы с префиксом test это сами проверки, assertEq и родня сверяют результат.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Bank} from "../src/Bank.sol";
contract BankTest is Test {
Bank internal bank;
address internal alice = address(0xA11CE);
function setUp() public {
bank = new Bank();
}
function test_DepositIncreasesBalance() public {
vm.deal(alice, 1 ether); // выдать alice эфир
vm.prank(alice); // следующий вызов будет от имени alice
bank.deposit{value: 1 ether}();
assertEq(bank.balanceOf(alice), 1 ether);
}
}
Строчки с vm. это читкоды: они управляют тестовым EVM. vm.prank подменяет отправителя, vm.deal выдаёт баланс, vm.warp двигает время, vm.expectRevert требует отката. В реальной сети такого не сделать, а в тесте это ставит любой сценарий.
Fuzzing: тесты, которые ищут контрпример сами
Вот где Foundry особенно силён. Если у тест-функции есть аргументы, forge превращает её в fuzz-тест: гоняет её сотни раз со случайными и граничными значениями и, если находит контрпример, печатает его.
// forge подставит сюда сотни разных amount, включая 0 и максимум uint96
function testFuzz_DepositThenBalance(uint96 amount) public {
vm.deal(alice, amount);
vm.prank(alice);
bank.deposit{value: amount}();
assertEq(bank.balanceOf(alice), amount);
}
Это то же property-based тестирование, что мы разбирали в уроке про property-based и snapshot-тесты, только про деньги. Граничные uint256 особенно коварны из-за фиксированной разрядности, и fuzzer ловит переполнения и краевые случаи, которые руками не придумаешь. Для контрактов, где ошибка стоит реальных денег, это не роскошь.
У обстрела есть и вторая половина: invariant-тесты. Fuzz-тест проверяет свойство одного вызова, а invariant-тест утверждение обо всём контракте (например, «сумма балансов равна эфиру на контракте») и натравливает на него случайные последовательности вызовов: forge сам дёргает deposit и withdrawAll в произвольном порядке от разных адресов и после каждой цепочки сверяет инвариант. Пишутся они как функции с префиксом invariant_ в тест-контракте. Именно так ловятся баги, которые проявляются не на одном вызове, а на их сочетании: тот же класс, что reentrancy в The DAO.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.