Foundry с 0. Часть 21
Эту неделю мы начнем с разговора о работе со временем в наших тестах.
Напишем простой контракт:
В Foundry я встречал 4 самых популярных читкода для манипуляцией временем:
1. vm.warp - устанавливает block.timestamp на конкретное значение времени
2. vm.roll - устанавливает block.number
3. skip - увеличивает текущий timestamp
4. rewind - уменьшает текущий timestamp
Какие же тесты можно тут написать?
Например, что bid() вызывается в определенный период времени:
или так:
Обратите внимание на последовательность вызовов! Если в одном тесте вы добавите время, то при вычитании другого значения позже, действие будет происходить с обновленным значением после первой манипуляции!
Ну, и простой тест для block.number:
Такие читкоды бывают полезны, когда вы пишите тесты для голосований ДАО или проверки возможности вывода депозита пользователя только в определенный промежуток времени.
В прочем, ничего сложного тут нет
#foundry #lesson21
Эту неделю мы начнем с разговора о работе со временем в наших тестах.
Напишем простой контракт:
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:
Здесь мы проверяем, что после депозита на реальный контракт в сети Ethereum у пользователя действительно повысится баланс.
После этого выполняем команду в терминале:
Видим, что все прошло и работает корректно. Мы также можем добавлять другие функции из нашего протокола для таких тестов.
Во втором варианте мы создаем форк не в терминале при помощи --fork-url, а используя читкоды.
И наш тест тогда будет выглядеть так:
С vm.createSelectFork мы создаем и выбираем сеть для форка. Да, это можно было бы делать двумя различными читкодами - createFork() и selectFork() - но с одним это проще.
Также стоит обратить внимание, что мы можем создать:
1. Просто форк сети с использованием последнего созданного блока:
2. Форк с использованием конкретного номера блока:
3. Форк с блоком, в котором есть определенная транзакция:
при этом перед самим тестом будет проведена эмуляция транзакций в данном блоке.
При этом вам также доступно манипулированием состоянием форк. Например, выполнив определенные действия в тесте на конкретном блоке или транзакции, вы можете "промотать" вперед-назад форк и выполнить другие действия. Сделать это поможет читкод - rollFork(), который принимает те же аргументы, что и его собрат выше.
Более того, необходимо отметить, что в рамках одного теста вы можете менять сети форка с одного на другой. При этом память теста будет обнуляться. Другими словами, если вы указали какие-либо данные в переменных для первой сети, то для второй - они будут обнулены.
Избежать этого поможет читкод - makePersistent(), который сохраняет данные памяти при переходе форков. Чуть подробнее об этом можно прочитать тут:
https://book.getfoundry.sh/cheatcodes/make-persistent
Вообще сказать, я не часто видел тесты с форками в файлах крупных протоколов. Может поэтому там и находят большое количество багов...
#foundry #lesson22
Настало время коснуться темы форков в наших тестах. Почему только коснуться? Потому что, когда я искал какие-либо достойные примеры этого тестирования, то чаще всего встречал либо базовые объяснения, либо копипасту.
И сейчас мы просто поговорим о читкодах и базовых примерах, а уже когда я найду хороший реальный пример реального протокола, то разберем его детальнее.
Итак, поехали.
Для начала, что есть вообще форк-тест?
Форк-тест - это такой вид тестов, когда вы проверяете код своего контракта не на локальном блокчейне, тот же 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
🔥2❤1
Foundry с 0. Часть 23
В этом небольшом посте хотел бы написать про FFI тесты, о которых узнал из этого видео:
https://youtu.be/DTyn5ShI2vQ
FFI, или foreign function interface, позволяет выполнять сторонние команды в рамках вашего Foundry теста. Например, вы можете выполнить какую-либо python, linux или другую команду и получить ее значение внутри вашего теста.
Честно признаться, я вообще не встречал не встречал такие тесты в контрактах и не совсем понимаю, для чего они могут потребоваться. Скорее всего, это может быть какой-то уникальный нишевый случай... Или может при связи фронтенда с протоколом.
В видео показывается, как разработчик может эмулировать исполнение команды cat в Linux и прочитать содержимое txt файла в тесте.
Мы создаем обычный файл и пишем там "Hello Foundry". Затем создаем простой тест:
Команда - cmds - это обычный массив строк, где первым элементом будет идти название команды, а вторым - название файла. Также можно сделать строку пропуска с символом "/n", тогда в массиве будет уже три элемента:
string[] memory cmds = new string[](3);
cmds[0] = "cat";
cmds[1] = "-n";
cmds[2] = "ffi_test.txt";
Как вы можете заметить, результатом выполнения этого читкода является байтовое значение, которое нам нужно будет конвертировать в строковое по итогу.
В терминале также потребуется добавить новую опцию для корректной работы теста:
Мы увидим, что в консоли вывелось содержание нашего txt, и значит все сработало как надо.
Если у вас есть примеры реальных тестов протоколов с использованием FFI, поделитесь в комментариях, очень интересно попробовать разобрать их.
#foundry #lesson23
В этом небольшом посте хотел бы написать про 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
👍5❤1🔥1
Foundry с 0. Часть 24
При всей важности fuzz тестов все обучающие видео и практические примеры сводятся к двум функциям и паре настроек конфигурации Foundry.
Fuzz тесты, или фаззинг, или нечеткое тестирование, по своей сути, это простой программный подбор таких значений в функции, при которых ее исполнение будет "ломаться".
Возьмём простой пример для наглядности:
Тут у нас простая функция для проверки какого-то вводимого числа. Из возможных 256 вариантов num - два не пройдут проверку. Но писать тест для каждого числа было бы достаточно накладно и долго.
Для этих целей и придумали фаззинг. Посмотрите на простой тест для этой функции.
Для начала мы создаем переменную и объект контракта в setUp(), затем пишем простой тест.
Фаззинг отличается от юнит тестов тем, что для функции теста мы оставляем аргументы, которые сам Foundry и будет подбирать.
Вспомните наши предыдущие тесты, где были такие функции как:
function test_IncrementEvent() public {}
в которых нет аргументов, и функцию для фаззинга:
function testFuzz_checkNum(uint8 num) public view {}
Тут мы как бы предлагаем Foundry поработать самому и найти такие числа, которые могут поломать функцию checkNum() в нашем контракте.
Если же упростить тестовую функцию до:
то при вызове теста, через 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" теста перед тем, как он может считаться окончательно проваленным.
При всей важности 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
Вообще, фаззинг тесты стали популярными в последнее время и все больше команд начинают уделять время на их написание. Они бывают особенно полезны для 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 также нужно иметь ввиду, как это все работает "под капотом". Например, возьмем простой тест:
Вот как эта функция, принимающая структуру, будет выглядеть в раскладке:
т.е. функция f принимает один аргумент из двух компонентов типа адрес и uint256.
4. Вывод отрицательных чисел в тестах
Через команду console.log мы можем выводить какую-либо информацию прямо из наших тестов в терминал. Однако с отрицательными числами так не выйдет.
В этом случае необходимо использовать - console.logInt().
5. Использование стандартных ошибок Foundry
В библиотеке StdErrors, которая по умолчанию наследуется в Test, основную библиотеку для проведения тестов, есть свой набор ошибок. Вы можете использовать их для своих тестов, например:
Полный список ошибок можно посмотреть тут:
https://book.getfoundry.sh/reference/forge-std/std-errors
Надеюсь, сегодня вы также узнали чуточку больше о Foundry.
Приятного обучения и легких выходных!
#foundry #lesson25
На следующей неделе у меня в планах рассмотреть 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
Что мне понравилось больше всего, так это то, что об этой теме редко говорят, а вообще идея стоящая.
Мутационные тесты - это такие тесты, когда вы целенаправленно изменяете код своего контракта, и смотрите, как поведет себя тест.
Простой пример:
Вы написала контракт в котором была представлена функция выше, позже создали для нее тест. А затем специально изменили знак условия, чтобы проверить, пройдет ли тест или нет.
Да, это простой пример, чтобы донести идею такого тестирования, а теперь чуть подробнее.
Смотрите, всем тестировщикам требуют написание тестов, которые бы покрывали 100% кода, функция, веток и условий. При этом все понимаю, что 100% покрытие кода тестами, вовсе не означает отсутствие ошибок. Вспомнить, хотя бы о тех же логических...
Возьмем другой пример.
Допустим мы написали такую функцию в своем контракте и создали тесты дл нее. А что произойдет, если мы уберем модификатор? Тест все равно пройдет!
Или еще пример:
Если мы напишем тесты, где значения для установки будут 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
Изучая тему написания тестов, я активно ищу примеры, да, и, в целом, различные логические виды тестов. И сегодня хочу рассказать вам о новом виде, который я открыл для себя буквально на днях - Мутационные тесты!
Классно звучит, не так ли? О них я узнал из этой статьи.
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
Около недели назад я делал опрос на канале, где спрашивал у вас, когда лучше провести 4 модуль нашего курса.
В итоге большинство проголосовало за начало следующего года.
Приятно видеть, что в опросе приняло участие 82 человека, которым интересна эта тема.
Я прекрасно понимаю, что многие хотят либо допройти предыдущие модули, либо позаниматься самостоятельно и подтянуть знания до старта, так как темы будут весь серьезные.
Хотелось бы запустить модуль уже с середины января, но буду еще спрашивать о готовности вас, так как понимаю, что новогодние праздники у многих могут затянуться и на весь месяц.
Чуть позже я сделаю пост с программой модуля, чтобы вы знали, какие темы там будут даваться точно.
А пока, я буду вкладываться в материалы по Foundry, чтобы вы могли научиться писать тесты более профессионально!
Приятного обучения и легкой недели!
#курс #модуль4
👍9❤🔥2🔥1👌1
Foundry с 0. Часть 27
Следующие пару дней мы посвятим теме invariant тестов. Сейчас о них много, кто говорит, но крайне мало команд используют в своей работе. А все из-за того, что этот паттерн, возможно, для некоторых разработчиков несколько сложен для понимания и написания.
Для начала поговорим о том, что вообще такое инварианты.
Я сам, давно, потратил некоторое время, пытаясь усвоить это определение. И то, что практически в каждом описании, которое я встречал, было "ивариант - это некоторое условие, которое всегда должно оставаться неизменным..." не давало мне полного представления.
Ну вот есть у нас переменные, есть константы, есть immutables, а как установить инвариант? Как его прописать в коде?
Забавно было понять для себя, что инварианты появляются сами собой при написании проекта. Ну, в том смысле, что для каждого протокола они могут быть и одинаковыми и разными. И заранее определить количество инвариантов в своем будущем проекте крайне сложно. К слову сказать, их может и не быть вовсе!
Попробую объяснить, что такое инвариант на простом популярном примере.
Вот мы написали некий контракт, в котором есть наш собственный токен. Наши пользователи могут его минтить, пересылать друг другу, сжигать и любые другие действия. При этом мы понимаем, что общее количество сминченых токенов будет равно количеству токенов на балансах наших пользователей. Т.е. если сложить все балансы токенов пользователей - сумма будет равна общему количеству токенов когда-либо выпущенных. Это и есть наш первый инвариант.
Это такое условие в нашем коде, которое всегда должно оставаться одинаковым. Вне зависимости от действий пользователя: перевода, сжигания и т.д.
Другим инвариантом, например, может быть условие, что каждый ваш конкретный пользователь токена не может пересылать больше, чем у него есть на балансе.
Или, в случае nft, что, например, у пользователя может быть только один токен на счету и не больше.
Вообще эти условия нужно искать в своем проекте уже после его написания. Это не только может помочь понять сой код чуть лучше, но найти потенциальные уязвимости.
Теперь давайте поговорим об инвариант тестах в Foundry на простом примере. У нас есть небольшой контракт:
Представим, что переменная flag всегда должна оставаться false не смотря ни на что.
И небольшой тест к нему:
Мы как обычно создаем переменную объекта нашего контракта и в setUp() подключаем его. А вот дальше...
Помните перед каждым тестом нам требовалось прописывать название, начиная с test..., например testIncrement(), testBlaBlaBla() и т.д.?
С инвариант тестами теперь нужно писать ключевое слово invariant. Это покажет Foundry, что мы переходим на другой вид тестов.
Как вообще работают инвариант тесты?
Если в обычных тестах (unit test) мы хотели сделать проверку конкретной части кода, например, require, то перед каждым конкретным тестом выполнялась функция setUp() и разворачивался новый контракт и его состояние памяти обнулялось.
Каждый тест мы проводили "с чистого листа".
Также и в случае с фаззингом. Каждый отдельный тест - это обнуленное состояние контракта. Поэтому, в случае unit и fuzz тестов, можно встретить определение - stateless tests.
Инвариант тесты - это уже stateful tests, когда состояние контракта не обнуляется, а наоборот, запоминается для каждого последующего вызова функции.
Смотрите, что получается:
В этот тесте через assertEq мы проверяем нашу переменную, которая всегда должна держать состояние false.
Следующие пару дней мы посвятим теме 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.
👍2❤1
Сам Foundry в данном простом тесте будет вызывать функции из нашего контракта в случайном порядке. Как вы думаете, что произойдет?
На определенном этапе он вызовет функцию func_5() и в консоли появится запись [FAIL. Reason: Assertion failed.], что означает наш инвариант тест провалился. При этом будет показан некий порядок вызовов функций в нашем контракте, который привел к провалу.
В общем из этого поста необходимо понять принципы работы инвариант тестов, так как со следующего посты мы возьмем чуть более сложную тему работы с WETH контрактом.
Вы можете посмотреть это забавное видео от Патрика, чтобы чуть лучше понять суть этих тестов.
#foundry #lesson27
На определенном этапе он вызовет функцию func_5() и в консоли появится запись [FAIL. Reason: Assertion failed.], что означает наш инвариант тест провалился. При этом будет показан некий порядок вызовов функций в нашем контракте, который привел к провалу.
В общем из этого поста необходимо понять принципы работы инвариант тестов, так как со следующего посты мы возьмем чуть более сложную тему работы с WETH контрактом.
Вы можете посмотреть это забавное видео от Патрика, чтобы чуть лучше понять суть этих тестов.
#foundry #lesson27
👍2❤1
Foundry с 0. Часть 28
После того, как мы получили первое представление об инвариант тестах, пора переходить к более реальным примерам. И сейчас мы подготовим новый WETH контракт для этого.
Если не хотите сами искать его, то можете просто скопировать и сохранить к себе в папку обучения следующий контракт:
https://github.com/t4sk/hello-foundry/blob/main/src/WETH.sol
и, при написании тестов для него, не забудьте импортировать:
import {WETH} from "../../src/WETH.sol";
Теперь напишем простой контракт:
Мы уже знаем, что следует писать 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
После того, как мы получили первое представление об инвариант тестах, пора переходить к более реальным примерам. И сейчас мы подготовим новый 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
Foundry с 0. Часть 29
Продолжаем совершенствоваться с инвариант тестами и сегодня поговорим о таком подвиде тестов, как Handler Based Tests.
Помните, в предыдущем посте мы делали тесты контракта WETH, в котором в случайном порядке вызывались вообще все функции, что приводило к большому количеству откатов. Мы сделали вывод, что такой тест нам не подходит и нужно провести его более прицельно. Как раз в этом нам и поможет дополнительный контракт помощник - Handler.
Я приведу его полный пример вместе с обновленным тестом и мы разберем его "по кирпичикам". Итак:
И контракт нашего теста:
Разберем наш Handler.
Handler - это некая смесь обычного контракта с тестовым, благодаря новым подключаемым библиотекам: CommonBase, StdCheats и StdUtils. Именно они позволяют использовать читкоды в Handler.
Итак, тут мы создаем переменные для нашего контракта WETH, который определим в конструкторе, а также wethBalance и numCalls, которые помогут написать хорошие тесты.
В данном примере мы будем тестировать три функции контракта WETH: deposit, withdraw и fallback, для этих целей мы создадим в Handler свои функции, которые будут вызывать функции из WETH.
Как вы можете заметить мы применим небольшие элементы фаззинга и позволим Foundry также отправлять различные суммы. При этом мы хотим, чтобы эти суммы были в некоторых рамках баланса нашего Handler. Ведь, если будут вообще все возможные суммы, то количество ревертов сильно возрастет. Ограничить можно с помощью уже пройдённого нами читкода:
amount = bound(amount, 0, weth.balanceOf(address(this)));
Также не забываем записывать в переменные отправляемые значение и увеличивать количество вызовов на 1.
Теперь контракт теста.
Для начала мы создаем переменные для объектов наших контрактов и прописываем функцию setUp(), которая играет определяющую роль.
weth = new WETH();
handler = new Handler(weth);
Инициализируем наши контракты.
deal(address(handler), 100 * 1e18);
Пополняем баланс Handler на 100 Эфиров, чтобы можно было что-то отправлять на WETH.
Продолжаем совершенствоваться с инвариант тестами и сегодня поговорим о таком подвиде тестов, как Handler Based Tests.
Помните, в предыдущем посте мы делали тесты контракта WETH, в котором в случайном порядке вызывались вообще все функции, что приводило к большому количеству откатов. Мы сделали вывод, что такой тест нам не подходит и нужно провести его более прицельно. Как раз в этом нам и поможет дополнительный контракт помощник - Handler.
Я приведу его полный пример вместе с обновленным тестом и мы разберем его "по кирпичикам". Итак:
import {CommonBase} from "forge-std/Base.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {StdUtils} from "forge-std/StdUtils.sol";
contract Handler is CommonBase, StdCheats, StdUtils {
WETH private weth;
uint256 public wethBalance;
uint256 public numCalls;
constructor(WETH _weth) {
weth = _weth;
}
receive() external payable {}
function sendToFallback(uint256 amount) public {
amount = bound(amount, 0, address(this).balance);
wethBalance += amount;
numCalls += 1;
(bool ok,) = address(weth).call{value: amount}("");
require(ok, "sendToFallback failed");
}
function deposit(uint256 amount) public {
amount = bound(amount, 0, address(this).balance);
wethBalance += amount;
numCalls += 1;
weth.deposit{value: amount}();
}
function withdraw(uint256 amount) public {
amount = bound(amount, 0, weth.balanceOf(address(this)));
wethBalance -= amount;
numCalls += 1;
weth.withdraw(amount);
}
function fail() external {
revert("fail");
}
}И контракт нашего теста:
contract WETH_Handler_Based_Invariant_Tests is Test {
WETH public weth;
Handler public handler;
function setUp() public {
weth = new WETH();
handler = new Handler(weth);
deal(address(handler), 100 * 1e18);
targetContract(address(handler));
bytes4[] memory selectors = new bytes4[](3);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.withdraw.selector;
selectors[2] = Handler.sendToFallback.selector;
targetSelector(
FuzzSelector({addr: address(handler), selectors: selectors})
);
}
function invariant_eth_balance() public {
assertGe(address(weth).balance, handler.wethBalance());
console.log("handler num calls", handler.numCalls());
}
}
Разберем наш Handler.
Handler - это некая смесь обычного контракта с тестовым, благодаря новым подключаемым библиотекам: CommonBase, StdCheats и StdUtils. Именно они позволяют использовать читкоды в Handler.
Итак, тут мы создаем переменные для нашего контракта WETH, который определим в конструкторе, а также wethBalance и numCalls, которые помогут написать хорошие тесты.
В данном примере мы будем тестировать три функции контракта WETH: deposit, withdraw и fallback, для этих целей мы создадим в Handler свои функции, которые будут вызывать функции из WETH.
function sendToFallback(uint256 amount) public {}
function deposit(uint256 amount) public {}
function withdraw(uint256 amount) public {}Как вы можете заметить мы применим небольшие элементы фаззинга и позволим Foundry также отправлять различные суммы. При этом мы хотим, чтобы эти суммы были в некоторых рамках баланса нашего Handler. Ведь, если будут вообще все возможные суммы, то количество ревертов сильно возрастет. Ограничить можно с помощью уже пройдённого нами читкода:
amount = bound(amount, 0, weth.balanceOf(address(this)));
Также не забываем записывать в переменные отправляемые значение и увеличивать количество вызовов на 1.
Теперь контракт теста.
Для начала мы создаем переменные для объектов наших контрактов и прописываем функцию setUp(), которая играет определяющую роль.
weth = new WETH();
handler = new Handler(weth);
Инициализируем наши контракты.
deal(address(handler), 100 * 1e18);
Пополняем баланс Handler на 100 Эфиров, чтобы можно было что-то отправлять на WETH.
👍1
targetContract(address(handler));
Очень важная строка! Здесь мы говорим, чтобы Foundry работал в тестах только с контрактом Handler, исключая вызовы на WETH.
Дальше мы можем написать небольшой тест:
Он прошел успешно, но все таки некоторое количество откатов может остаться. Происходит это из-за функции fail(), которую мы написали специально для проверки адекватности инвариант теста.
Попробуем исключить fail() из результатов. Да, конечно, можно просто удалить эту функцию из контракта, но есть и другой способ.
В функции setUp() мы можем дополнительно указать те функции, которые хотим использовать.
Мы создаем байтовый массив и помещаем туда селекторы необходимых нам для тестов функций.
А тут мы говорим Foundry, что в своих тестах мы хотим использовать только этот набор.
И есть сейчас мы выполним команду forge test, то количество откатов будет нулевым!
Наш тест стал уже намного лучше, но еще не совсем дотягивающим до нормы. Если подумать, то контрактом WETH будет пользоваться не один пользователь, а непредвиденное количество.
Поэтому в следующем посте мы разберем концепцию ролей в инвариант тестах!
#foundry #lesson29
Очень важная строка! Здесь мы говорим, чтобы Foundry работал в тестах только с контрактом Handler, исключая вызовы на WETH.
Дальше мы можем написать небольшой тест:
function invariant_eth_balance() public {
assertGe(address(weth).balance, handler.wethBalance());
console.log("handler num calls", handler.numCalls());
}Он прошел успешно, но все таки некоторое количество откатов может остаться. Происходит это из-за функции fail(), которую мы написали специально для проверки адекватности инвариант теста.
Попробуем исключить fail() из результатов. Да, конечно, можно просто удалить эту функцию из контракта, но есть и другой способ.
В функции setUp() мы можем дополнительно указать те функции, которые хотим использовать.
bytes4[] memory selectors = new bytes4[](3);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.withdraw.selector;
selectors[2] = Handler.sendToFallback.selector;
Мы создаем байтовый массив и помещаем туда селекторы необходимых нам для тестов функций.
targetSelector(
FuzzSelector({addr: address(handler), selectors: selectors})
);
А тут мы говорим Foundry, что в своих тестах мы хотим использовать только этот набор.
И есть сейчас мы выполним команду forge test, то количество откатов будет нулевым!
Наш тест стал уже намного лучше, но еще не совсем дотягивающим до нормы. Если подумать, то контрактом WETH будет пользоваться не один пользователь, а непредвиденное количество.
Поэтому в следующем посте мы разберем концепцию ролей в инвариант тестах!
#foundry #lesson29
👍5
Foundry с 0. Часть 30
В завершении базовых постов по инвариант тестам стоит поднять тему Actor management.
В прошлом уроке мы написали более продвинутый тест от имени одного контракта, а теперь посмотрим, как организовать процесс теста, когда вызовы в наш WETH контракт идут от нескольких Handler, что имитирует вызовы функций от разных пользователей.
Как вы можете догадаться, что нам потребуется дополнительный контракт:
Здесь мы создаем массив из объектов нашего контракта Handler и инициализируем их в конструкторе.
Далее нам также нужно повторить те же функции в Handler, которые мы планируем вызывать.
Обратите внимание на аргументы в этих функциях! Мы позволяем Foundry самому случайным образом выбрать один из наших Handler и послать вызов от его имени. Не забудьте использовать читкод bound(), чтобы Foundry мог использовать индекс контракта, который действительно есть в массиве.
Далее обновим наш контракт с тестом:
В функции setUp() мы создаем объекты наших контрактов и пополняем балансы каждого созданного Handler.
В конце, выбираем контракт manager как целевой для тестов. Напомню, что если этого не сделать, то инвариант тест будет проходить по каждому созданному контракту и вызывать случайным образом функции в них.
В приведенном выше тесте мы хотим удостовериться, что баланс токенов WETH будет равен или больше балансам всех наших Handler контрактов.
Именно поэтому мы создаем новую переменную total, в которую записываем все значения от переменных wethBalance в каждом Handler.
Запускаем наш тест через консольную команду forge test и видим, что все прошло успешно без каких-либо откатов!
Чуть позже мы посмотрим еще на реальные инвариант тесты от действующих протоколов, а сейчас предлагаю вам попрактиковаться самим и пересмотреть прекрасные видео от Smart Contract Programmer.
#foundry #lesson30
В завершении базовых постов по инвариант тестам стоит поднять тему Actor management.
В прошлом уроке мы написали более продвинутый тест от имени одного контракта, а теперь посмотрим, как организовать процесс теста, когда вызовы в наш WETH контракт идут от нескольких Handler, что имитирует вызовы функций от разных пользователей.
Как вы можете догадаться, что нам потребуется дополнительный контракт:
contract ActorManager is CommonBase, StdCheats, StdUtils {
Handler[] public handlers;
constructor(Handler[] memory _handlers) {
handlers = _handlers;
}
function sendToFallback(uint256 handlerIndex, uint256 amount) public {
uint256 index = bound(handlerIndex, 0, handlers.length - 1);
handlers[index].sendToFallback(amount);
}
function deposit(uint256 handlerIndex, uint256 amount) public {
uint256 index = bound(handlerIndex, 0, handlers.length - 1);
handlers[index].deposit(amount);
}
function withdraw(uint256 handlerIndex, uint256 amount) public {
uint256 index = bound(handlerIndex, 0, handlers.length - 1);
handlers[index].withdraw(amount);
}
}Здесь мы создаем массив из объектов нашего контракта Handler и инициализируем их в конструкторе.
Далее нам также нужно повторить те же функции в Handler, которые мы планируем вызывать.
Обратите внимание на аргументы в этих функциях! Мы позволяем Foundry самому случайным образом выбрать один из наших Handler и послать вызов от его имени. Не забудьте использовать читкод bound(), чтобы Foundry мог использовать индекс контракта, который действительно есть в массиве.
Далее обновим наш контракт с тестом:
contract WETH_Multi_Handler_Invariant_Tests is Test {
WETH public weth;
ActorManager public manager;
Handler[] public handlers;
function setUp() public {
weth = new WETH();
for (uint256 i = 0; i < 3; i++) {
handlers.push(new Handler(weth));
// Send 100 ETH to handler
deal(address(handlers[i]), 100 * 1e18);
}
manager = new ActorManager(handlers);
targetContract(address(manager));
}
function invariant_eth_balance() public {
uint256 total = 0;
for (uint256 i = 0; i < handlers.length; i++) {
total += handlers[i].wethBalance();
console.log("Handler num calls", i, handlers[i].numCalls());
}
console.log("ETH total", total / 1e18);
assertGe(address(weth).balance, total);
}
}В функции setUp() мы создаем объекты наших контрактов и пополняем балансы каждого созданного Handler.
В конце, выбираем контракт manager как целевой для тестов. Напомню, что если этого не сделать, то инвариант тест будет проходить по каждому созданному контракту и вызывать случайным образом функции в них.
В приведенном выше тесте мы хотим удостовериться, что баланс токенов WETH будет равен или больше балансам всех наших Handler контрактов.
Именно поэтому мы создаем новую переменную total, в которую записываем все значения от переменных wethBalance в каждом Handler.
Запускаем наш тест через консольную команду forge test и видим, что все прошло успешно без каких-либо откатов!
Чуть позже мы посмотрим еще на реальные инвариант тесты от действующих протоколов, а сейчас предлагаю вам попрактиковаться самим и пересмотреть прекрасные видео от Smart Contract Programmer.
#foundry #lesson30
👍2
Foundry с 0. Часть 31
С темой инвариант тестов мы закончили базовую часть по тестам в Foundry и теперь пришло время к разборам, паттернам и практике.
Сегодня хочу рассказать об интересном способе работы с нескольким блокчейн сетями и написание тестов для них, который я подсмотрел на курсе Патрика Колинса. Он объясняет это во второй части своих видео.
Вы можете посмотреть первые полчаса, чтобы наглядно понять, как реализовывать этот паттерн. А вкратце опишу этот процесс тут.
Итак, у нас есть какой-то контракт и он использует в своем решении функции из уже загруженного контракта стороннего протокола на разных сетях, например ChainlinkPriceFeed.
Как мы помним из предыдущих постов, что тесты на разных сетях проводятся с помощью команды:
forge test --fork-url $RPC_LINK
При этом адреса контракта ChainlinkPriceFeed на разных сетях могут быть разными, и переустанавливать новый при каждом тесте может быть весьма времязатратно.
В видео предлагается простое решение, как с помощью скриптовых контрактов можно связать различные сети, наш протокол и тесты.
А вот тут вы можете сами разобраться в полном коде:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/HelperConfig.s.sol
1. У нас есть некий контракт, при развертывании которого необходимо указывать адрес ChainlinkPriceFeed контракта той сети, где планируется деплой.
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/src/FundMe.sol
2. Также у нас есть контракт для тестов. И тут стоит обратить внимание на функцию setUp():
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/test/unit/FundMeTest.t.sol
Здесь мы разворачиваем вспомогательный контракт DeployFundMe и уже через него получаем адреса helperConfig и нашего основного FundMe, для которого мы и будем писать дальше тесты.
3. Посмотрим контракт DeployFundMe:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/DeployFundMe.s.sol
Здесь мы делаем деплой контракта помощника - HelperConfig, и уже через него получаем актуальный для нужной сети адрес контракта ChainlinkPriceFeed. После чего делаем деплой в нужную сеть с нужным адресом!
4. А теперь сам HelperConfig:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/HelperConfig.s.sol
С темой инвариант тестов мы закончили базовую часть по тестам в Foundry и теперь пришло время к разборам, паттернам и практике.
Сегодня хочу рассказать об интересном способе работы с нескольким блокчейн сетями и написание тестов для них, который я подсмотрел на курсе Патрика Колинса. Он объясняет это во второй части своих видео.
Вы можете посмотреть первые полчаса, чтобы наглядно понять, как реализовывать этот паттерн. А вкратце опишу этот процесс тут.
Итак, у нас есть какой-то контракт и он использует в своем решении функции из уже загруженного контракта стороннего протокола на разных сетях, например ChainlinkPriceFeed.
Как мы помним из предыдущих постов, что тесты на разных сетях проводятся с помощью команды:
forge test --fork-url $RPC_LINK
При этом адреса контракта ChainlinkPriceFeed на разных сетях могут быть разными, и переустанавливать новый при каждом тесте может быть весьма времязатратно.
В видео предлагается простое решение, как с помощью скриптовых контрактов можно связать различные сети, наш протокол и тесты.
А вот тут вы можете сами разобраться в полном коде:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/HelperConfig.s.sol
1. У нас есть некий контракт, при развертывании которого необходимо указывать адрес ChainlinkPriceFeed контракта той сети, где планируется деплой.
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/src/FundMe.sol
2. Также у нас есть контракт для тестов. И тут стоит обратить внимание на функцию setUp():
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/test/unit/FundMeTest.t.sol
FundMe public fundMe;
HelperConfig public helperConfig;
function setUp() external {
DeployFundMe deployer = new DeployFundMe();
(fundMe, helperConfig) = deployer.run();
vm.deal(USER, STARTING_USER_BALANCE);
}
Здесь мы разворачиваем вспомогательный контракт DeployFundMe и уже через него получаем адреса helperConfig и нашего основного FundMe, для которого мы и будем писать дальше тесты.
3. Посмотрим контракт DeployFundMe:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/DeployFundMe.s.sol
contract DeployFundMe is Script {
function run() external returns (FundMe, HelperConfig) {
HelperConfig helperConfig = new HelperConfig();
address priceFeed = helperConfig.activeNetworkConfig();
vm.startBroadcast();
FundMe fundMe = new FundMe(priceFeed);
vm.stopBroadcast();
return (fundMe, helperConfig);
}
}Здесь мы делаем деплой контракта помощника - HelperConfig, и уже через него получаем актуальный для нужной сети адрес контракта ChainlinkPriceFeed. После чего делаем деплой в нужную сеть с нужным адресом!
4. А теперь сам HelperConfig:
https://github.com/Cyfrin/foundry-fund-me-f23/blob/main/noscript/HelperConfig.s.sol
contract HelperConfig is Script {
NetworkConfig public activeNetworkConfig;
uint8 public constant DECIMALS = 8;
int256 public constant INITIAL_PRICE = 2000e8;
struct NetworkConfig {
address priceFeed;
}
event HelperConfig__CreatedMockPriceFeed(address priceFeed);
constructor() {
if (block.chainid == 11155111) {
activeNetworkConfig = getSepoliaEthConfig();
} else {
activeNetworkConfig = getOrCreateAnvilEthConfig();
}
}
function getSepoliaEthConfig() public pure returns (NetworkConfig memory sepoliaNetworkConfig) {
sepoliaNetworkConfig = NetworkConfig({
priceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306
});
}
function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory anvilNetworkConfig) {
if (activeNetworkConfig.priceFeed != address(0)) {
return activeNetworkConfig;
}
vm.startBroadcast();
MockV3Aggregator mockPriceFeed = new MockV3Aggregator(
DECIMALS,
INITIAL_PRICE
);
vm.stopBroadcast();
emit HelperConfig__CreatedMockPriceFeed(address(mockPriceFeed));
anvilNetworkConfig = NetworkConfig({priceFeed: address(mockPriceFeed)});
}
}Здесь через конструктор мы определяем сеть для деплоя и с помощью дополнительных функций решаем, что возвращать: либо адрес задеплоенного контракта в реальной сети, либо mock контракт, который мы можем использовать в локальной сети Anvil!
В общем получается, что мы создали специальные скриптовые контракты для определения сети деплоя и разворачивания нашего основного контракта с правильным адресами ChainlinkPriceFeed.
После этого мы можем выполнять в консоли команды для теста, всего лишь меняя нашу ссылку rpc-link, вместо того, чтобы каждый раз изменять код контракта.
Если вы создаете свой протокол сразу для нескольких сетей, крайне рекомендую этот паттерн для проведения тестов.
Более подробно можно посмотреть по ссылке на видео в начале поста.
#foundry #lesson31
❤1👍1
Foundry с 0. Часть 32
В процессе знакомства с новым конкурсным проектом заметил новый для себя способ написания тестов, о котором самое время рассказать в рамках этого цикла постов.
Во всех уроках и видео нам говорят, что тесты в Foundry пишутся с пустыми аргументами в функциях, например:
function testIncremet() public {}
А если мы указываем аргументы, то это уже получается фаззинг, в котором Foundry сам подставляет нужные значения.
В протоколе Morpho пошли немного дальше и попытались как бы объединить об этих способа.
У них есть тест для проверки исполнения функции займа:
И в значения аргументов этой функции записываются результаты работы другой служебной функции из контракта помощника:
Тут они ограничиваются необходимыми рамками, делаются дополнительные проверки и готовые значения передаются для тестов.
На мой взгляд достаточно интересное решение вместо того, чтобы городить полотна кода.
Посмотреть их тесты и разобраться самим можно по ссылке на их репо.
P.S. Репо открыт на время конкурса, не уверен, что будет в доступе постоянно.
#foundry #lesson32
В процессе знакомства с новым конкурсным проектом заметил новый для себя способ написания тестов, о котором самое время рассказать в рамках этого цикла постов.
Во всех уроках и видео нам говорят, что тесты в Foundry пишутся с пустыми аргументами в функциях, например:
function testIncremet() public {}
А если мы указываем аргументы, то это уже получается фаззинг, в котором Foundry сам подставляет нужные значения.
В протоколе Morpho пошли немного дальше и попытались как бы объединить об этих способа.
У них есть тест для проверки исполнения функции займа:
function testBorrowAssets(
uint256 amountCollateral,
uint256 amountSupplied,
uint256 amountBorrowed,
uint256 priceCollateral
) public {
(amountCollateral, amountBorrowed, priceCollateral) =
_boundHealthyPosition(amountCollateral, amountBorrowed, priceCollateral);
amountSupplied = bound(amountSupplied, amountBorrowed, MAX_TEST_AMOUNT);
_supply(amountSupplied);
oracle.setPrice(priceCollateral);
collateralToken.setBalance(BORROWER, amountCollateral);
vm.startPrank(BORROWER);
morpho.supplyCollateral(marketParams, amountCollateral, BORROWER, hex"");
uint256 expectedBorrowShares = amountBorrowed.toSharesUp(0, 0);
(uint256 returnAssets, uint256 returnShares) =
morpho.borrow(marketParams, amountBorrowed, 0, BORROWER, RECEIVER);
vm.stopPrank();
...
}
И в значения аргументов этой функции записываются результаты работы другой служебной функции из контракта помощника:
function _boundHealthyPosition(uint256 amountCollateral, uint256 amountBorrowed, uint256 priceCollateral)
internal
view
returns (uint256, uint256, uint256)
{
priceCollateral = bound(priceCollateral, MIN_COLLATERAL_PRICE, MAX_COLLATERAL_PRICE);
amountBorrowed = bound(amountBorrowed, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);
uint256 minCollateral = amountBorrowed.wDivUp(marketParams.lltv).mulDivUp(ORACLE_PRICE_SCALE, priceCollateral);
if (minCollateral <= MAX_COLLATERAL_ASSETS) {
amountCollateral = bound(amountCollateral, minCollateral, MAX_COLLATERAL_ASSETS);
} else {
amountCollateral = MAX_COLLATERAL_ASSETS;
amountBorrowed = Math.min(
amountBorrowed.wMulDown(marketParams.lltv).mulDivDown(priceCollateral, ORACLE_PRICE_SCALE),
MAX_TEST_AMOUNT
);
}
vm.assume(amountBorrowed > 0);
vm.assume(amountCollateral < type(uint256).max / priceCollateral);
return (amountCollateral, amountBorrowed, priceCollateral);
}
Тут они ограничиваются необходимыми рамками, делаются дополнительные проверки и готовые значения передаются для тестов.
На мой взгляд достаточно интересное решение вместо того, чтобы городить полотна кода.
Посмотреть их тесты и разобраться самим можно по ссылке на их репо.
P.S. Репо открыт на время конкурса, не уверен, что будет в доступе постоянно.
#foundry #lesson32
👍2
Foundry с 0. Часть 33
Раз уж в прошлом посты мы познакомились с протоколом Morpho, то сейчас можно разобрать еще один их репо, в котором показано как работать с оракулами Chainlink и писать тесты для них.
Вот ссылка на сам репо.
https://github.com/morpho-org/morpho-blue-oracles
Кратко описывая работу контракта можно сказать, что это реализация создана исключительно для работы сервиса Morpho и просто так использовать его в своем протоколе не получится.
Контракт наследует от интерфейса IOracle, подключает служебные библиотеки, а также использует Chainlink Data Feed для получения актуальных цен на токены.
В контракте всего одна функция и конструктор, в который передаются адреса контрактов Chainlink, с который мы хотим получить информацию.
Функции price() просто возвращает немного измененную цену на токен. Тут происходят преобразования суммы (scale) для последующей работы с ней в основном контракте Morpho.
Теперь разберем файл теста.
https://github.com/morpho-org/morpho-blue-oracles/blob/main/test/ChainlinkOracleTest.sol
Здесь подключаются необходимые библиотеки и указываются адреса необходимых контрактов Chainlink, расположенных в основной сети.
Далее в функции setUp() мы создаем подключение и делаем форк сети:
ETH_RPC_URL - переменная среды, в которую нам нужно определить нашу rpc ссылку и сохранить в файле .env.
Далее можно писать функции для тестирования:
В целом, все довольно просто.
Вы также можете создать специальный скрипт для деплоя в разные сети и указать адреса контрактов Chainlink там. Пример этого вы можете найти в позапрошлом посте.
#foundry #lesson33
Раз уж в прошлом посты мы познакомились с протоколом Morpho, то сейчас можно разобрать еще один их репо, в котором показано как работать с оракулами Chainlink и писать тесты для них.
Вот ссылка на сам репо.
https://github.com/morpho-org/morpho-blue-oracles
Кратко описывая работу контракта можно сказать, что это реализация создана исключительно для работы сервиса Morpho и просто так использовать его в своем протоколе не получится.
Контракт наследует от интерфейса IOracle, подключает служебные библиотеки, а также использует Chainlink Data Feed для получения актуальных цен на токены.
В контракте всего одна функция и конструктор, в который передаются адреса контрактов Chainlink, с который мы хотим получить информацию.
Функции price() просто возвращает немного измененную цену на токен. Тут происходят преобразования суммы (scale) для последующей работы с ней в основном контракте Morpho.
Теперь разберем файл теста.
https://github.com/morpho-org/morpho-blue-oracles/blob/main/test/ChainlinkOracleTest.sol
Здесь подключаются необходимые библиотеки и указываются адреса необходимых контрактов Chainlink, расположенных в основной сети.
Далее в функции setUp() мы создаем подключение и делаем форк сети:
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
} ETH_RPC_URL - переменная среды, в которую нам нужно определить нашу rpc ссылку и сохранить в файле .env.
Далее можно писать функции для тестирования:
function testOracleWbtcUsdc() public {
ChainlinkOracle oracle = new ChainlinkOracle(vaultZero, wBtcBtcFeed, btcUsdFeed, usdcUsdFeed, feedZero, 1, 8, 6);
(, int256 firstBaseAnswer,,,) = wBtcBtcFeed.latestRoundData();
(, int256 secondBaseAnswer,,,) = btcUsdFeed.latestRoundData();
(, int256 quoteAnswer,,,) = usdcUsdFeed.latestRoundData();
assertEq(
oracle.price(),
(uint256(firstBaseAnswer) * uint256(secondBaseAnswer) * 10 ** (36 + 8 + 6 - 8 - 8 - 8))
/ uint256(quoteAnswer)
);
}В целом, все довольно просто.
Вы также можете создать специальный скрипт для деплоя в разные сети и указать адреса контрактов Chainlink там. Пример этого вы можете найти в позапрошлом посте.
#foundry #lesson33
👍2❤1
Foundry с 0. Часть 34
Разбирая темы написания тестов с использованием различных паттернов, сервисов и сторонних программ, я пришел к выводу, что, в целом, алгоритм действий очень похож.
Сначала вы определяете для себя, на каких сетях хотите проводить тесты своего протокола, затем описываете набор сторонних контрактов и сервисов, и в завершении все это подключаете в свои тесты через setUp() и скрипты.
В прошлом посте был небольшой пример работы с оракулами Chainlink, и хочу сказать, что с DeFi платформами примерно тоже самое: нашли контракт, подключили, сделали форк и проводите тесты.
Кстати сказать, что компания Immunefi ранее в 2023 выпустила прекрасный набор POC-шаблонов, в которых вы можете подсмотреть как писать тесты для:
1. Флешзайма с Uniswap (V2 и V3), Aave, Balancer, Euler;
2. Манипуляции ценой с использованием dex платформ;
3. Реентранси;
Ссылка на их репо: https://github.com/immunefi-team/forge-poc-templates/tree/main
Даже если не использовать их шаблоны напрямую, можно подсмотреть, как это реализовано и постараться сделать у себя также или лучше.
С мостами дела обстоят несколько сложнее, так как некоторые расчеты происходят вне сети (оффчейн). В тестах за оффчейн операции часто отвечает javanoscript, а в остальном написание ничем не отличается от уже пройденных нами.
Вы можете сами убедиться в этом, прочитав эту статьи с примерами по тестам мостов:
1. Setting Up A Bridge With Foundry
https://medium.com/immunefi/setting-up-a-bridge-with-foundry-c0d807a3998
Вообще, насколько я понял, самое трудное при работе с проектом, на котором уже были написаны много тестов, это разобраться в архитектуре и понять замысел разработчиков.
Из своего опыта знаю, что на разбор контрактов помощников (Handler, Helper, Utils) действующего проекта, может уйти столько же времени, как и на аудит хорошего контракта.
Кстати, замечал не раз, что у разработчиков есть привычка называть функцию теста тем, что он тестирует (это определенно правильно), но нет привычки писать комментарии к служебным функция в тестах. Например, они сделали настройку определенного состояния контракта на момент теста, но забыли описать, зачем это было сделано. И при том, что тест будет пройден, уязвимость в самом коде остается.
В общем, делайте как можно больше комментариев к своим тестам, служебным функциям и состояниям контракта на момент теста.
#foundry #lesson34
Разбирая темы написания тестов с использованием различных паттернов, сервисов и сторонних программ, я пришел к выводу, что, в целом, алгоритм действий очень похож.
Сначала вы определяете для себя, на каких сетях хотите проводить тесты своего протокола, затем описываете набор сторонних контрактов и сервисов, и в завершении все это подключаете в свои тесты через setUp() и скрипты.
В прошлом посте был небольшой пример работы с оракулами Chainlink, и хочу сказать, что с DeFi платформами примерно тоже самое: нашли контракт, подключили, сделали форк и проводите тесты.
Кстати сказать, что компания Immunefi ранее в 2023 выпустила прекрасный набор POC-шаблонов, в которых вы можете подсмотреть как писать тесты для:
1. Флешзайма с Uniswap (V2 и V3), Aave, Balancer, Euler;
2. Манипуляции ценой с использованием dex платформ;
3. Реентранси;
Ссылка на их репо: https://github.com/immunefi-team/forge-poc-templates/tree/main
Даже если не использовать их шаблоны напрямую, можно подсмотреть, как это реализовано и постараться сделать у себя также или лучше.
С мостами дела обстоят несколько сложнее, так как некоторые расчеты происходят вне сети (оффчейн). В тестах за оффчейн операции часто отвечает javanoscript, а в остальном написание ничем не отличается от уже пройденных нами.
Вы можете сами убедиться в этом, прочитав эту статьи с примерами по тестам мостов:
1. Setting Up A Bridge With Foundry
https://medium.com/immunefi/setting-up-a-bridge-with-foundry-c0d807a3998
Вообще, насколько я понял, самое трудное при работе с проектом, на котором уже были написаны много тестов, это разобраться в архитектуре и понять замысел разработчиков.
Из своего опыта знаю, что на разбор контрактов помощников (Handler, Helper, Utils) действующего проекта, может уйти столько же времени, как и на аудит хорошего контракта.
Кстати, замечал не раз, что у разработчиков есть привычка называть функцию теста тем, что он тестирует (это определенно правильно), но нет привычки писать комментарии к служебным функция в тестах. Например, они сделали настройку определенного состояния контракта на момент теста, но забыли описать, зачем это было сделано. И при том, что тест будет пройден, уязвимость в самом коде остается.
В общем, делайте как можно больше комментариев к своим тестам, служебным функциям и состояниям контракта на момент теста.
#foundry #lesson34
👍2❤1🤩1
Foundry с 0. Часть 35
Сегодня хочу коснуться темы дебаггинга в Foundry.
Я сам далеко не спец в детальных проходах по опкодам и пошаговому исследованию состояний памяти и хранилища, да и обучающих видео, достойных ссылок, я не нашел, поэтому будет лишь представление команд и описание их результатов.
Для того чтобы хорошо разбираться в дебаггинге нужно хорошо понимать несколько вещей:
1. Как работает EVM "под капотом";
2. Как работают опкоды;
3. Уметь писать код на assembly, хотя бы на базовом уровне;
4. Понимать как работает стек в Solidity;
5. Понимать как работает memory;
У меня есть "провисания" в знаниях в некоторых темах, которые я регулярно отслеживаю и доучиваю, но все равно иногда сложно "дойти до сути".
В первоначальном знакомстве со всем этим может помочь прекрасное видео от Ильи, где он разбирает байткод и показывает некоторые опкоды.
https://youtu.be/pz8NeV6bo3E
Но вернемся к дебаггингу в Foundry.
Как я понял из комментариев разработчиков, сейчас дебаггинг еще на начальном уровне и у команды большие планы на него. И сейчас эта опция скорее в формате "просмотра начинки".
Итак, допустим, что у нас есть некоторая функция в тестах, работу которой мы хотим разобрать.
Открываем наш терминал и прописываем команду:
например:
Наш терминал, как бы разделится на 4 части с 4 разными окошками.
Вверху будут окошко опкодов и стека, а внизу - актуальный код контракта или его функция и отображение memory.
Вы также можете встретить цветовое обозначение в одном из этих окошек:
1. Красный - то, что записывается текущим опкодом,
2. Зеленый - то, что записалось предыдущим опкодом,
3. Голубой - то, что читается текущим опкодом
Навигация по значения в каждом окошке совершается с помощью клавиш на клавиатуре. Ниже окна дебаггера будет их обозначение, также его можно посмотреть тут:
https://book.getfoundry.sh/forge/debugger
В процессе своего обучения и работы с конкурсными аудитами, я не часто встречался лицом к лицу с дебаггингом (кода или транзакций, не важно), поэтому если у вас есть хорошие видео на эту тему, я с удовольствием их посмотрю и поделюсь на канале.
Приятного обучения!
#foundry #lesson35
Сегодня хочу коснуться темы дебаггинга в Foundry.
Я сам далеко не спец в детальных проходах по опкодам и пошаговому исследованию состояний памяти и хранилища, да и обучающих видео, достойных ссылок, я не нашел, поэтому будет лишь представление команд и описание их результатов.
Для того чтобы хорошо разбираться в дебаггинге нужно хорошо понимать несколько вещей:
1. Как работает EVM "под капотом";
2. Как работают опкоды;
3. Уметь писать код на assembly, хотя бы на базовом уровне;
4. Понимать как работает стек в Solidity;
5. Понимать как работает memory;
У меня есть "провисания" в знаниях в некоторых темах, которые я регулярно отслеживаю и доучиваю, но все равно иногда сложно "дойти до сути".
В первоначальном знакомстве со всем этим может помочь прекрасное видео от Ильи, где он разбирает байткод и показывает некоторые опкоды.
https://youtu.be/pz8NeV6bo3E
Но вернемся к дебаггингу в Foundry.
Как я понял из комментариев разработчиков, сейчас дебаггинг еще на начальном уровне и у команды большие планы на него. И сейчас эта опция скорее в формате "просмотра начинки".
Итак, допустим, что у нас есть некоторая функция в тестах, работу которой мы хотим разобрать.
Открываем наш терминал и прописываем команду:
forge test --debug FUNC
например:
forge test --debug "testSomething()"
Наш терминал, как бы разделится на 4 части с 4 разными окошками.
Вверху будут окошко опкодов и стека, а внизу - актуальный код контракта или его функция и отображение memory.
Вы также можете встретить цветовое обозначение в одном из этих окошек:
1. Красный - то, что записывается текущим опкодом,
2. Зеленый - то, что записалось предыдущим опкодом,
3. Голубой - то, что читается текущим опкодом
Навигация по значения в каждом окошке совершается с помощью клавиш на клавиатуре. Ниже окна дебаггера будет их обозначение, также его можно посмотреть тут:
https://book.getfoundry.sh/forge/debugger
В процессе своего обучения и работы с конкурсными аудитами, я не часто встречался лицом к лицу с дебаггингом (кода или транзакций, не важно), поэтому если у вас есть хорошие видео на эту тему, я с удовольствием их посмотрю и поделюсь на канале.
Приятного обучения!
#foundry #lesson35
👍2❤1🤩1
Foundry с 0. Часть 36
Ребята, крепитесь, остались последние три поста по Foundry и дальше будем выбирать новую тему или делать общие посты по Solidity и аудиту, а пока...
Короткий пост по написанию тестов для контрактов на yul.
Как вы можете знать, или догадываться, контракты могут быть написаны не только на Solidity, но и чисто на yul, т.е. низкоуровневом языке.
За все время работы со смарт контрактами мне ни разу не встречались подобные проекты.
Ну, разве что в аудите ZK Sync был служебный контракт Bootloader...
Так вот, проводить тесты для таких проектов все же надо, но как это сделать?
Один из участников канала (спасибо @dmitryq) поделился ссылкой на репо, который позволяет в удобном формате подготовить yul контракт.
В этом репо вы можете найти сам пример подготовки теста:
https://github.com/crystalbit/foundry-yul-boilerplate
А в этом контракты помощники для деплоя:
https://github.com/crystalbit/deploy-yul
В целом там все достаточно понятно и просто. Вам потребуется импортировать несколько дополнительных контрактов в файл теста:
import "deploy-yul/BytecodeDeployer.sol";
import "deploy-yul/YulDeployer.sol";
И подготовить необходимые контракты вашего протокола:
Если вы проходили все предыдущие посты из цикла Foundry с 0, то данный процесс работы не составит для вас труда.
#foundry #lesson36
Ребята, крепитесь, остались последние три поста по Foundry и дальше будем выбирать новую тему или делать общие посты по Solidity и аудиту, а пока...
Короткий пост по написанию тестов для контрактов на yul.
Как вы можете знать, или догадываться, контракты могут быть написаны не только на Solidity, но и чисто на yul, т.е. низкоуровневом языке.
За все время работы со смарт контрактами мне ни разу не встречались подобные проекты.
Ну, разве что в аудите ZK Sync был служебный контракт Bootloader...
Так вот, проводить тесты для таких проектов все же надо, но как это сделать?
Один из участников канала (спасибо @dmitryq) поделился ссылкой на репо, который позволяет в удобном формате подготовить yul контракт.
В этом репо вы можете найти сам пример подготовки теста:
https://github.com/crystalbit/foundry-yul-boilerplate
А в этом контракты помощники для деплоя:
https://github.com/crystalbit/deploy-yul
В целом там все достаточно понятно и просто. Вам потребуется импортировать несколько дополнительных контрактов в файл теста:
import "deploy-yul/BytecodeDeployer.sol";
import "deploy-yul/YulDeployer.sol";
И подготовить необходимые контракты вашего протокола:
YulDeployer yulDeployer = new YulDeployer();
BytecodeDeployer bytecodeDeployer = new BytecodeDeployer();
address basic;
address basicBytecode;
function setUp() external {
basic = yulDeployer.deployContract("Basic");
basicBytecode = bytecodeDeployer.deployContract("Basic");
}
Если вы проходили все предыдущие посты из цикла Foundry с 0, то данный процесс работы не составит для вас труда.
#foundry #lesson36
❤3👍1🤩1