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

Продолжаем совершенствоваться с инвариант тестами и сегодня поговорим о таком подвиде тестов, как 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.

Дальше мы можем написать небольшой тест:

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, что имитирует вызовы функций от разных пользователей.

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

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

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 пошли немного дальше и попытались как бы объединить об этих способа.

У них есть тест для проверки исполнения функции займа:

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