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

Еще один вопрос на сегодня: есть ли среди участников канала те, кто хорошо умеет работать с Foundry и писать тесты?

Важно умение писать тесты для токенов, NFT, Vaults, связки с uniswap и другими defi, варианты с флешзаймами и т.д. Т.е. прям хорошо владеющие этим навыком!

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

Ниже будет опрос, кликните, кто на скиле)
👍6
Пишешь тесты на Foundry 2

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

Из опроса видно, что 15 участников чата уверены в своих силах для написания тестов на Foundry. Честно говоря, я думал, что будет куда меньше!

Зачем я проводил опрос?

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

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

Нужны 3-4 человека, которым интересная эта тема и которые будут готовы к такой работе.

Если хотите поучаствовать, то напишите мне в личку @zaevlad. Желательно приложить ссылку на свой репо, где уже есть написанные тесты.

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

Всем спасибо за пройденный опрос!

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

#foundry
🔥7
Активное выдалось лето

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

Потом я решил посмотреть, а чем же я занимался все лето, и вот, что получилось:

1. За три месяца я поучаствовал в 7 конкурсных аудитах на Code4rena;

2. Заходил на 3 конкурса на новой платформе CodeHawks;

3. Запустил и написал материалы для 2 модулей своего курса для начинающих разработчиков;

4. Пошел отбор и стажировался в крутой зарубежной компании, занимающейся безопасностью и аудитом;

5. На канале стало 1000+ участников!

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

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

Это так, просто делюсь с вами текущими событиями.

P.S. Все также еще актуален отклик на тестировщика Foundry!
🔥24👍5
Реентранси, рекурсия и ресмысление...

Каждый, кто начинает свой путь в аудите и безопасности смарт контрактов, одной из первых атак изучает Реентранси, грубо говоря, повторное вхождение в одну функцию несколько раз.
Я уже не раз писал на канале о этом, находил сам в задачах и аудитах, но что-то никогда не задумывался, как она работает "под капотом".

И вот вчера, меня немного поставили в ступор простым кодом:

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 именно на этом и построена!
👍92👏2😁1
Теперь же происходит переполнение и функция откатывается. Это можно легко проверить и в 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
👍13
Письменное задание в Spearbit

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

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

Есть два контракта: прокси и Логики. Деплой логики делается только однажды для всех пользователей. Прокси - для каждого свой.

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

Тут есть критическая уязвимость. Задание: найти ее и дать свои рекомендации по ее устранению.

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

Вот ссылка на репо задания: https://github.com/spearbit-audits/writing-exercise

Удачи в поисках!

#proxy #bug #spearbit
👍111🔥1
Что за адрес 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE?

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

Его можно посмотреть на Etherscan тут.

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