uz
Feedback
Solidity. Смарт контракты и аудит

Solidity. Смарт контракты и аудит

Kanalga Telegram’da o‘tish

Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов

Ko'proq ko'rsatish
2 592
Obunachilar
Ma'lumot yo'q24 soatlar
-17 kunlar
+430 kunlar
Postlar arxiv
Курс по Defi на Youtube Не помню наверняка, но, вроде, несколько видео с данного цикла я уже выкладывал на канале. А на днях я переоткрыл его для себя. Спустя некоторое время, когда я стал разбираться в протоколах и тем, что вообще происходит в контрактах DeFi, лекции из курса стали звучать немного по другому. Сам курс, в 4 частях, на английском языке. Если начнете смотреть хотя бы по одному видео каждый день, то вам хватит до нового года! Course I: DeFi Infrastructure Course II: DeFi Primitives Course III: DeFi Deep Dive Course IV: DeFi Risks and Opportunities Приятного просмотра! #defi

Более дешевый способ проверки на нулевой адрес Встретил в Твиттере код, который якобы призвал сделать более дешевой проверку на нулевой адрес. Забил его в Ремикс, чтобы самому узреть эти гигантскую разницу... Вот сам код: // 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

Повышаем мастерство с Forge coverage Я уже несколько раз писал на канале, что в скором времени компании будут нанимать отдельных разработчиков для написания тестов для своих протоколов. А знания и навыки работы с Hardhat или Foundry будут стоять в описаниях вакансий с Solidity, как это сейчас с JS/React.  Вот и в этой статье рассказывается, что покрытие тестами на 100% не всегда означает, что проверены все возможные функции и линии в контракте. И разработчик должен уметь работать с отчетами покрытия, также как он работает с самими написаниями тестов. Например, вы знали, что с foundry coverage можно запустить дополнительную команду report debug, которая покажет, какие строки и функции вы забыли протестировать? Или, что с помощью forge coverage --report lcov можно сформировать отчет в html разметке? Или, что, используя плагин Coverage Gutters, можно визуально в контракте увидеть код без тестов? В общем, профессиональная команда тестировщиков должна будет выдавать отчет с абсолютным покрытием тестом и отчетом для клиента, где код будет светиться "зеленым" и везде будет 100% в таблице. В статье даже есть репо, с которым вы можете потренироваться! Советую всем и каждому повысить свои навыки в работе с Foundry. #foundry #coverage

Checks-Effects-Interactions устарел? Наткнулся на интересную статью, где автор рассуждает над паттерном CEI и о том, что сейчас самое время сделать его чуть более безопасным. В статье рассказывается о новом подходе к написанию безопасных функций, который включает в себя проверку инвариантов и называется FREI-PI (Function Requirements-Effects-Interactions + Protocol Invariants). На самом деле, некоторые аудиторы уже могли встречать его, просматривая контракты таких протоколов, как dYdX, Compound или Aave. В общем, почитать подробнее можно тут. На мой взгляд, это вполне может вскоре стать отдельным пунктом для аудиторского отчета. #freypi #cie

Баг в протоколе Lybra Интересная находка была в конкурсном аудите протокола Lybra, в наборе которого были прокси контракты. При всей очевидности проблемы, найти ее смог только один аудитор. Почему очевидной? Просто потому, что этому обучают всех с момента знакомства с прокси контрактами. Для протокола был создан отдельный контракт конфигуратор, который так и называется LybraConfigurator. По своей сути, это был Логический контракт для прокси контракта. Проблема заключалась в том, что тут был конструктор, с помощью которого устанавливались переменные в память, что и было ошибкой. Как мы знаем, конструктор исполняется только один раз, во время деплоя контракта, и позволяет сделать изначальную установку переменных в контракт. Но запись идет в память данного контракта, а не в память прокси, как это положено в данном случае. Поэтому на момент? когда были бы развернуты контракты в сети, то две переменные в прокси остались бы с нулевым значением. Вот такой очевидный и не очень баг. Более детально о нем можно прочитать тут, в отчете code4rena. Будьте внимательны при работе с прокси! #proxy #constructor

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

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

Плагин для VSCode Некоторое время назад подбирал для себя простое решения для того, чтобы отмечать файлы и контракты, аудит которых уже провел. Многие из плагинов были слегка накрученные и с множеством дополнительных функций, что порой, скорее, мешало работе, чем помогало ей. Вот нашел для себя простой плагин Mark files: После установки, кликаете правой кнопкой мыши на древе файлов и в появившемся меню выбираете "Mark \ unmark selected file". Радом с файлом или папкой появится маленький значок. В общем, крайне простой инструмент. #plaggin

Побитовые в 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

Что за адрес 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE? Это специальный адрес, который работает как идентификатор для Эфира, нативной валюты. Подобно адресам токенов ERC20, данный адрес используется для представления Эфира в контрактах, как если он был токеном. Его можно посмотреть на Etherscan тут. #ether

Письменное задание в Spearbit Для тех, кто не в курсе, Spearbit крутая зарубежная компания, которая занимается безопасностью смарт контрактов. Пару раз в год они набирают к себе аудиторов. Для этого требуется пройти тест из 4 вариантов ответа на время и после технического интервью еще выполнить письменное задание. Предлагаю вашему вниманию одно из таких заданий от февраля 2022 года. Прекрасная практика для аудиторов и тех, кто любит решать задачи. Есть два контракта: прокси и Логики. Деплой логики делается только однажды для всех пользователей. Прокси - для каждого свой. Пользователи держат все свои активы на прокси контракте и, в случае необходимости, посылают вызовы на контракт Логики. Тут есть критическая уязвимость. Задание: найти ее и дать свои рекомендации по ее устранению. Вы получите дополнительный бонус, если в рекомендациях расскажите о нишевом решении для предотвращения уязвимости. Еще один бонус получите, если в рекомендациях расскажите, как можно убрать уязвимость, убрав два слова в контракте, и изменив всего одно. Вот ссылка на репо задания: https://github.com/spearbit-audits/writing-exercise Удачи в поисках! #proxy #bug #spearbit

Теперь же происходит переполнение и функция откатывается. Это можно легко проверить и в 0.8 версии поместив вычитание в unchecked:
unchecked {
  balances[msg.sender] -= _amount;
} 

Реентранси снова пройдет! В общем, такое получилось небольшое расследование. Если все так же не очень понятен смысл поста, то рекомендую сначала посмотреть видео про рекурсию, а потом еще раз почитать про реентранси. P.S. Если захотите увидеть, как все происходит внутри, добавьте этот тест в Foundry:
contract TestReentrancy is Test {
    using stdStorage for StdStorage;
    Wallet public wallet;
    AttacWallet public attacWallet;

    function setUp() public{
        wallet = new Wallet();
        attacWallet = new AttacWallet(payable(wallet));
        vm.deal(address(wallet), 10 ether);
        vm.deal(address(attacWallet), 2 ether);
    }

    function test_testSetup() public {  
        attacWallet.depositAttack();
        attacWallet.attack();
        console.log(address(attacWallet).balance);
    }

} 

И запустите команду в терминале: forge test --match-contract TestReentrancy -vvvv Надеюсь, некоторым, как и мне вчера, стало чуть более понятна эта атака. #reentrancy

Реентранси, рекурсия и ресмысление... Каждый, кто начинает свой путь в аудите и безопасности смарт контрактов, одной из первых атак изучает Реентранси, грубо говоря, повторное вхождение в одну функцию несколько раз. Я уже не раз писал на канале о этом, находил сам в задачах и аудитах, но что-то никогда не задумывался, как она работает "под капотом". И вот вчера, меня немного поставили в ступор простым кодом:
contract Wallet {
  
  mapping(address => uint) public balances;

  function deposit(address _to) public payable {
    balances[_to] = balances[_to] + msg.value;
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      require(result, "External call returned false");
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

contract AttacWallet {
  uint constant SUM = 1 ether;
  Wallet walletRe;

  constructor(address payable _wallet) {
    walletRe = Wallet(_wallet);
  }

  function depositAttack() public payable {
    walletRe.deposit{value: SUM}(address(this));
  }

  function attack() external payable {
    walletRe.withdraw(SUM);
  }

  receive() external payable {
    if(address(walletRe).balance >= SUM) {
      walletRe.withdraw(SUM);
    } 
  }
} 

Есть два контракта: Wallet и AttacWalet, второй предназначен для взлома первого. Вопрос был простой: "Почему не работает реентранси?". И код написан правильно. Но вот, почему не проходит... Я пошел сравнивать этот код с кодом примеров данной атаки, которые были в разных статьях в поиске гугл. Достаточно большое количество ресурсов, в том числе solidity-by-example, показывали пример атаки, где в итоге баланс пользователя обнуляется, а не минусуется, как в данном примере. Другими словами, вместо: balances[msg.sender] -= _amount; было balances[msg.sender] = 0; И вот если в данном контракте также обнулять контракт, то атака работает! Мне потребовалось некоторое время и помощь коллег, чтобы разобраться с этим. Скажу сразу, я не правильно понимал ход выполнения атаки реентранси. До сего дня я полагал, что функция будет заходить в withdraw(), доходить до call вызова, перебрасываться в контракт хакера в receive(), а оттуда снова в withdraw() и так по кругу, пока не будет выполнено условие if(address(walletRe).balance >= SUM) {}, после чего call наконец пройдет, и исполнение функции дойдет до balances[msg.sender] -= _amount. Короче, это не так. На самом деле функция заходит в некую рекурсию, т.е. выполняется withdraw() полностью, но в своем программном порядке. Лучше всего о рекурсии можно узнать из этого видео, спасибо @elawbek за наводку! Смотрите, что получается. Допустим у нас есть 10 Эфиров на контракте Wallet, которые мы хотим увести. На контракте хакера 1 Эфир для атаки. Сначала мы делаем депозит и переводим 1 Эфир с контракта хакера, на контракт Wallet, и так теперь 11 Эфира. После чего мы запускаем функцию attack(). Мы попадаем в withdraw(), происходит рекурсия, которая под капотом исполняет данную функцию 11 раз, так как всего там 11 Эфира лежит. И когда рекурсия начинает "сворачиваться", то оказывается, что успешная пересылка Эфира прошла всего один раз, и тогда уже сработало balances[msg.sender] -= _amount. В каждый последующий раз мы вычитали 1 Эфир из balances[msg.sender], на котором уже был ноль. Происходило переполнение и call вызов возвращал "External call returned false". Именно поэтому атака реентранси тут и стопорилась! Но почему в некоторых статьях также можно встретить описание атаки, где присутствует balances[msg.sender] -= _amount? Как, например, тут. Объяснение просто: все дело в pragma и версии Solidity. До 0.8 версии в математических расчетах не было проверки на overflow. Система не выдавала ошибку, когда программа пыталась вычесть какое-либо число из 0. Кстати, задача Ethernaut именно на этом и построена!

Активное выдалось лето Я тут вчера рефлексировал по прошедшему лету и удивлялся, что оно так быстро прошло. В последние дни все чаще появлялось чувство усталости (может и из-за сезонной аллергии) и ощущение, что я немного торможу свое собственное обучение. Потом я решил посмотреть, а чем же я занимался все лето, и вот, что получилось: 1. За три месяца я поучаствовал в 7 конкурсных аудитах на Code4rena; 2. Заходил на 3 конкурса на новой платформе CodeHawks; 3. Запустил и написал материалы для 2 модулей своего курса для начинающих разработчиков; 4. Пошел отбор и стажировался в крутой зарубежной компании, занимающейся безопасностью и аудитом; 5. На канале стало 1000+ участников! Начинается новый сезон, и я думаю, взять небольшой отпуск от конкурсных аудитов. Они занимаю очень много времени. Хочу пересмотреть и изучить материалы, которые все это время сохранял к себе в закладки, провести 3 модуль курса и подготовить почву для дальнейших проектов. Это так, просто делюсь с вами текущими событиями. P.S. Все также еще актуален отклик на тестировщика Foundry!

Пишешь тесты на Foundry 2 Пока выдалась свободная минута, хочу продолжить мысль про последние посты. Из опроса видно, что 15 участников чата уверены в своих силах для написания тестов на Foundry. Честно говоря, я думал, что будет куда меньше! Зачем я проводил опрос? При общении с зарубежными разработчиками, я понял, что есть некоторый интерес к коммерческому написанию тестов для смарт контрактов, без разработки самих протоколов. Другими словами, вам дают контракт и вы пишите тесты для них. И я планирую вывести эту услугу осенью-зимой, вкупе с парой других сопутствующих. Нужны 3-4 человека, которым интересная эта тема и которые будут готовы к такой работе. Если хотите поучаствовать, то напишите мне в личку @zaevlad. Желательно приложить ссылку на свой репо, где уже есть написанные тесты. На данный момент точно по срокам не скажу, разбираю свои текущие задачи. Сейчас просто собираю желающих и проверяю интерес. Всем спасибо за пройденный опрос! Для тех, кто не знает Foundry, могу найти хорошего преподавателя, который даст уроки. Но это уже в конце года, сейчас еще идет текущий курс. #foundry

Пишешь тесты на Foundry
Anonymous voting

Пишешь тесты на Foundry? Еще один вопрос на сегодня: есть ли среди участников канала те, кто хорошо умеет работать с Foundry и писать тесты? Важно умение писать тесты для токенов, NFT, Vaults, связки с uniswap и другими defi, варианты с флешзаймами и т.д. Т.е. прям хорошо владеющие этим навыком! Хочу попробовать один проект запустить осенью или зимой на зарубежный рынок. Ниже будет опрос, кликните, кто на скиле)

Есть ли авторы? Слушайте, сейчас у меня пошел какой-то нереальный загруз в работе, что я не успеваю подготавливать хороший материал по темам языка и аудита для канала. Я подумал, может кто-то из вас хотел бы попробовать написать несколько постов для канала? Может у вас есть какие-то любимые темы, которыми вы хотели бы поделиться, и которых еще не было на канале? Также хотелось бы видеть авторские посты, а не копипаста с других каналов или статей. Посты с пруфами, ссылками и репо. Есть желающие?

1000+ участников! С таким активным летом на события в жизни, совсем пропустил, что канал перевалил за 1000 участников! Потихоньку мы идем к цели и становимся самым большим каналом в русскоязычном сегменте, который посвящен Solidity и аудиту! 800+ информационных постов, 350+ ссылок на различные ресурсы, 2 активных обучающих модуля и 3 в плане, а также куча разных подборок - все это и делает наше сообщество одним из самых крутых! С нового учебного года мы продолжим наш путь в тонкости языка и проблем безопасности современных смарт контрактов, будем говорить и про различные тулзы, которые стали появляться чуть ли не каждую неделю, и про опкоды и yul, и, возможно, попробуем залезть в ноду! Ожидается много интересного! А пока, гуляем последние дни лета, готовимся к новому сезону и копим силы! Поздравляю всех с 1К!

Еще один пример работы с Error В протоколе Dopex встретил хороший пример работы с ошибками (require / error) в контракте. Была создана специальная функция для валидации входящих условий: function _validate(bool _clause, uint256 _errorCode) internal pure { if (!_clause) revert RdpxV2CoreError(_errorCode); } которая проверяет условие в родительской функции, и если не true, то порождает Error с нужным кодом. Сам Error выглядит максимально просто: error RdpxV2CoreError(uint256); ну, и в коде, в комментариях, есть список ошибок по номеру, например: // ERROR CODES // E1: "Insufficient bond amount", // E2: "Bond has expired", // E3: "Invalid parameters" Как это работает? Например, у нас есть функция, которая должна принимать адрес в качестве аргумента: function getAddress(address addr) external {...} и нам нужно проверить, чтобы этот адрес не был нулевым, поэтому мы передаем условие в функцию _validate(): function getAddress(address addr) external { _valudate(addr != address(0), 4); } Идет проверка, если адрес окажется нулевым, по код выдаст ошибку под номером 4, из чего мы узнаем о проваленной проверке. Вот такое простое решение. #error

Solidity. Смарт контракты и аудит - Telegram kanali @solidityset statistikasi va tahlili