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

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

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

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

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

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

В общем, тесты - тесты - тесты. Вот, что самое сложное в работе аудитора.

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

Начинаем штудировать тесты в foundry.
👍3
Тесты с Foundry

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

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

Что мне мешало с тестами?

Во-первых, не хватало какого-то визуала для отслеживания результата. Например, в Ремиксе очень удобная система с кнопками-функциями и консоли, где отображаются транзакции. Нажал кнопку и смотришь, как все прошло. Есть проблема? Открываешь дебаггер. Да и баланс сразу можно проверить.

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

Во-вторых, typenoscript, typechain, deploy - все это потрясающие вещи для работы. И вполне можно было подготавливать файл-скелет для каждого проекта и его теста, но что-то не то для меня.

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

Почему я решил перейти на Foundry?

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

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

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

Далее опишу, к чему я пока пришел за день практики.

#foundry
👍2
Тесты с Foundry. Часть 2

Прежде всего тесты пишутся на Solidity. Если вы только начали учить блокчейн разработку, то вам не придется дополнительно изучать JavaScript, typenoscript и node, а это еще 3-6 месяцев дополнительного времени обучения минимум.

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

В lib хранятся все подключенные библиотеки (например, openzeppelin), в noscript можно будет найти json файл с abi контракта, в src пишутся контракты проекта, а в test - соответственно тесты.

Файлы для тестов следует называть по определённому шаблону. Например, контракт у вас под именем Day.sol, следовательно контракт для теста будет - Day.t.sol, т.е. название проекта, потом буква t, потом sol.

Открыв файл теста, мы делаем практически все также как и при написании обычного контракта. Пишем SPDX-License, pragma, импортируем библиотеку для тестов:

import "forge-std/Test.sol";

и наши контракты

import {Day} from "../src/Day.sol";   

Далее нужно назвать наш контракт, как оригинальный и добавить Test, например DayTest, подключив наследование от контракта Test.sol, например:

contract DayTest is Test {}

Далее начинается самое интересное с тестами.

#foundry
👍2
Тесты с Foundry. Часть 3

Я также рекомендовал бы сразу подключить еще один служебный контракт в файл с тестами, а именно:

import "forge-std/console.sol";

Он позволит в тестах вызывать console.log() и получать нужные данные в терминале. Очень удобно, особенно, если вы хотите посмотреть какую-нибудь информацию об адресе или балансе.

Далее создаем переменные нашего контракта для теста:

Day public day;

По сути, благодаря Foundry нам не нужно прописывать getContractFactory, deploy, deployed, loadFixture, it тесты и много чего еще.

Идем дальше.

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

function setUp() public {}

Именно в нее мы можем записать создание контрактов и передачу в их конструкторы какие-либо параметры. Например:

function setUp() public {
   day = new Day(constructor params);
}

Теперь в остальных функциях тестов мы можем вызывать функции в контракте, как и обычно: day.increment().

Также существуют специальные Foundry Cheat Codes, которые позволяют выполнять специальные действия, в том числе эмулировать вызовы функций от имени других пользователей или контрактов, а также управлять block.timestamp, block.number, difficulty и остальными.

При этом не нужно писать длинные строки с await, достаточно в функции вызвать, например, vm.roll(100) и номер блока сразу изменится.

Также можно читать переменные, которые определяются в начале контракта, такие как owner. Для этого можно сделать так:

console.log(day.owner());

С помощью дополнительной библиотеки StdStorage можно также читать значения из слотов памяти.

Для того чтобы подключиться к вызову функции от другого аккаунта или контракта, как мы делали в hh connect(hacker), тут мы можем эмулировать действие, вызвав служебную функцию vm.prank(hacker). Дальше вызовы будут от данного адреса.

С адресами также все просто. Не обязательно запускать узел или использовать предустановленные адреса, можно также создать адрес для теста:

adderess alisa = address(0x01);

и дальше действовать уже от имени Алисы.

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

А в следующем посте просто расскажу про команды для тестов в терминале.

#foundry
👍6
Тесты с Foundry. Часть 4

Команды в тестах также предельно просты:

forge build - собирает проект и создает abi контаркта;
forge test - проводит тесты контрактов из папки test;
forget test -v (-vv, -vvv, -vvvv) - дает больше информации по проведенным тестам;
forge test -vv -m funcName - проведение теста конкретной функции;
forge test --gas-report - отчет по газу;
forge coverage - покрытие тестами;

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

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

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

#foundry
👍5
Тесты с Foundry. Часть 5

Еще одна классная штука, которая есть в Foundry и нет в hardhat или remix, это проведение fuzz тестов.

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

Ранее для подобных тестов использовались сторонние сервисы, типа Echidna от Trail of Bits. Это, конечно, очень крутая программа, но если вам нужно сделать простые тесты, то Foundry подойдет, как нельзя кстати.

Возьмем к примеру такую функцию:

function testWithdraw() public {
payable(address(safe)).transfer(1 ether);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}

Если мы переводим 1-2 Эфира, то все ок. А если кто-то захочет сломать функцию и перевести другие суммы?

И вот как она будет выглядеть в fuzz тестах:

function testWithdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}

Т.е. мы разрешаем Foundry вставлять в параметры функции свои значения под uint amount и проверять исполнение.

Если прогнать этот тест, то мы увидим, что он провалится, так как на одном из шагов будет предложено значение 2**96 wei, что вызовет overflow.

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

Также в тестах можно определять границы подставляемых значений с помощью команды vm.assume(), например, для нашего случая:

vm.assume(amount > 0.1 ether);

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

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

fuzz_runs = 10000 (количество тестов)

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

В общем, Foundry сильно облегчает нам работу!

#foundry
👍3
Тесты с Foundry. Часть 6

В видео у Ильи мы делали тесты конечных результатов выполнения функции с помощью команды assertEq(), которая проверяет равны ли введённые значения.

В Foundry есть и другие команды для проверки условий:

assertGt() - проверка, если "а" больше "б";
assertGe() - проверка, если "а" больше или равно "б";
assertLt() - проверка, если "а" меньше "б";
assertLe() - проверка, если "а" меньше или равно "б";
assertTrue() - проверка, если значение возвращает true;

Также возможно и такие проверки:

assertEqDecimal() - проверка равности значений decimals, например:

uint256 a = 1 ether;
uint256 b = 1e18 wei;
assertEqDecimal(a, b, 18);

assertEq32() - равны ли значения bytes32;
assertEq0() - равны ли значения bytes;

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

В общем, с помощью cast команд в локальной сети можно просматривать транзакции, подписывать ее, рассчитывать газ, получить код деплоя или информацию о блоке, работать с адресом и abi контракта, а также получать данные из etherscan (при наличии API ключа) и много чего еще.

Вот, чем больше я изучаю Foundry, тем больше он мне нравится. И что самое интересное, это довольно таки новый проект. Есть большая вероятность, что с дальнейшим развитием, он станет еще круче!

#foundry
👍3
Защита от атак Signature Replay

Несколько раз в задачах встречал уязвимости с подписями пользователей транзакций, что приводило к Front run и Signature Replay атакам. Мне захотелось чуть больше узнать о них. И для начала небольшой пример кода:

function unlock(
  address _to,
  uint256 _amount,
  uint8[] _v,
  bytes32[] _r,
  bytes32[] _s
)
  external
{
  require(_v.length >= 5);
  bytes32 hashData = keccak256(_to, _amount);
  for (uint i = 0; i < _v.length; i++) {
    address recAddr = ecrecover(hashData, _v[i], _r[i], _s[i]);
    require(_isValidator(recAddr));
  }
  to.transfer(_amount);
}

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

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

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

Самой простой защитой было бы добавить nonce в аргументы функции (индивидуальный номер) и проверку типа require(_nonce == nonce++). 

Как же получше защититься от них?

Вот три самых популярных решения:

1. Сохранять message hash в контракте, и позже проверять его на предмет повтора;

2. Включать в message hash адрес контракта, чтобы удостовериться, что сообщение используется только в данном контрактом;

3. Никогда не генерировать message hash вместе с подписью пользователя.

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

#signaturereplay #signature #replay #security
👍2
Новые уязвимости? Часть 1. Фантомные функции

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

Фантомные функции

Представьте, что разработчик делает вызов случайной функции в НЕзадеплоеный контракт. Интуитивно, он ожидает, что транзакция будет откатана (revert), но это не так.

Объяснить это можно особенностью работы EVM: вся работа байткода контракта заканчивается специальным опкодом Stop. Именно он говорит EVM вернуть результат (return) без каких-либо ошибок.

Ситуация меняется с задеплоенными контрактами, когда разработчик вызывает несуществующую функцию и EVM делает revert.

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

Вот тут и может таиться уязвимость. Разработчик ожидает, что:

1) Транзакция откатится, если такой функции в контракте не существует;
2) А если такая функция существует, то транзакция может откатиться, если что-то пойдет не так;

Но, что если вызываемой функции в контракте нет, но есть fallback функция, которая принимает любые параметры и никогда не откатывается? Посмотрите на код ниже:

function depositWithPermit(...) {
IERC20(token).permit(receiver, address(this), value, deadline, v, r, s);
IERC20(token).transferFrom(receiver, address(this), value);
}

Это реальный код одного из проектов. Разработчики ожидали, что transferFrom() может быть вызван только после того как пользователь пройдет permit(), в другом случае должен был произойти откат. Однако некоторые токены, типа WETH, не имеют функции permit(), но есть fallback(), которая никогда не делает revert.

В этом случае хакер может пройти permit() и опустошить контракт, заранее сделав approve() на себя.

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

#security #phantom
2
Новые уязвимости? Часть 2. Double-Entry Point Tokens

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

Эта уязвимость была обнаружена в контрактах Compound с токеном TrueUSD.

У них был некий Legacy Contract, который пользователи могли использовать как и контракт токена TUSD, и все, что он делал, это просто перенаправлял действия в TUSD.

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

При этом, если пользователь пополнял TUSD, то мог  влиять на цену токена в пуле token\cToken, что приводило к другой атаке - манипуляция оракулом.

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

#security #phantom
Новые уязвимости? Часть 3. Новые наивные токены

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

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

Также было и с токеном XDAI, который после обновления, добавил новую функцию-хук callAfterTransfer(). Именно ее копировальщики и не восприняли всерьез.

Без определённых действий защиты для этой функции, которые предотвращали reentrancy в XDAI, остальные контракты оказались уязвимыми для хакеров.

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

#security #phantom
Новые уязвимости? Часть 4. Атаки NFT Flashloan

Эта уязвимость была обнаружена при AirDrop токенов для проекта BAYC. Владелец NFT мог вызвать функцию claimTokens() и получить взамен токены Apes равные количеству NFT.

Другие проекты, типа NFTX пытаются привнести механизмы DeFi в мир NFT. Другими словами, они блокируют на контракте ваш NFT и дают взамен токены.

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

Получается, что сторонний пользователь может взять займ этого NFT, зайти на проект BYAC и получить бесплатный AirDrop.

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

#security #phantom
👍1
Новые уязвимости? Часть 5. Read-Only Reentrancy

Еще одна интересная атака. Попробую объяснить ее просто.

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

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

Так вот, несмотря на то, что в external функции ставят защиту, view функции порой обходят стороной. А зря.

Теперь небольшой пример.

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

Далее есть view функция get_virtual_price(), на которую полагаются другие протоколы, и которая показывает цену токена.

Хакер, в этих протоколах, мог вызывать view функцию через fallback(), в тот момент когда шла рассылка токенов в remove_liquidity(), и проводить свои манипуляции не до конца измененной ценой данного токена. 

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

Проводя аудит контракта, теперь нам нужно следить и за view функциями, особенно, когда они на полагаются на баланс или цену токенов из стороннего контракта или биржи в целом.      

#security #phantom
👍1
Пример работы Frontrun ботов

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

Рекомендую к просмотру каждому.

#frontrun #security
👍2
Код - ловушка

Посмотрите внимательно на код на скрине. Если вы повстречаете его где-либо, то, скорее всего, перед вами типичная honeypot ловушка.

Помните, что address(this).balance всегда включает в себя msg.value данной транзакции.

#security
👍4
Задача на день

Эта задача ранее была опубликована Immunefi, и прекрасно подтверждает один недавний пост на канале. Сможете понять в чем тут дело?

Подсказка

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

#task
👍1
Интересный нюанс с call вызовом

В Твиттере у одного из аудиторов увидел пост о том, что call вызов может привести к атаке gas griefing через байты, которые он возвращает в качестве ответа. Смотрите, что получается:

(bool success,) = payable(receiver).call{gas: 3000, value: amount}(hex"");

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

(bool success, bytes memory data)

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

При этому bytes data, которая возвращается из return, будет скопирована в память. И, если размер data будет слишком большой, то стоимость записи в память может превысить лимиты и получится gas griefing.

Аудитор предлагает использовать assembly, который автоматически не копирует возвращаемые значения в память, например:

bool success;
assembly {
success := call(3000, receiver, amount, 0, 0, 0)
}

Не могу точно сказать, что это 100% уязвимость и нужно переходить на assembly всем, так как доказательств в документации я не нашел.

Тем не менее, данный вопрос поднимался не раз на GitHub, вот один из таких примеров.

С обучением разработки смарт контрактов, как в той пословице: "Чем глубже в лес, тем больше дров". Всегда найдется какое-нибудь "но", на которое потом будешь обращать внимание.

#assembly #call
👍2
Блог с разбором взломов

Вчера на канале у Officer CIA проскочило короткое сообщение, в котором он рекомендовал блоги:

faith2dxy.xyz и faraz.faith

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

Автор делает разборы взломов с примерами кода и объяснением хода атак. Все достаточно подробно описано. Прекрасное чтиво на праздники.

#secirity #blog
👍4
Домашнее задание на каникулы

Новый год приближается и в повседневности наступает больше суеты. Сейчас у меня не получается сесть за обучение на несколько часов и концентрироваться на нем. Поэтому я решил взять небольшой отпуск с 28 декабря по 8 января.

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

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

Сборник найденных уязвимостей 1

Сборник найденных уязвимостей 2

Сборник найденных уязвимостей 3

Аудиты Code4rena

Аудиты Immunefi

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

С 9 января мы продолжим наше обучение, вернемся к аудитам и ведению канала.

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

#homework
👍3
Делимся ссылками

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

Вы также можете в комментариях поделиться своими подписками и рекомендациями, и не только в Твиттере.

Faith

patrickd

zzykxx

Trust

okkothejawa

pashov

afeli.eth

WINTΞR

philogy

Liam | Sydney

WATCHPUG

0xrudra

Samrat Gupta

rmi7.eth

Christoph Michel

Officer's Notes

alpharush

this is now a shitpost account


Компании

Chainlink

Trail of Bits

OpenZeppelin

Uniswap Labs

Hardhat

Code4rena

Alchemy | The web3 developer platform

Developer DAO

ConsenSys

Etherscan

Immunefi

rekt

Paradigm CTF

QuillAudits

SHERLOCK

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

Можете спокойно сохранять себе и делать репосты.

#twitter
🔥7👍4