Раздел 30 · Web3 на Rust

Разработка контрактов на Foundry

senior~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Разработка контрактов на 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-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.