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

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

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

В проекте Sparkn по-другому.

P.S. Репо, возможно, будет открыто только на время конкурса.

Здесь три контракта: proxy, distributor и proxyFactory.

Proxy - самая простая реализация прокси паттерна с одной лишь fallback функцией.

Distributor - контракт Логики для прокси с разными функциями, основная из которых это перечисление средств пользователям.

ProxyFactory - основной контракт для взаимодействия.

Смотрите, что получается. Контракты proxyFactory и distributor постоянные. Т.е. distributor хоть и является контрактом Логики для прокси, но в данном случае он не обновляемый, так как функционал для этого не заложен.

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

Итак, суть в том, что пользователь может заранее вычислить свой адрес прокси и отправить туда некоторую сумму для распределения пользователям. Затем он вызывает функцию деплоя proxy, сразу после чего идет туда вызов и с помощью delegatecall вызывается функция из distributor, которая делает рассылку активов для установленных пользователей.

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

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

На мой взгляд сделано очень круто!

#proxy
👍5
Еще один пример работы с 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
👍5
Снимоку.PNG
2.6 KB
1000+ участников!

С таким активным летом на события в жизни, совсем пропустил, что канал перевалил за 1000 участников! Потихоньку мы идем к цели и становимся самым большим каналом в русскоязычном сегменте, который посвящен Solidity и аудиту!

800+ информационных постов, 350+ ссылок на различные ресурсы, 2 активных обучающих модуля и 3 в плане, а также куча разных подборок - все это и делает наше сообщество одним из самых крутых!

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

Ожидается много интересного!

А пока, гуляем последние дни лета, готовимся к новому сезону и копим силы!

Поздравляю всех с 1К!
🎉36🔥3
Есть ли авторы?

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

Я подумал, может кто-то из вас хотел бы попробовать написать несколько постов для канала? Может у вас есть какие-то любимые темы, которыми вы хотели бы поделиться, и которых еще не было на канале?

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

Есть желающие?
Пишешь тесты на 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