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

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
👍21
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
👍21🤩1
Foundry с 0. Часть 35

Сегодня хочу коснуться темы дебаггинга в 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
👍21🤩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";


И подготовить необходимые контракты вашего протокола:

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
Foundry с 0. Часть 37

Когда я разбирался с документацией Foundry, то видел там раздел по написанию тестов для подписей стандарта EIP712.

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

Вот сама ссылка на документацию:

https://book.getfoundry.sh/tutorials/testing-eip712

А сейчас мы быстро пробежимся по коду.

Итак, для написания тестов в данном случае мы будем работать с токеном ERC20 от компании Solmate, так как там уже по умолчанию идет функция permit().

https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol

Нам также потребуются еще два контракта: это MockToken, который будет наследовать от ERC20 и SigUtils, который поможет правильно работать с подписями.

Наш токен предельно простой:

contract MyToken is ERC20 {
address public owner;
constructor(address newOwner) ERC20("token", "MTK", 18) {}

function mint(address user, uint256 amount) public {
_mint(user, amount);
}
}


SigUtils чуть более длинный:

contract SigUtils {
bytes32 internal DOMAIN_SEPARATOR;

constructor(bytes32 _DOMAIN_SEPARATOR) {
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
}

// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}

function getStructHash(Permit memory _permit)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
PERMIT_TYPEHASH,
_permit.owner,
_permit.spender,
_permit.value,
_permit.nonce,
_permit.deadline
)
);
}

function getTypedDataHash(Permit memory _permit)
public
view
returns (bytes32)
{
return
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
getStructHash(_permit)
)
);
}
}


Все просто: мы создали структуру подписи подобную ERC20 permit() и две функции для хеширования структуры подписи и цельных данных.

И теперь мы готовы писать тесты для нашего токена.

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

function setUp() public {
token = new MyToken(address(this));
sigUtils = new SigUtils(token.DOMAIN_SEPARATOR());

ownerPrivateKey = 0xA11CE;
spenderPrivateKey = 0xB0B;

owner = vm.addr(ownerPrivateKey);
spender = vm.addr(spenderPrivateKey);

token.mint(owner, 1e18);
}


После этого можно написать первый тест для функции permit():

function test_Permit() public {
SigUtils.Permit memory permit = SigUtils.Permit({
owner: owner,
spender: spender,
value: 1e18,
nonce: 0,
deadline: 1 days
});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

token.permit(
permit.owner,
permit.spender,
permit.value,
permit.deadline,
v,
r,
s
);

assertEq(token.allowance(owner, spender), 1e18);
assertEq(token.nonces(owner), 1);
}


Тут мы создаем структуры с помощью контракта SigUtils, хешируем и подписываем с помощью приватного ключа, затем уже вызываем функцию permit() и делаем проверки на nonce и allowance.

В файле примере, который вы найдете в конце поста, можно посмотреть тесты на: срок действия подписи, невалидного signer и nonce, а также на signature replay.
👍32
Также в документации можно найти и другой пример работы с подписями (depositWithPermit), но на деле там все тоже самое с использованием нашего контракта помощника SigUtils.

Надеюсь, тестирование подписей стало теперь чуть проще для вас.

А тут вы найдете все тесты для нашего ERC20 и его функции permit().

#foundry #lesson37
👍21🤩1
Foundry с 0. Часть 38

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

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

И для этого я предлагаю вам разобрать нашумевший взлом протокола Kyberswap!

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

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

1. KyberSwap Hack Explained

2. A Deep Dive Into the KyberSwap Hack

3. KyberSwap - REKT

А уже после постарайтесь разобраться с контрактами и тестами из этого репо:

https://github.com/paco0x/kyber-exploit-example

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

Итак, за дело! Приятного обучения!

#foundry #lesson38
👍21🔥1🤩1
Foundry c 0. Полный цикл постов

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

Надеюсь вам он не только понравился, но и оказался очень полезным и своевременным!

Буду рад репостам и комментариям!

P.S. Да, осталась еще пара более сложных тем, которые я буду добавлять по мере нахождения полезного материала и разборов!


Введение


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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

Foundry с 0. Часть 16. Консоль и логирование


Foundry с 0. Часть 17. Покрытие тестами

Foundry с 0. Часть 18. Hardhat и Foundry


Первые шаги


Foundry с 0. Часть 19. Деплой контрактов

Foundry с 0. Часть 20. Работа с токенами

Foundry с 0. Часть 21. Работа со временем

Foundry с 0. Часть 22. Форк тесты


Продвинутые тесты


Foundry с 0. Часть 23. FFI тесты

Foundry с 0. Часть 24. Фазз тесты

Foundry с 0. Часть 25. Другие команды

Foundry с 0. Часть 26. Мутационные тесты

Foundry с 0. Часть 27. Инвариант тесты. Общее

Foundry с 0. Часть 28. Инвариант тесты. WETH

Foundry с 0. Часть 29. Инвариант тесты. Handler

Foundry с 0. Часть 30. Инвариант тесты. Actors


Паттерны и примеры


Foundry с 0. Часть 31. Работа с разными сетями

Foundry с 0. Часть 32. Паттерн с функциями в тестах

Foundry с 0. Часть 33. Работа с оракулами

Foundry с 0. Часть 34. Биржи, мосты и прокси

Foundry с 0. Часть 35. Дебаггинг

Foundry с 0. Часть 36. Работа с yul в тестах

Foundry с 0. Часть 37. Работа с подписями EIP712


Финальный практикум


Foundry с 0. Часть 38. Финальный практикум


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

Приятного обучения!

#foundry #summary
🔥393🤩3👍1
Foundry с 0. Часть 39

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

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

P.S. Напомню, что .env файлы обязательно нужно ставить в gitignore, чтобы случайно не загрузить конфиденциальную информацию в публичные репо. Из-за этого уже некоторые протоколы имели проблемы со взломами...
Теперь приватные ключи можно хранить в специальном зашифрованном файле под паролем на вашем компьютере. Весь процесс занимает всего пару минут.

Открываем наш терминал и прописываем команду:

cast wallet import defautKey --interactive

Там появится поле:

Enter private key

куда следует ввести наш приватный ключ, например:

0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

P.S. Это ключ от первого аккаунта в Anvil, можете потренироваться сами.

Далее вводим пароль для нашего файла:

Enter password

В итоге в терминале появится адрес, типа:

0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Его нужно записать для дальнейшего использования.

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

cast wallet list

А сейчас немного практики. Если раньше при деплое контрактов с помощью скрипта мы выполняли команду:

forge noscript noscriptPath --rpc-url $URL --private-key $KEY

то теперь она другая:

forge noscript noscriptPath --rpc-url $URL --account defaultKey --sender addressKey --broadcast

где addressKey - это как раз тот адрес, который мы сохраняли после создания файла приватного ключа.

В терминале снова появится строка, в которой нам нужно ввести пароль:

Enter keystore password

только после этого будет выполнен деплой!

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

Сам файл с приватным ключом можно посмотреть в корневой папке Foundry с помощью команд:

cd .foundry/keystore
cat defaultKey


тут будет хранится зашифрованная версия приватного ключа по стандарту EIP-2335.

Заботьтесь о своей безопасности в сети!

#foundry #lesson39
🔥7👍1
Solidity Tip

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

В функциях возможно определить локальный маппинг, но с указанием на storage data location (не уверен, как это точно перевести).

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

#solidity #mapping #tip
👍9
Игры с байткодом

Байткод контрактов обычно начинается с 6080604052, который генерируется за счет операции mstore(0x40,0x80). Но возможен и такой вариант:

Попробуйте выполнить операцию mstore(0x40,returndatasize()) в самом начале. Ваш байткод будет начинаться с 3d604052!

Пример кода можно увидеть на скрине выше.

P.S. По слухам, это может помешать верификации на Etherscan, так что пробуйте с осторожностью.  

#bytecode #tip
1👍1
Работа с оракулами Chainlink

Увидел в твиттере пост gmhacker.eth, который поделился ссылкой на репо LiquidityProtocol.

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

1. Когда система получает данные о цене от Chainlink;
2. Когда система получает данные от другого оракула;
3. Когда оба оракула не заслуживают доверия;
4. Использование другого оракула, когда Chainlink заморожен;
5. Использование Chainlink, когда нет доверия другому оракулу;

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

Рекомендую всем обратиться к первоисточнику и изучить контракт!

https://github.com/liquity/dev/blob/main/packages/contracts/contracts/PriceFeed.sol

А как вы работаете с оракулами?

#oracle #chainlink
Проблемы с Древом Меркла

Ранее в этом году я выкладывал небольшой пост со ссылкой на ветку в Твиттере, в которой разбиралась уязвимость Древа Меркла в библиотеке от Open Zeppelin.

Чуть позже в одном из чатов видел дискуссию о комментарии в этом же контракте:

You should avoid using leaf values that are 64 bytes long prior to hashing, or use a hash function other than keccak256 for hashing leaves.

И вот только сейчас встретил статью, которая помогла многие вещи поставить на место. В ней рассказывается об second preimage attack и почему разработчики должны избегать хеширования листьев древа в 64 байта. Более того, в статье показываются способы, как избежать этой проблемы.

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

Поэтому рекомендую каждому ее прочитать от и до:

https://www.rareskills.io/post/merkle-tree-second-preimage-attack

Ну, а если вы, как и я, зададитесь вопросом, почему это SECOND preimage attack, а где же FIRST, то вот эта статья поможет ответить на вопрос.

Надеюсь, вопросов по Древу Меркла у вас стало теперь меньше.

#merkletree #merkle
🔥41
Подключение WSL для Windows

В последнее время у меня появилась необходимость работы с некоторыми программами, которые не работают на Windows и требуются либо Mac, либо Linux. Поэтому я решил попробовать установить WSL на свой компьютер, описать процесс и возникшие сложности.

Подсистема Windows для Linux (WSL) позволяет разработчикам запускать среду GNU/Linux с большинством программ командной строки, служебных программ и приложений непосредственно в Windows без каких-либо изменений и необходимости использовать традиционную виртуальную машину или двойную загрузку.

Простыми словами - это виртуалка Linux на Windows. Вот тут можно узнать об этом чуть больше:

https://learn.microsoft.com/ru-ru/windows/wsl/about

А по этому описанию мы и будем производить установку:

https://learn.microsoft.com/ru-ru/windows/wsl/install

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

При попытке выполнить команду wsl --install в терминале, у меня возникла ошибка 0xc004000d. Немного погуглив, я нашел ее решение.

Вам нужно зайти в панель управление в Windows (обратите внимание, не Параметры, а именно Панель управления), дальше идем в раздел Программы и кликаем на пункт Включение и отключение компонентов Windows. Появится новое окошко и там нужно поставить флажок на пункте Подсистема Windows для Linux. Затем перезагружаем компьютер.

Теперь можно выполнить необходимую команду.

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

wsl --install -d Ubuntu 2

После этого возможна еще одна перезагрузка компьютера или же откроется окно Ubuntu и система попросит вас придумать имя и пароль.

Теперь в нашем терминале будет доступен еще один вид Ubuntu. Его можно найти, если нажать на небольшую стрелочку "вниз" рядом со вкладкой терминала.

Дальше можно настроить совместимость нашего редактора кода - VS Code.

Запускаем редактор, открываем раздел плаггинов и ищем Remote Development от Microsoft. Это пакет из 4 приложений, так что устанавливаем их все.

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

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

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

Рекомендую в терминале создать отдельную папку Development / Audits, чтобы самому не потеряться позже.

Это делается с помощью команд:

mkdir Audits
cd Audits/


Также, есть вероятность, что потребуется заново установить и настроить git, python, Node.js, npm, yarn и другие программы, которыми вы пользовались на Windows.

Хочу обратить ваше внимание на то, что есть две версии WSL и WSL2.

Не могу сейчас сказать, в чем там разница, но в моем случае возникали проблемы при обновлении Node.js и npm с первой версией WSL.

Также уточню, что вам желательно иметь уже некий опыт работы с Linux системами, иначе настройка всех пакетов в WSL может вызвать у вас большие трудности.

#wsl
👍1
Курс по безопасности от Патрика Коллинса

Если вы не в курсе, то Патрик Коллинс (тот кто создал 32 часовой курс по Solidity и 27 часовой по Foundry), планирует запустить платформу для обучения всем необходимым навыкам по web3 - Cyfrin Updraft.

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

Они решили открыть один курс для общего доступа без всяких кодов.

Этот курс по Безопасности и аудиту. Рассчитан на 27 часов. Как я понял, сейчас это "звездный" курс для привлечения внимания от популярных деятелей в сфере аудита: Tincho, Owen Thrum, Andi Li, Johny Time, Pashov и других.

Каждый урок занимает 5 - 15 минут. Можно проходить в своем темпе. Уверен, это достойное занятие для себя до конца года.

Пройти курс можно тут:

https://updraft.cyfrin.io/courses/security

Будет интересно узнать ваше мнение об этом.

#updraft
🔥11👍1