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

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

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

Контракты конкурсного аудита можно посмотреть тут, а тут ссылка на контракт и функцию из примера.

Итак, посмотрим на функцию с побитовой операцией:

  function _updateStakerHistory(
Staker storage staker,
uint256 latestPrincipal,
uint256 latestStakedAtTime
) internal {
staker.history.push(
s_checkpointId++,
(uint224(uint112(latestPrincipal)) << 112) | uint224(uint112(latestStakedAtTime))
);
}

s_checkpointId - это просто уникальный идентифкатор для ведения учета, который увеличивается на +1 при каждом вызове функции.

(uint224(uint112(latestPrincipal)) << 112) - сначала значение latestPrincipal уменьшается до uint112, а затем увеличивается до uint224, для того чтобы вместить значение для операции сдвига влево.

uint224(uint112(latestStakedAtTime)) - берем значение latestStakedAtTime и также приводим его к uint112 в начале, и к uint224 позже.

Побитовая операция OR (вот эта палочка - "|" между значениями) служит для объединения ранее сдвинутого latestPrincipal со latestStakedAtTime.

В результате получается, что latestPrincipal занимает верхние 112 бит, а latestStakedAtTime - нижние 112 бит.

Для себя и тех, кто забыл, напомню, как работает побитовое OR (ИЛИ).

Допустим у нас есть два значения:

а 1011010101
b 0111010111

Если хотябы одно значение будет равно "1", то и результат будет равен "1". Отсюда получаем:

с 1111010111

Т.е. вы поняли теперь как работает функция в chainlink? Мы берем значение, обрезаем его до uint112 и тут же увеличивает до uint224, освобождая место при помощи сдвига влево (<<) для другого значения, которое мы и записываем на освободившееся пространство.

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

Прекрасное компактное решение от команды Chainlink!

#bit #or #shift
👍4🔥3
Плагин для VSCode

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

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

Вот нашел для себя простой плагин Mark files:

После установки, кликаете правой кнопкой мыши на древе файлов и в появившемся меню выбираете "Mark \ unmark selected file". Радом с файлом или папкой появится маленький значок.

В общем, крайне простой инструмент.

#plaggin
👍7
Return в Solidity и return в assembly

Знали ли вы, что return в assembly ведет себя по-другому, чем return в solidity?

В assembly return фактически является опкодом, который прекращает выполнение контекста и возвращает срез (часть информации) памяти.

Например, в функции:

function someLogic() external returns(bool success) {

assembly {
return(0x00, 0x20)
}
_someMoreLogic();
}

действие никогда не дойдет до _someMoreLogic(), прекратившись на участке assembly.

В solidity "return <value>" как бы говорит компилятору, что функция завершила свое выполнение и <value> должно быть возвращено для следующего контекста.

Для external функций это, по сути, означает вызов Return, а для internal - типа "просто возвращайся".

Return в solidity служит как полезная абстракция и позволяет нашим функциям прекращаться раньше, порой избегая другую логику исполнения, как например тут:

function someLogic() internal {
if (isOwner()) return;
uint fee = calculateFee();
_charheFee();
}

Если же мы хотим создать подобную логику с помощью assembly, нам потребуется использовать for циклы:

function someLogic() internal {

assembly{

for {} 1 {} {
if eq(caller(), sload(owner, slot)) {
break
}

let fee := calcFee()
break

}
}
}

В этом случае for {} 1 {} {} выступает эквивалентом while(true), и исполнение может прекратиться либо после первого if, при вополнении условий, либо уже в конце функции.

Пост переведен из данной ветки Твиттера от philogy.

Фух, я еще постигаю assembly и мне крайне интересно, как работает вся эта штуковина изнутри.

#return #assembly
5👍1
Сборник советов по оптимизации газа

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

Всего приведено около 70 примеров! В общем, интересное чтиво в свободное время.

Сборник от RareSkills

Будет также полезно тем, кто пишет своего бота и детекторы на скан контрактов!

#gas
🔥4👍1
Баг в протоколе Lybra

Интересная находка была в конкурсном аудите протокола Lybra, в наборе которого были прокси контракты.

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

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

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

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

Вот такой очевидный и не очень баг.

Более детально о нем можно прочитать тут, в отчете code4rena.

Будьте внимательны при работе с прокси!

#proxy #constructor
👍4
Checks-Effects-Interactions устарел?

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

В статье рассказывается о новом подходе к написанию безопасных функций, который включает в себя проверку инвариантов и называется FREI-PI (Function Requirements-Effects-Interactions + Protocol Invariants).

На самом деле, некоторые аудиторы уже могли встречать его, просматривая контракты таких протоколов, как dYdX, Compound или Aave.

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

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

#freypi #cie
👍6
Повышаем мастерство с Forge coverage

Я уже несколько раз писал на канале, что в скором времени компании будут нанимать отдельных разработчиков для написания тестов для своих протоколов. А знания и навыки работы с Hardhat или Foundry будут стоять в описаниях вакансий с Solidity, как это сейчас с JS/React. 

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

Например, вы знали, что с foundry coverage можно запустить дополнительную команду report debug, которая покажет, какие строки и функции вы забыли протестировать?

Или, что с помощью forge coverage --report lcov можно сформировать отчет в html разметке?

Или, что, используя плагин Coverage Gutters, можно визуально в контракте увидеть код без тестов?

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

В статье даже есть репо, с которым вы можете потренироваться!

Советую всем и каждому повысить свои навыки в работе с Foundry.

#foundry #coverage
🔥6
Более дешевый способ проверки на нулевой адрес

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

Вот сам код:

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.20;

contract AssemblyAddressZero  { 
  error ZeroAddress();

  function expensiveZero(address toCheck) public returns(bool success) {
     if(toCheck == address(0)) revert ZeroAddress();
     return true;
  }

  function cheapZero(address toCheck) public returns(bool success) {
    assembly {
       if iszero(toCheck) {
          let ptr := mload(0x40)
          mstore(ptr, 0xd92e233d00000000000000000000000000000000000000000000000000000000)
          revert(ptr, 0x4)
       }
     }
   return true;
}
}

Разница в одном-двух десятке единиц газа... Хммм...

А как вы относитесь к такой переоптимизации? Нужно ли бороться за каждую единицу газа в контракте?

#gas
😁4
Курс по Defi на Youtube

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

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

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

Course I: DeFi Infrastructure

Course II: DeFi Primitives

Course III: DeFi Deep Dive

Course IV: DeFi Risks and Opportunities

Приятного просмотра!

#defi
🔥192👍1
Проблема с approve для USDT

Некоторые продвинутые разработчики знают о проблеме с функцией approve() для некоторых токенов, например, USDT.

Загвоздка заключается в том, что нельзя изменить allowance через approve без его предварительного обнуления. Например, если один пользователь дал разрешение другому на перевод 100 его токенов, то потом вы не сможете обновить это значение до, скажем, 50 токенов. Нужно будет сначала отправить транзакцию с обнулением разрешения, а затем выдать новое на 50 токенов.

Примерно в августе вышло обновление в библиотеке Open Zeppelin, где представили функцию forceApprove(), которая решает эту проблему.

Она выглядит так:

function forceApprove(IERC20 token, address spender, uint256 value) internal {
   bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

    if (!_callOptionalReturnBool(token, approvalCall)) {
        _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
        _callOptionalReturn(token, approvalCall);
    }
}

Интересно, какие проблему аудиторы будут находить с этим обнослением? И будут ли?

#approve #erc20
👏2🤔2
Block Stuffing атака

Смотрел старые ветки в Твиттере и встретил упоминание про атаку Block Stuffing, о которой я еще не писал на канале.

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

Возьмем к примеру сеть Avalanche с токеном AVAX. Gas limit в блоке составляет примерно 15 млн, а стоимость транзакции обычного трансфера ERC20 токена около 0,0075 $. Такая низкая стоимость открывает двери к нашей атаке: чтобы заполнить целый блок в сети потребуется всего лишь 5,5$ (или около того, в зависимости от стоимости газа на тот момент).

Таким образом всего за небольшую сумму хакер может держать нужную ему транзакцию не исполненной продолжительное время. Или же, например, мешать Chainlink data feed получать актуальную информацию по токенам в каком-то конкретном протоколе (data feeds обновляется примерно раз в 10-20 блоков, и ожидание для defi протокола может стоить хороших денег).

Чуть больше об этой атаке можно прочитать тут или тут.

P.S. Я редко видел в аудиторских отчетах упоминание этой атаки, так что может она не такая уж и важная? Или недооценённая...

#blockstuffing #stuff
Работа с процентами

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

Как мы знаем, в Solidity вообще большие проблемы с математикой, в частности, с делением. А для того, чтобы посчитать 10% от какого-либо числа нужна именно эта операция.

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

100 шариков - 100 %
——————- ———-
? шариков - 10 %


100 * 10 / 100 = 10 шариков

вроде как, перемножаем крест-накрест.

В Solidity, в самой популярной реализации, примерно также, с одним уточнением.

Я часто в аудитах видел, что вводится некое понятие BasisPoints и приравнивается к 10 000.

uint256 constant public BASIS_POINTS = 10000;

Также можно встретить названия bps или просто bp.

Вот эти 10000, по своей сути, равны 100,00%. Два нуля после запятой сделаны для работы с более мелкими процентами, типа 1,85%.

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

(tokenAmount * 275) / BASIS_POINTS;

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

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

А вы знаете еще какие-нибудь варианты работы с процентами?

#math #percentage
👍12
Frontrun и sandwitch attacks

Ранее я пропустил выход этих видео от аудитора Owen Thurm на его ютуб канале, но посмотрел на выходных и решил поделиться на канале.

Он рассказывает про две популярные атаки: фронтран и сэндвич, в довольно понятном ключе и с примерами. Опытные пользователи вряд ли найдут тут что-то новое, а вот для новичков будет вполне полезно.

Front-Running Attacks

Sandwich Attacks

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

Приятного просмотра!

#frontrun #sandwith
👍5🔥1
Создание хуков в Uniswap v4

С момента выхода v4 прошло уже некоторое время, но до сих пор я не встречал его активного применения в протоколах или конкурсных аудитах. Большинство все также сидит на v3, или даже на v2.

Тут попалась статья, где подробно рассказывается о том, как работать с хуками в v4: что это, как работает и как подключить к своему проекту.

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

How to ​integrate Uniswap 4 and create custom hooks

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

#uniswap #hooks
👍1
UPD. Мини биржа канала

Добавил еще 6 профессионалов, к которым вы можете обращаться за помощью в написании, тестировании и аудите смарт контрактов. Более того, есть те, что смогут написать для вашего протокола сайт или приложение (ключ: фронтенд), или же дать частный урок по вопросам Solidity!

Приятно, что инициативу с биржей поддерживает все больше людей!

#market
9👍3
Возможные проблемы с EIP712

Аудитор Chinmay написал небольшой пост на Medium, где поделился своей информацией с примерами о проблемах, которые могут возникнуть с подписями стандарта EIP712. 

Chinmay - активный участник конкурсных аудитов на code4rena, sherlock и codehawks, поэтому к его посту стоит как минимум присмотреться.

Вкратце, он описывает следующие проблемные зоны:

1. Использование не правильного адреса контракта в Domain Separator;
2. Данные в подписываемом сообщении должны быть уникальными;
3. Domain Separator должен пересчитываться, когда имя\версия меняются;
4. Риск Signature Replay в случае хардфорка;
5. Не следование спецификации стандарта EIP712;

Более подробно об этом можно прочитать тут: Auditor’s Digest : The risks of EIP712

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

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

#eip712
👍2
Коротко о Keccak256

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

Что такое хеш-функция?

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

Почему keccak256?

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

Keccak256 в Solidity

keccak256 — это функция, встроенная в Solidity. Она принимает любое количество входных данных и преобразует их в уникальный 32-байтовый хеш (32 байта это как раз 256 бит).

pragma solidity 0.8.20;

contract Hash {
function hash(string memory _text, uint256 _number, address _address) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_text, _number, _address));
}
}

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

Полную версию статьи про keccak256 можно почитать здесь.

P.S. Сегодняшний пост написан ребятами из компании MetaLamp, в рамках поста для приглашенных авторов. По ссылке вместе со статьей, вы сможете найти авторский репо от команды с другими полезными материалами по Solidity.

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

#keccak256
🔥15👍2
Баг из протокола Mia

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

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

function _normalizeDecimalsMultiple(uint256[] memory _deposits, address[] memory _tokens) internal view
        returns (uint256[] memory deposits)
{
   for (uint256 i = 0; i < _deposits.length; i++) {
      deposits[i] = _normalizeDecimals(_deposits[i], ERC20(_tokens[i]).decimals());
   }
}


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

Ответ:

Функция не будет работать, так как deposits никогда не были сохранены в памяти.

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

{
  uint len = _deposits.length;      
  deposits = new uint256[](len);
  for (uint256 i = 0; i < len; i++) {
     deposits[i] = _normalizeDecimals(_deposits[i], ERC20(_tokens[i]).decimals());
  }
}

Интересный пример, не так ли?

#bug
👍12
Голоса для канала?

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

Как думаете, нужна ли такая опция для контента? Может делать дневные задачи с ними?

Можно поддержать канал бустом по ссылке: https://news.1rj.ru/str/solidityset?boost

#boost
👍7
Программа 3 модуля курса

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

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

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

Вместе с темами уроков ученики будут тренироваться:

1. Работе в редакторе кода;
1. Работе с GitHub;
2. Написанием комментариев к коду;
3. Изучением контрактов реальных проектов;
4. Решении задач и базовые понимания безопасности;
5. Плюс два финальных проекта, которые можно будет показать в резюме;

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


Итак, программа третьего модуля:

1. Подготовка и настройка IDE
1.1. Плагины для работы
2. Терминал, Node js, NPM
3. Начало работы с Git и GitHub

4. Hardhat. Настройка и запуск
5. Hardhat. Из чего состоит
6. Foundry. Настройка и запуск
7. Foundry. Из чего состоит

9. Голландский аукцион
10. MultiSig и Timelock
11. Commit/reveal
12. DAO и governance

13. Ролевая система
14. ERC4626
15. ERC4907
16. ERC6551


Ориентировочный старт: 2 октября
Стоимость: 3000 рублей, 30 (USDT, USDC)


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

Начало продаж, по традиции, в среду.

#курс
🔥121👍1
Чуть больше о практике модуля

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

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

В этот раз будет по другому.

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

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

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

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

#курс
3