Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Foundry с 0. Часть 15

В завершении этой недели практики с Foundry стоит поговорить об организации файлов при написании тестов. Вы ведь сами понимаете, что "пихать" все тесты в один файл это не только глупо, но и трудночитаемо.

Попробуем представить, какие тесты нам могут потребоваться для нашего проекта:

1. Простые тесты, которые проверят работу функций;
2. Фаззинг тесты;
3. Инвариант тесты;
4. Форк тесты;
5. Интеграционные тесты, когда мы проверяем, что наш протокол правильно взаимодействует со сторонними контрактами, типа Uniswap или Chainlink;

Более того, нам, возможно, нужно будет проверить работу протокола с токенами и другими шаблонами, ну, и не стоит забывать про какие-нибудь хелперы или библиотеки, которые мы можем написать исключительно для наших тестов.

В целом, наша папка test может получиться такой:

test

|-- differential
|-- fork
|-- fuzzing
|-- integartion
|-- invariant
|-- mocks
|-- scenario
|-- unit
|-- utils

|- simple tests



В mocks хранятся шаблоны токенов и других контрактов, которые мы хотим развернуть для тестов, в scenario - сценарии логических тестов, в unit - тесты для условий и порождения событий, в utils - хелперы и библиотеки.

Это самый полный список папок и тестов, который мне удалось найти и собрать вместе. Также не стоит забывать, что было бы хорошо создать подробный readme.md файл или другую документацию с описанием тестов для последующей работы разработчиков и аудиторов.

Я бы также рекомендовал бы добавить сюда отдельную папку hacks, в которую помещать все poc тесты после аудита.

Как вы можете заметить, написание тестов занимает практически столько же времени, сколько и написание самих контрактов, если не больше. Это очень кропотливая работа, на грани с полноценным аудитом.

Надеюсь, этот пост поможет вам лучше составлять архитектуру своих тестов и проверять свой проект вдоль и поперек.

#foundry #lesson15
👍1🔥1
Foundry c 0. Содержание 1

Прошло уже три недели с момента, как мы начали разбирать тестирование с Foundry. Пришло время собрать материал в один пост для более удобной навигации.


Общий план цикла постов


Итак, за этом время мы прошли:


Введение


Foundry с 0. Часть 0. Установка

Foundry с 0. Часть 1. Из чего состоит Foundry?

Foundry с 0. Часть 2. Cast команды (1)

Foundry с 0. Часть 3. Cast команды (2)

Foundry с 0. Часть 4. Cast команды (3)

Foundry с 0. Часть 5. Chisel

Foundry с 0. Часть 6. Конфигурация Foundry

Foundry с 0. Часть 7. Библиотеки

Foundry с 0. Часть 8. Старт проекта

Foundry с 0. Часть 9. Виды тестов

Foundry с 0. Часть 10. Ресурсы



Базовая практика


Foundry с 0. Часть 11. Простые тесты

Foundry с 0. Часть 12. Разные assert

Foundry с 0. Часть 13. Fail тесты

Foundry с 0. Часть 14. Читкоды и prank()

Foundry с 0. Часть 15. Организация файлов



Даже учитывая то, что по Foundry сейчас можно найти достаточно много информации, все равно бывает сложно перевести ее и написать понятным языком.

Буду рад, если поддержите лайком и сделаете репост.

#foundry #summary
👍23🔥41
Foundry с 0. Часть 16

Думал тему из этого поста постепенно поднимать в нескольких других, но понял, что лучше основную идею изложить сейчас, а потом уже дополнять при необходимости.

А поговорим мы о работе с консолью: вызовом тестов и логированием из них.

Из предыдущих постов мы узнали, для того чтобы вызвать процесс тестирования нашего контракта, достаточно в консоли прописать:

forge test


или

forge test -vv


и других "-v..." для более подробной информации.

Тем не менее, порой, когда тестовых контрактов и самих тестов очень много, нам нужно вызвать что-то конкретное, и ждать выполнения всех тестов из всех контрактов может стоит нам времени.

Для этого существуют удобные дополнения к команде test:

1. forge test --match-test testName

Выполним конкретный тест из контрактов.

2. forge test --match-contract contractName

Выполним все тесты из конкретного контракта.

3. forge test --match-path Path

Выполнить тесты по указанному пути.

Так же мы может исключить какой-нибудь контракт или тест из процесса тестирования с помощью опций:

--no-match-test
--no-match-contract
--no-match-path


Более того, с помощью таких опций-хелперов мы можем подключать optimizer и указывать количество проходов:

--optimize
--optimizer-runs
--via-ir


Или указать EVM версию для наших тестов:

--evm-version

Как вы, надеюсь, поняли, перед опцией должно быть forge test, а после указание на файл / тест / версию и так далее.

Но, что самое интересное, эти команды можно комбинировать для точечных тестов, например:

forge test --match-contract myContract --match-test myTest --evm-version london


Также есть еще другие опции для тестов на форках, отслеживание газа и дебаггинг, но о них мы будем говорить уже в своих постах.

Все опции для тестов можно посмотреть тут:

https://book.getfoundry.sh/reference/forge/forge-test

Теперь пара слов о логировании в тестах.

Порой нам нужно будет выводить значения из наших тестов, чтобы узнать, кто был вызывающим функции, изменение баланса или результат деления. Для этого мы можем просто написать:

console.log(param);


Однако, если таких вызовов много в ходе выполнения теста, то можно очень просто потеряться в них. Поэтому предлагаю вам пару способов для удобного вывода информации.

1. Подписывать логи:

console.log("This is owner: ", contract.owner());


Будет так:

Logs:
This is owner: 0x00c7bF7d9E7D071Df3B53dEec96cD4bf0f6c0220


2. Разделять по строкам:

console.log("This is owner");
console.log(contract.owner());


Будет так:

Logs:
This is owner
0x00c7bF7d9E7D071Df3B53dEec96cD4bf0f6c0220


3. Выделять дефисами:

console.log("This is owner ----------- ", contract.owner());


Будет так:

Logs:
This is owner ---------- 0x00c7bF7d9E7D071Df3B53dEec96cD4bf0f6c0220


4. Оставлять пробелы, используя пустые логи:

console.log(" ");

5. Выделять разным цветом.

Для этого потребуется в импорте тестов добавить библиотеку StdStyle:

import {Test, console, StdStyle} from "forge-std/Test.sol";

После этого можно применять цвета к логам:

console.log("Owner: ", StdStyle.yellow(counter.owner()));


Стилей достаточно много: красный, зеленый, желтый, синий, мажента, голубой, жирный, приглушенный, курсив, зачеркнутый и обычный.

Попробуйте сами, так наглядность логов в ваших тестах станет еще лучше!

#foundry #lesson16
7👍1
Foundry с 0. Часть 17

Пару слов, думаю, стоит сказать о том, как работать с отчетами по газу и покрытия тестами. Начнем со второго.

Что мне нравилось в Hardhat, так это то, что отчеты там можно было генерировать в формате html сразу, без дополнительных "плясок с баяном". В Foundry дело обстоит несколько сложнее, особенно для пользователей Windows.

Вообще, для того, чтобы посмотреть отчет о покрытии кода тестами, нужно выполнить простую команду:

forge coverage


В терминале прогонятся все созданные тесты и по итогу появится небольшая таблица, где будет показано покрытие кода в процентном соотношении. Сама таблица состоит из 4 колонок: lines, statements, branches, funcs: которые отвечают за покрытие тестами строк, условий, утверждений и функций.

Да, как вы можете понять, что покрытие 100% функций вовсе не означает, что покрытие всего контракта будет равняется 100%. В самих функция бывает еще много чего для проверки.

Также есть команда:

forge coverage --report debug


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

Ну, и последняя команда:

forge coverage --report lcov


которая сформирует отчет в формате файла .info, и вот тут начинаются "танцы".

С помощью этого файла можно было бы сформировать удобный отчет в формате html (простой веб страницы), как это делается в Hardhat, но есть некоторые проблемы...

В сети я нашел рекомендации формировать такой отчет с помощью программы genhtml. Делается это также в терминале командой:

https://linux.die.net/man/1/lcov

genhtml -o report --branch-coverage


или другими, которые можно посмотреть тут:

https://manpages.ubuntu.com/manpages/xenial/man1/genhtml.1.html

Однако эта прога работает только с системами Linux и на Windows не ставится. По крайней мере, сделать у меня это без напряга не получилось.

Другие программы или даже плаггины в самом VScode работают крайне криво, и я не могу порекомендовать какую-либо из них.

P.S. Если вы знаете что-то, что поможет конвертировать файлы из lcov в html без потери данных, прошу написать в комментариях.

Тем не менее, хочу рассказать о другом плагине, который помогает просматривать покрытие тестами прямо в контракте - Coverage Gutters (у меня от ryanluker).

После его установки и генерации lcov.info с forge, вы можете зайти в Command Palette (ctrl+shift+p), не уверен, как это переводится на русский, и выбрать Display Coverage (ctrl+shift+7). Код в контракте выделится красными, желтыми и зелеными полосками, которые как раз и обозначают места, требующие вашего внимания.

В принципе, достаточно удобно просматривать код и понимать, что еще нужно протестировать.

Единственный момент, о котором редко говорят в теме тестов, это то, что покрытие кода на 100% не значит, что он безопасный на 100% и функции выполняют задумку разработчиков.

Есть еще логические баги и уязвимости, которые необходимо проверять отдельными тестами - сценариями использования протокола. Об этом мы будем говорить чуть позже.

Так же существует отчет по газу, который можно получить командой:

forge test --gas-report


Он показывает расход газа на ту или иную функцию, а также количество вызовов этой функции в контракте.

С помощью этого отчеты вы можете отслеживать свои модификации кода и применение различных паттерном снижения использования газа в своем случае.

Вместе с этим вы можете скачать для себя популярный анализатор кода - 4naly3er.

С помощью него можно быстро сканировать код на предмет популярных недочетов в коде, исправив которые вы сэкономите немного газа и даже сможете найти мелкие недочеты в своем коде.

В общем, полезная штука, которая используется на конкурсной площадке code4rena и на основе которой создаются более мощные программы для анализа кода.

#foundry #lesson17
🔥6
Foundry с 0. Часть 18

Совсем забыл, что хотел еще поднять тему интеграций Foundry в проект Hardhat и наоборот.

Начнем с того, что у вас есть проект на Hardhat и вы хотите подключить Foundry и писать дальше тесты на нем.

Нужно уточнить, что Foundry в своей работе сильно полагается на работу с Git, поэтому перед началом работы следует убедиться, что в проекте есть связка с git. Если же ее нет, то для начала стоит выполнить команду:

git init

Затем убедитесь, что Foundry также установлен на компьютере, на котором вы работаете:

forge --version

Если терминал выдаст ошибку, то стоит вернуться в самое начало и установить Foundry с нуля.

Однако предположим, что все у нас было установлено и git в проекте есть, поэтому далее выполняем команду:

npm install --save-dev @nomicfoundation/hardhat-foundry


и после добавляем запись в файл hardhat.config.ts:

import "@nomicfoundation/hardhat-foundry";


Для завершения установки Foundry в проект исполняем:

npx hardhat init-foundry


что создаст файл foundry.toml и загрузит библиотеки forge-std.

После этого можно будет писать тесты на Foundry в Hardhat проекте.

Также, если вы хотите использовать Hardhat в Foundry проекте, то следует выполнить следующую команду:

npm install --save-dev hardhat @nomicfoundation/hardhat-foundry


и добавить запись в файл hardhat.config.js:

require("@nomicfoundation/hardhat-foundry"); 


Теперь вы сможете писать тесты на javanoscript / typenoscript.

Сейчас я все чаще встречаю комбинированные тесты для больших протоколов, поэтому, уверен, нужно знать оба способа интеграций одной программы для тестов в другую.

#foundry #lesson18
👍1
Foundry с 0. Часть 19

Сегодня мы поговорим о деплое контрактов в локальную, тестовые и обычные блокчейн сети нашего контракта, а также о его верификации, публичных сервисах нод и работе с приватными ключами.

Изначально я планировал разделить это все на несколько постов, но решил, что для большей наглядности будет лучше объединить все в одном. Практиковаться будем с нашим контрактом Counter из предыдущих постов.

Для начала поднимем тему rpc-url и приватных ключей, а точнее, работу с ними в написании тестов.

В постах про cast команды нам нужно было часто указывать специальную ссылку для подключения к блокчейн узлам, чтобы можно было получать необходимую информацию. Мы писали что-то типа:

cast chain-id --rpc-url link


где вместо link нужно было подставить https://blablabla... Или же создавали кошелек на основе приватного ключа:

cast wallet address --private-key PRIVATE_KEY


где вместо PRIVATE_KEY указывали настоящий приватный ключ.

Я точно не могу сказать и рассказать почему, но это считается не безопасным способом, т.е. не рекомендуется указывать свои конфиденциальные данные в терминале. Возможно, потому что эту информацию могут получить хакеры через вирусы, или потому что VScode отправляет массу информации в Microsoft... В общем, не делайте так.

Для вашего личного тестирования смарт контрактов, более безопасным будет использование переменных среды. Как это сделать?

Нам нужно будет в корне нашего проекта создать отдельный файл ".env". Обратите внимание, что точка в начале названия обязательна.

Уже в этом файле мы делаем запись:

RPC_LINK=https://mainnet.infura.io/v3/...
PRIVATE_KEY=ndsf349jf9...


и другие по необходимости.

Затем в терминале выполняем команду:

source .env


К слову сказать, что тут вам потребуется установленный python на свой компьютер.

Также, возможно, у вас будут возникать ошибки, типа "command not found" или какие-то вроде них. Это означает, что переменные среды не применяются для проекта. В моем случае помогли:

1. Установка pytnon
2. Переход на bash терминал с PowerShell
3. Написание переменных, знака равенства и значения вместе
4. Возможно, вам также потребуется добавить слово export перед переменной

После этого необходимо проверить, чтобы запись .env была в файле .gitignore во избежание случайной загрузки конфиденциальных данный на сервера GitHub в открытый доступ. Если записи нет, то просто добавьте ее вручную.

Теперь команды cast можно выполнять так:

cast chain-id --rpc-url $RPC_LINK 


Обратите внимание на знак $ перед RPC_LINK!

P.S. В Части 2 я писал, где можно получить rpc-url бесплатно.

Так вы сможете скрыть конфиденциальную информацию с команд терминала.

Есть еще более безопасный вариант использования информации о приватных ключах в командах терминала, но он задействует keystore json файл с паролем. Это чуть более продвинутый способ и, если захотите, то напишу отдельный пост на эту тему. А пока идем дальше.

Итак, на данный момент у нас должна быть rpc ссылка от одного из сервисов нод блокчейна, и мы должны понимать, что такое переменные среды. Все это нужно для того, чтобы научиться делать деплой контрактов.

Сам деплой можно делать в локальный блокчейн Anvil, в тестовые сети, типа Goerly, или в основные сети. Начнем с первого.

Для запуска Anvil достаточно выполнить команду в терминале:

anvil

При этом нам предоставят 10 аккаунтов для проведения тестов вместе с их приватными ключами, мнемоник фразу, chain id и rpc ссылку, которая выглядит как: Listening on 127.0.0.1:8...

Более того, блокчейн установит для вас base fee, gas limit и timestamp, которыми вы можете управлять в случае необходимости.

В целом, деплой контрактов в различные сети ничем особым не отличается, происходит это с помощью специального скрипта, который тоже пишется на Solidity.

Заходим в папку noscripts и открываем / создаем новый файл, назовем его Counter.s.sol.

Обратите внимание, что по принципу названия файлов для тестов, где окончание t.sol обязательно, в данном случае для скриптов также обязательно окончание s.sol.

В этом файле создаем следующий скрипт:
🔥1
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";

contract CounterScript is Script {
function run() public {
vm.startBroadcast();
Counter counter = new Counter();
vm.stopBroadcast();
console2.log(address(counter));
}
}


Служебная функция run() как раз и служит для деплоя, vm.startBroadcast и vm.stopBroadcast еще пара читкодов, которые позволяют отправлять транзакции в блокчейн.

Открываем новый терминал, не удаляя тот, где запущен Anvil, и выполняем команду:

forge noscript noscript/Counter.s.sol


В итоге у нас будет будет что-то вроде:

Script ran successfully.
Gas used: 228424

== Logs ==
0x90193C961A926261B75...


Был ли в данном случае выполнен деплой? Нет, сейчас просто прогнался наш контракт как скрипт.

Как я понял из сторонних статей и объяснений на форумах, эта простая команда нужна для того, чтобы проверить наличие ошибок при деплое контрактов.

Также симулировать деплой можно с помощью похожей команды, указав rpc ссылку сети, в которую будет идти деплой. Например, для Anvil:

forge noscript noscript/Counter.s.sol --rpc-url http://127.0.0.1:8...


При этом вам могут показать некоторые предупреждения, как например:

EIP-3855 is not supported in one or more of the RPCs used.
Unsupported Chain IDs: 31337.
Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly.


И уже для полноценного деплоя нам нужно указать приватный ключ от аккаунта, с которого будет идти деплой контрактов в сеть, а также добавить ключевое слово --broadcast:

forge noscript noscript/Counter.s.sol --rpc-url http://127.0.0.1:8.. --private-key $PRIVATE_KEY --broadcast


Теперь, если мы откроем терминал, где у нас был запущен блокчейн, то можем увидеть новую запись:

[Success]Hash: 0xdb858b56859825d35f6bffe022c9524e640869974e219387a87b4d542ce311f1
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.0009359 ETH (233975 gas * 4 gwei)


Кстати, сюда же мы можем отправлять транзакции через cast send и cast call, так как это полноценный контракт в блокчейне.

Также мы можем заметить, что в древе файлов нашего проекта появилась новая папка broadcast. Здесь находится информация по деплою нашего контракта.

Существует еще более директивный способ деплоя контрактов в сеть с помощью команды

forge create

Здесь вам также нужно будет указывать rpc ссылку и приватный ключ.

Больше информации тут: https://book.getfoundry.sh/reference/forge/forge-create

При этом с create, те же аргументы для контрактов нужно будет также прописывать через дополнительные команды, типа --constructor-args, в то время как для скриптовой версии, все это указывается как в обычном коде Solidity.

Скриптовой вариант более удобнее и проще, в особенности для деплоя множества контрактов.

Ну, и в завершение, пару слов о верификации контрактов.

При обычном способе, нам нужно было бы заходить на тот же etherscan и вручную указывать многие параметры нашего контракта, включая его код на Solidity. При этом, по слухам, было много шансов ошибиться на каком-либо шаге и каждый раз начинать все заново. С Foundry этот процесс стал проще.

Для этого мы можем добавить команду --verify в команду исполнения скрипта:

forge noscript noscript/Counter.s.sol --rpc-url http://127.0.0.1:8... --private-key $PRIVATE_KEY --broadcast --verify


Вообще для скриптов написали достаточно много команд, их можно посмотреть тут:

https://book.getfoundry.sh/reference/forge/forge-noscript

Дополнительно темы деплоя мы будем касаться в постах, где это будет необходимо, указывая на новые нюансы и опции.

А пока что, попробуйте потренироваться сами.

#foundry #lesson19
2👍1🔥1
Foundry с 0. Часть 20

В преддверии остальных, более сложных видов тестов, стоит поговорить о некоторых вариантах работы с токенами и nft.

Создадим простой контракт с использованием библиотек от Open Zeppelin: erc20 и erc721.

contract MyERC20 is ERC20 {
address immutable owner;
constructor() ERC20("Token1", "SYM1") {
owner = msg.sender;
_mint(msg.sender, 10_000_000);
}

function mint() external {
require(msg.sender == owner, 'not an owner');
_mint(msg.sender, 10000);
}
}

contract NFT is ERC721, Ownable {
using Strings for uint256;
string public baseURI;
uint256 public currentTokenId;
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;

constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) {
baseURI = _baseURI;
}

function mintSimple(address recipient) public payable returns (uint256) {
uint256 newTokenId = ++currentTokenId;
_safeMint(recipient, newTokenId);
return newTokenId;
}

function mintTo(address recipient) public payable returns (uint256) {
if (msg.value != MINT_PRICE) {
revert MintPriceNotPaid();
}
uint256 newTokenId = ++currentTokenId;
if (newTokenId > TOTAL_SUPPLY) {
revert MaxSupply();
}
_safeMint(recipient, newTokenId);
return newTokenId;
}

}


И поместим их в файл под названием Helper.sol.

Зачем это будет нужно?

Представьте, что вы разработали свой протокол, который работает с токенами и nft и нужно протестировать некоторые процессы: переводы на баланс контракта, минт долей, выдача прав и т.д. Для этого может потребоваться наличие некоторого количества токенов на счету фиктивных пользователей.

Да, можно через prank() подключать аккаунт админа токена и минтить на нужный адрес необходимое количество токенов, а можно все делать и через простые читкоды.

Для начала, как и для остальных тестов, мы подключаем библиотеки Test, контракта, который будем тестировать, и наш файл Helper.

В функции setUp() разворачиваем необходимые контракты из под нужных нам аккаунтов, например:

function setUp() public {

vm.startPrank(COUNTER_ADMIN);
counter = new Counter();
vm.stopPrank();

vm.startPrank(TOKEN_ADMIN);
token = new MyERC20();
vm.stopPrank();

vm.startPrank(NFT_ADMIN);
nft = new NFT(nftName, nftSymbol, nftLink);
vm.stopPrank();

}


Итак, у нас есть развернутые контракты. А NFT, в одном из случаев, можно сминтить за Эфир. Но в только что созданных аккаунтах пользователей нет ни Эфира, ни токенов. Что же делать?

Для пополнения баланса Эфира любого аккаунта, можно использовать следующую команду:

vm.deal(USER, 19 ether);


где вместо USER вы можете указать любой адрес аккаунта.

А для пополнения баланса токенов следует использовать:

deal(address(DAI), USER, 1 ether);


Не забывайте проверять через console.log правильно ли установились балансы!

К слову сказать, на балансе контракта нашего теста, например CounterTest, уже по умолчанию будет лежать достаточно Эфира для проведения операций. Проверить это можно с помощью обычного лога:

console.log(address(this).balance);

Более того, есть специальный читкод, который объединяет deal() и prank()! Т.е. написав:

hoax(USER, 1000);


на счет пользователя зачислится 1000 Эфира и последующий вызов, будет эмулирован из-под аккаунта USER. Это бывает очень удобно, когда в тестах нам нужно, скажем, купить nft и вместо двух команд мы можем написать всего одну!

С помощью этих команд можно управлять балансами пользователей и проводить тесты более просто.

Также старайтесь держать архитектуру папки в правильном виде, сохраняя файлы помощники в папку Utils или mock файлы в одноименную папку.

#foundry #lesson20
👍2🔥1
Вопрос по 4 модулю курса?

Оставлю этот пост и опрос ниже до понедельника, чтобы чуть больше людей его увидели и приняли участие.

Как многие знают, недавно у нас закончился 3 модуль курса "Разработчик смарт контрактов на языке Solidity". Было много практики и информации для изучения. Кроме того, многие участники канала решили докупить и предыдущие модули, чтобы проходить курс в своем темпе.

И сейчас хочу спросить у вас об актуальности 4 модуля.

Тут будут подниматься темы: древа Меркла, подписи и их безопасность, работа с прокси контрактами, разбор storage, memory, calldata, опкоды и assembly, побитовые операции и базовый дебаггинг.

Достаточно много времени будет уходить на изучение каждой темы и детальное ее понимание для учеников.

И у меня возник вопрос к вам: стоит ли его проводить в этом году?

Я хочу сказать, что он точно будет, только нужно выбрать для этого время.

Много учеников еще до проходят предыдущие модули в своем темпе, разбирая каждый урок и делая практические задания. Новый модуль будет "вместить" в себя достаточно сложно.

Я бы предложил запустить его после новогодних праздников, когда все до пройдут текущие модули.

А что думаете вы?

Прошу поучаствуйте в опросе ниже.
Когда лучше запустить 4 модуль курса?
Final Results
38%
В этом году
62%
В начале следующего года
Foundry с 0. Часть 21

Эту неделю мы начнем с разговора о работе со временем в наших тестах.

Напишем простой контракт:

pragma solidity 0.8.18;

contract Auction {
uint256 public startAt = block.timestamp + 1 days;
uint256 public endAt = block.timestamp + 2 days;

function bid() external {
require(
block.timestamp >= startAt && block.timestamp < endAt, "cannot bid"
);
}

function end() external {
require(block.timestamp >= endAt, "cannot end");
}
}


В Foundry я встречал 4 самых популярных читкода для манипуляцией временем:

1. vm.warp - устанавливает block.timestamp на конкретное значение времени
2. vm.roll - устанавливает block.number
3. skip - увеличивает текущий timestamp
4. rewind - уменьшает текущий timestamp

Какие же тесты можно тут написать?

Например, что bid() вызывается в определенный период времени:

function testBid() public {
vm.warp(startAt + 1 days);
auction.bid();
}


или так:

function testTimestamp() public {
uint256 t = block.timestamp;

// set block.timestamp to t + 100
skip(100);
assertEq(block.timestamp, t + 100);

// set block.timestamp to t + 100 - 100;
rewind(100);
assertEq(block.timestamp, t);
}


Обратите внимание на последовательность вызовов! Если в одном тесте вы добавите время, то при вычитании другого значения позже, действие будет происходить с обновленным значением после первой манипуляции!

Ну, и простой тест для block.number:

function testBlockNumber() public {
uint256 b = block.number;
// set block number to 11
vm.roll(11);
assertEq(block.number, 11);
}


Такие читкоды бывают полезны, когда вы пишите тесты для голосований ДАО или проверки возможности вывода депозита пользователя только в определенный промежуток времени.

В прочем, ничего сложного тут нет

#foundry #lesson21
🔥6
Foundry с 0. Часть 22

Настало время коснуться темы форков в наших тестах. Почему только коснуться? Потому что, когда я искал какие-либо достойные примеры этого тестирования, то чаще всего встречал либо базовые объяснения, либо копипасту.

И сейчас мы просто поговорим о читкодах и базовых примерах, а уже когда я найду хороший реальный пример реального протокола, то разберем его детальнее.

Итак, поехали.

Для начала, что есть вообще форк-тест?

Форк-тест - это такой вид тестов, когда вы проверяете код своего контракта не на локальном блокчейне, тот же anvil, а на реальной сети, типа Ethereum или Optimism. Это бывает очень полезно, когда вы планируете работу своего протокола на разных сетях и хотите удостовериться, что код работает корректно на каждой выбранной сети.

Есть два способа проводить форк-тесты.

Вот простой тест для контракта с использованием WETH:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";
import "forge-std/console.sol";

interface IWETH {
function balanceOf(address) external view returns (uint256);
function deposit() external payable;
}

contract ForkTest is Test {
IWETH public weth;

function setUp() public {
weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
}

function testDeposit() public {
uint256 balBefore = weth.balanceOf(address(this));
console.log("balance before", balBefore);

weth.deposit{value: 100}();

uint256 balAfter = weth.balanceOf(address(this));
console.log("balance after", balAfter);
}
}


Здесь мы проверяем, что после депозита на реальный контракт в сети Ethereum у пользователя действительно повысится баланс.

После этого выполняем команду в терминале:

forge test --fork-url $RPC_LINK  --match-contract ForkTest


Видим, что все прошло и работает корректно. Мы также можем добавлять другие функции из нашего протокола для таких тестов.

Во втором варианте мы создаем форк не в терминале при помощи --fork-url, а используя читкоды.

И наш тест тогда будет выглядеть так:

function testDeposit() public {
uint256 forkId = vm.createSelectFork("https://mainnet.infura.io/v3/...", blockNumber);

uint256 balBefore = weth.balanceOf(address(this));
console.log("balance before", balBefore);

weth.deposit{value: 100}();

uint256 balAfter = weth.balanceOf(address(this));
console.log("balance after", balAfter);
}


С vm.createSelectFork мы создаем и выбираем сеть для форка. Да, это можно было бы делать двумя различными читкодами - createFork() и selectFork() - но с одним это проще.

Также стоит обратить внимание, что мы можем создать:

1. Просто форк сети с использованием последнего созданного блока:

vm.createSelectFork("https://mainnet.infura.io/v3/...");


2. Форк с использованием конкретного номера блока:

vm.createSelectFork("https://mainnet.infura.io/v3/...", blockNumber);


3. Форк с блоком, в котором есть определенная транзакция:

vm.createSelectFork("https://mainnet.infura.io/v3/...", txBytes32);


при этом перед самим тестом будет проведена эмуляция транзакций в данном блоке.

При этом вам также доступно манипулированием состоянием форк. Например, выполнив определенные действия в тесте на конкретном блоке или транзакции, вы можете "промотать" вперед-назад форк и выполнить другие действия. Сделать это поможет читкод - rollFork(), который принимает те же аргументы, что и его собрат выше.

Более того, необходимо отметить, что в рамках одного теста вы можете менять сети форка с одного на другой. При этом память теста будет обнуляться. Другими словами, если вы указали какие-либо данные в переменных для первой сети, то для второй - они будут обнулены.

Избежать этого поможет читкод - makePersistent(), который сохраняет данные памяти при переходе форков. Чуть подробнее об этом можно прочитать тут:

https://book.getfoundry.sh/cheatcodes/make-persistent

Вообще сказать, я не часто видел тесты с форками в файлах крупных протоколов. Может поэтому там и находят большое количество багов...

#foundry #lesson22
🔥21
Foundry с 0. Часть 23

В этом небольшом посте хотел бы написать про FFI тесты, о которых узнал из этого видео:

https://youtu.be/DTyn5ShI2vQ

FFI, или foreign function interface, позволяет выполнять сторонние команды в рамках вашего Foundry теста. Например, вы можете выполнить какую-либо python, linux или другую команду и получить ее значение внутри вашего теста.

Честно признаться, я вообще не встречал не встречал такие тесты в контрактах и не совсем понимаю, для чего они могут потребоваться. Скорее всего, это может быть какой-то уникальный нишевый случай... Или может при связи фронтенда с протоколом.

В видео показывается, как разработчик может эмулировать исполнение команды cat в Linux и прочитать содержимое txt файла в тесте.

Мы создаем обычный файл и пишем там "Hello Foundry". Затем создаем простой тест:

contract FFITest is Test {
function testFFI() public {
string[] memory cmds = new string[](2);
cmds[0] = "cat";
cmds[1] = "ffi_test.txt";
bytes memory res = vm.ffi(cmds);
console.log(string(res));
}
}



Команда - cmds - это обычный массив строк, где первым элементом будет идти название команды, а вторым - название файла. Также можно сделать строку пропуска с символом "/n", тогда в массиве будет уже три элемента:

string[] memory cmds = new string[](3);
cmds[0] = "cat";
cmds[1] = "-n";
cmds[2] = "ffi_test.txt";


Как вы можете заметить, результатом выполнения этого читкода является байтовое значение, которое нам нужно будет конвертировать в строковое по итогу.

В терминале также потребуется добавить новую опцию для корректной работы теста:

forge test --ffi -vv


Мы увидим, что в консоли вывелось содержание нашего txt, и значит все сработало как надо.

Если у вас есть примеры реальных тестов протоколов с использованием FFI, поделитесь в комментариях, очень интересно попробовать разобрать их.

#foundry #lesson23
👍51🔥1
Foundry с 0. Часть 24

При всей важности fuzz тестов все обучающие видео и практические примеры сводятся к двум функциям и паре настроек конфигурации Foundry.

Fuzz тесты, или фаззинг, или нечеткое тестирование, по своей сути, это простой программный подбор таких значений в функции, при которых ее исполнение будет "ломаться".

Возьмём простой пример для наглядности:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Fuzz {
function checkNum(uint8 num) external view returns(bool){
if (num == 100) revert ("EQ 100");
if (num == 0) revert ("EQ 0");
return true;
}
}

Тут у нас простая функция для проверки какого-то вводимого числа. Из возможных 256 вариантов num - два не пройдут проверку. Но писать тест для каждого числа было бы достаточно накладно и долго.

Для этих целей и придумали фаззинг. Посмотрите на простой тест для этой функции.

contract FuzzTest is Test {
Fuzz public fuzz;
function setUp() public {
fuzz = new Fuzz();
}

/// forge-config: default.fuzz.runs = 100
function testFuzz_checkNum(uint8 num) public view {

vm.assume(num != 0);
num = uint8(bound(num, 1, 99));
fuzz.checkNum(num);
}
}


Для начала мы создаем переменную и объект контракта в setUp(), затем пишем простой тест.

Фаззинг отличается от юнит тестов тем, что для функции теста мы оставляем аргументы, которые сам Foundry и будет подбирать.

Вспомните наши предыдущие тесты, где были такие функции как:

function test_IncrementEvent() public {}

в которых нет аргументов, и функцию для фаззинга:

function testFuzz_checkNum(uint8 num) public view {}

Тут мы как бы предлагаем Foundry поработать самому и найти такие числа, которые могут поломать функцию checkNum() в нашем контракте.

Если же упростить тестовую функцию до:

function testFuzz_checkNum(uint8 num) public view {
fuzz.checkNum(num);
}


то при вызове теста, через forge test, мы получим примерно такую запись в терминале:

Running 1 test for test/Counter.t.sol:FuzzTest
[FAIL. Reason: EQ 100 Counterexample: calldata=0x6bee15a90000000000000000000000000000000000000000000000000000000000000064, args=[100]] testFuzz_checkNum(uint8) (runs: 95, μ: 8571, ~: 8571)


Обратите внимание на args=[100] - то значение, которые было найдено через фаззинг, и которое ломало вызов наш вызов.

(runs: 95, μ: 8571, ~: 8571) - runs - означает количество "пробегов" Foundry по нашему тесты до момента, когда он нашел значение "на вылет", μ: 8571 - среднее количество потраченного газа, ~: 8571 - значение по газу, которое потратили на "средний" тест. Например, у нас было проведено 95 прогонов, они были проранжированы по мере потребления газа от 1 до 95, и вот под ~ - будет тест находящийся на 48 позиции, т.е. средний из всех 95.

Но вернемся к записи в терминале. Сейчас у нас тест провалился, поэтому добавим новый читкод vm.assume().

vm.assume(num != 0); - будет означать, что наш тест должен "предполагать", что подставленное значение не должно быть равным нулю. Если такое случается, то тест не проваливается, а продолжает цикл.

num = uint8(bound(num, 1, 99)); - эта запись означает, что мы как бы ставим в рамки num значение от 1 до 99. И все значения, которые будет подбирать Foundry будут преобразовываться в числа в промежутке от 1 до 99.

Также стоит сказать пару слов об inline-config. Это такие настройки, которые мы указываем перед тестом через комментарии.

/// forge-config: default.fuzz.runs = 100

В этом случаем мы устанавливаем "пробег" тестов на количестве 100, в то время, как значение по умолчанию равно 256.

Конечно, мы можем установить и 100_000, но проведение такого теста займет куда больше времени, особенно, если вызываемая функция будет намного сложнее, чем в данном примере.

Также есть еще одна настройка:

/// forge-config: default.fuzz.max-test-rejects = 500

которая устанавливает количество "fail" теста перед тем, как он может считаться окончательно проваленным.
👍3🔥1
На самом деле, я ни разу не видел этой настройки в реальных примерах, и не могу со 100% уверенностью обозначить ситуацию, когда она может потребоваться.

Вообще, фаззинг тесты стали популярными в последнее время и все больше команд начинают уделять время на их написание. Они бывают особенно полезны для DeFi протоколов и проверки вводов-выводом активов пользователей.

Уверен, мы не раз встретим такие примеры в рамках разбора реальных тестов от действующих протоколов.

#foundry #lesson24
👍4🔥1
Foundry с 0. Часть 25

На следующей неделе у меня в планах рассмотреть invariant тесты и работу с подписями стандарта EIP-712. Вообще, давайте пройдемся по ближайшим планам с темой Foundry.

На данный момент вышло уже 25 постов, где я постарался объяснить базовые нюансы написания тестов. Осталось несколько более сложных тем:

1. Тесты с использованием контрактов Uniswap и Chainlink;
2. Дебаггинг;
3. Работа с L2, ZKsynk и ZeroLayer;
4. Тестирование мостов;

Информации по этим темам не так много, поэтому времени уйдет на подготовку постов чуть больше. Возможно, дальше они будут выходить не каждый день.

Также хотелось бы разобрать несколько примеров из серии Damn Vulnerable DeFi, Immunefi reports и реальные тесты протоколов из конкурсных аудитов.

В целом, работы нам где-то еще на месяц. Зато после этого мы качественно поднимем свои навыки работы с Foundry и написанием тестов!

А сегодня хочу рассказать вам о некоторых командах Foundry, о которых редко упоминают в обучающих видео.

1. Команда forge fmt

Простая и удобная команда, которая позволяет быстро отформатировать ваш код. Нет, она не исправит ваши ошибки в коде, но сможет убрать не нужные отступы и пробелы, сделает код визуально адекватным.

2. Команда forge doc

Она создает документацию по функциям и контрактам вашего протокола и сохраняет все в отдельные md файлы.

Кстати, для тех, кто не знал, есть прекрасный плаггин Markdown All in One (от Yu Zhang), который может отобразить md файлы в вашем редакторе в удобном читаемом виде.

3. Команда forge config

Она выводит все доступные опции для настройки конфигурационного файла foundry.toml. Можно копировать прямо с терминала, помещать в файл и корректировать необходимые настройки.

4. Кодирование структур

Структуры в Solidity кодируются в виде кортежей данных (tuple), подробнее об этом можно прочитать тут:

https://docs.soliditylang.org/en/latest/abi-spec.html#mapping-solidity-to-abi-types

В тестах Foundry также нужно иметь ввиду, как это все работает "под капотом". Например, возьмем простой тест:

contract Test {
struct MyStruct {
address addr;
uint256 amount;
}
function f(MyStruct memory t) public pure {}
}


Вот как эта функция, принимающая структуру, будет выглядеть в раскладке:

{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "addr",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"internalType": "struct Test.MyStruct",
"name": "t",
"type": "tuple"
}
],
"name": "f",
"outputs": [],
"stateMutability": "pure",
"type": "function"
}


т.е. функция f принимает один аргумент из двух компонентов типа адрес и uint256.

4. Вывод отрицательных чисел в тестах

Через команду console.log мы можем выводить какую-либо информацию прямо из наших тестов в терминал. Однако с отрицательными числами так не выйдет.

В этом случае необходимо использовать - console.logInt().

5. Использование стандартных ошибок Foundry

В библиотеке StdErrors, которая по умолчанию наследуется в Test, основную библиотеку для проведения тестов, есть свой набор ошибок. Вы можете использовать их для своих тестов, например:

vm.expectRevert(stdError.arithmeticError);


Полный список ошибок можно посмотреть тут:

https://book.getfoundry.sh/reference/forge-std/std-errors

Надеюсь, сегодня вы также узнали чуточку больше о Foundry.

Приятного обучения и легких выходных!

#foundry #lesson25
🔥3
Foundry с 0. Часть 26

Изучая тему написания тестов, я активно ищу примеры, да, и, в целом, различные логические виды тестов. И сегодня хочу рассказать вам о новом виде, который я открыл для себя буквально на днях - Мутационные тесты!

Классно звучит, не так ли? О них я узнал из этой статьи.
https://www.rareskills.io/post/solidity-mutation-testing

Что мне понравилось больше всего, так это то, что об этой теме редко говорят, а вообще идея стоящая.

Мутационные тесты - это такие тесты, когда вы целенаправленно изменяете код своего контракта, и смотрите, как поведет себя тест.

Простой пример:

// original function
function mint() external payable {
require(msg.value >= PRICE, "insufficient msg value");
}

// mutated function
function mint() external public {
require(msg.value < PRICE, "insufficient msg value");
}


Вы написала контракт в котором была представлена функция выше, позже создали для нее тест. А затем специально изменили знак условия, чтобы проверить, пройдет ли тест или нет.

Да, это простой пример, чтобы донести идею такого тестирования, а теперь чуть подробнее.

Смотрите, всем тестировщикам требуют написание тестов, которые бы покрывали 100% кода, функция, веток и условий. При этом все понимаю, что 100% покрытие кода тестами, вовсе не означает отсутствие ошибок. Вспомнить, хотя бы о тех же логических...

Возьмем другой пример.

function mint(address to_, string memory questId_) public onlyMinter {
// business logic
}


Допустим мы написали такую функцию в своем контракте и создали тесты дл нее. А что произойдет, если мы уберем модификатор? Тест все равно пройдет!

Или еще пример:

uint256 public LIMIT = 5;

// original
function mint(uint256 amount) external {
require(amount < LIMIT, "exceeds limit");
}

// mutation
function mint(uint256 amount) external {
require(amount <= LIMIT, "exceeds limit");
}


Если мы напишем тесты, где значения для установки будут 3 или 8?

Во втором случае тест пройдет, хотя условие будет нарушено!

Как я понял из статьи, мутационные тесты помогают нам создавать такие тесты, которые будут удовлетворять логику нашего протокола не зависимо от изменений. Более того, это помогает проявлять непредвиденные уязвимости в нашем контракте!

Как можно нам проводить такие тесты:

1. Удалять модификатор из функций;
2. Изменять знаки условий: ==, >=, >= и т.д.;
3. Изменяя значения констант;
4. Заменят строковые значения на пустые строки;
5. Изменяя true/false в результатах;
6. Изменяя && на || или побитовое & на |;
7. Изменяя математические операторы;
8. Удаляя целые линии кода;
9. Меняя местами линии кода;

Для удобства проведения подобных тестов даже были созданы специальные инструменты:

1. vertigo-rs
https://github.com/RareSkills/vertigo-rs

2. Gambit
https://docs.certora.com/en/latest/docs/gambit/index.html

3. Universal Mutator
https://github.com/sambacha/universalmutator/tree/new-solidity-rules

Мутационные тесты сродни обычным unit тестам, а значит они никак не работают с состояниями контракта. Поэтому разработчикам не следует рассматривать какой-либо вариант тестирование, как единственный правильный и преследовать цель заполнить coverage на 100%. Только комбинация нескольких видов тестирования поможет сократить количество потенциальных проблем в вашем коде.

#foundry #lesson26
👍2🔥1
Новости про 4 модуль курса

Около недели назад я делал опрос на канале, где спрашивал у вас, когда лучше провести 4 модуль нашего курса.

В итоге большинство проголосовало за начало следующего года.

Приятно видеть, что в опросе приняло участие 82 человека, которым интересна эта тема.

Я прекрасно понимаю, что многие хотят либо допройти предыдущие модули, либо позаниматься самостоятельно и подтянуть знания до старта, так как темы будут весь серьезные.

Хотелось бы запустить модуль уже с середины января, но буду еще спрашивать о готовности вас, так как понимаю, что новогодние праздники у многих могут затянуться и на весь месяц.

Чуть позже я сделаю пост с программой модуля, чтобы вы знали, какие темы там будут даваться точно.

А пока, я буду вкладываться в материалы по Foundry, чтобы вы могли научиться писать тесты более профессионально!

Приятного обучения и легкой недели!

#курс #модуль4
👍9❤‍🔥2🔥1👌1
Foundry с 0. Часть 27

Следующие пару дней мы посвятим теме invariant тестов. Сейчас о них много, кто говорит, но крайне мало команд используют в своей работе. А все из-за того, что этот паттерн, возможно, для некоторых разработчиков несколько сложен для понимания и написания.

Для начала поговорим о том, что вообще такое инварианты.

Я сам, давно, потратил некоторое время, пытаясь усвоить это определение. И то, что практически в каждом описании, которое я встречал, было "ивариант - это некоторое условие, которое всегда должно оставаться неизменным..." не давало мне полного представления.

Ну вот есть у нас переменные, есть константы, есть immutables, а как установить инвариант? Как его прописать в коде?

Забавно было понять для себя, что инварианты появляются сами собой при написании проекта. Ну, в том смысле, что для каждого протокола они могут быть и одинаковыми и разными. И заранее определить количество инвариантов в своем будущем проекте крайне сложно. К слову сказать, их может и не быть вовсе!

Попробую объяснить, что такое инвариант на простом популярном примере.

Вот мы написали некий контракт, в котором есть наш собственный токен. Наши пользователи могут его минтить, пересылать друг другу, сжигать и любые другие действия. При этом мы понимаем, что общее количество сминченых токенов будет равно количеству токенов на балансах наших пользователей. Т.е. если сложить все балансы токенов пользователей - сумма будет равна общему количеству токенов когда-либо выпущенных. Это и есть наш первый инвариант.

Это такое условие в нашем коде, которое всегда должно оставаться одинаковым. Вне зависимости от действий пользователя: перевода, сжигания и т.д.

Другим инвариантом, например, может быть условие, что каждый ваш конкретный пользователь токена не может пересылать больше, чем у него есть на балансе.

Или, в случае nft, что, например, у пользователя может быть только один токен на счету и не больше.

Вообще эти условия нужно искать в своем проекте уже после его написания. Это не только может помочь понять сой код чуть лучше, но найти потенциальные уязвимости.

Теперь давайте поговорим об инвариант тестах в Foundry на простом примере. У нас есть небольшой контракт:

contract InvariantIntro {
bool public flag;

function func_1() external {}
function func_2() external {}
function func_3() external {}
function func_4() external {}

function func_5() external {
flag = true;
}
}


Представим, что переменная flag всегда должна оставаться false не смотря ни на что.

И небольшой тест к нему:

contract IntroInvariantTest is Test {
InvariantIntro private target;

function setUp() public {
target = new InvariantIntro();
}

function invariant_flag_is_always_false() public {
assertEq(target.flag(), false);
}
}


Мы как обычно создаем переменную объекта нашего контракта и в setUp() подключаем его. А вот дальше...

Помните перед каждым тестом нам требовалось прописывать название, начиная с test..., например testIncrement(), testBlaBlaBla() и т.д.?

С инвариант тестами теперь нужно писать ключевое слово invariant. Это покажет Foundry, что мы переходим на другой вид тестов.

Как вообще работают инвариант тесты?

Если в обычных тестах (unit test) мы хотели сделать проверку конкретной части кода, например, require, то перед каждым конкретным тестом выполнялась функция setUp() и разворачивался новый контракт и его состояние памяти обнулялось.

Каждый тест мы проводили "с чистого листа".

Также и в случае с фаззингом. Каждый отдельный тест - это обнуленное состояние контракта. Поэтому, в случае unit и fuzz тестов, можно встретить определение - stateless tests.

Инвариант тесты - это уже stateful tests, когда состояние контракта не обнуляется, а наоборот, запоминается для каждого последующего вызова функции.

Смотрите, что получается:

function invariant_flag_is_always_false() public {
assertEq(target.flag(), false);
}


В этот тесте через assertEq мы проверяем нашу переменную, которая всегда должна держать состояние false.
👍21
Сам Foundry в данном простом тесте будет вызывать функции из нашего контракта в случайном порядке. Как вы думаете, что произойдет?

На определенном этапе он вызовет функцию func_5() и в консоли появится запись [FAIL. Reason: Assertion failed.], что означает наш инвариант тест провалился. При этом будет показан некий порядок вызовов функций в нашем контракте, который привел к провалу.

В общем из этого поста необходимо понять принципы работы инвариант тестов, так как со следующего посты мы возьмем чуть более сложную тему работы с WETH контрактом.

Вы можете посмотреть это забавное видео от Патрика, чтобы чуть лучше понять суть этих тестов.

#foundry #lesson27
👍21
Foundry с 0. Часть 28

После того, как мы получили первое представление об инвариант тестах, пора переходить к более реальным примерам. И сейчас мы подготовим новый WETH контракт для этого.

Если не хотите сами искать его, то можете просто скопировать и сохранить к себе в папку обучения следующий контракт:

https://github.com/t4sk/hello-foundry/blob/main/src/WETH.sol

и, при написании тестов для него, не забудьте импортировать:

import {WETH} from "../../src/WETH.sol";

Теперь напишем простой контракт:

contract WETH_Open_Invariant_Tests is Test {
WETH public weth;

function setUp() public {
weth = new WETH();
}

receive() external payable {}

function invariant_totalSupply_is_always_zero() public {
assertEq(0, weth.totalSupply());
}
}


Мы уже знаем, что следует писать invariant в названии функции, и что assertEq выполняет проверку общего количества токенов, которое должно быть равно 0.

Foundry будет в случайном порядке вызывать функции из контракта WETH и сравнивать баланс.

Если мы запустим тест, то увидим, что он прошел успешно! Обратите внимание на саму запись в консоли, которая выглядит так:

[PASS] invariant_totalSupply_is_always_zero() (runs: 256, calls: 3840, reverts: 2187)

Т.е. тест был выполнен 256 раз, в которых было вызвано 3840 функций, и получено 2187 откатов. Что за откаты / реверты?

Если посмотреть на контракт WETH, то можно увидеть такие функции, как deposit, transfer, withdraw и другие, в которых есть определенные условия для исполнения. Например, для трансфера у нас на счету должно быть некоторое количество токенов, а их у нас нет. Поэтому произошел реверт. И так 2187 раз.

Сам инвариант тест завершился успешно, так как, не смотря на откаты, totalSupply() у нас оставался равен 0 все это время.

Тем не менее, даже при успешном выполнении теста, он не будет считаться корректным, потому, что получилось достаточно большое количество ревертов и вызывались функции, которые не играют роли.

Мы пойдем дальше и ограничим рамки инвариант теста с помощью дополнительного контракта. Но об этом уже в следующем посте.

#foundry #lesson28
1👍1