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

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

В этой статье более подробно рассматриваются три последние версии компилятора Solidity: 0.4.24, 0.6.12 и 0.8.9. Мы исследуем некоторые из ошибок, которые были обнаружены и исправлены в каждой из этих версий, а также добавленные функции.

Чтобы проиллюстрировать эти концепции в реальном контексте, мы исследуем смарт-контракты Lido Finance, на которые повлияли ошибки, появившиеся в этих трех версиях компилятора Solidity. Анализируя эти тематические исследования, мы надеемся предоставить ценную информацию о том, как разработчики Solidity могут избежать потенциальных проблем и обеспечить стабильность и безопасность своих смарт-контрактов.

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

Помимо чтения примечаний к выпуску, вы также можете выполнить поиск по проблемам Solidity на GitHub, чтобы узнать, сообщалось ли о каких-либо дополнительных ошибках, но они еще не устранены. Кроме того, инструменты тестирования и аудита программного обеспечения, такие как Slither или Echidna, могут помочь выявить потенциальные ошибки в вашем коде при работе с конкретной версией компилятора Solidity.

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

Функции

Существует множество различных языковых «фич», непонятных разработчикам, и иногда эти проблемы могут приводить к различным ошибкам и эксплойтам. К сожалению, на все проблемы нет должной документации, и узнать о них можно только на практике. Например, такие проблемы, как целочисленное переполнение, могут быть связаны со спецификой Solidity в версиях компилятора до выпуска 0.8.0, однако, поскольку это общеизвестный факт, в этой статье он рассматриваться не будет. Вместо этого он фокусируется на других, менее популярных, но все же очень интересных функциях.

1. Try/catch функция.

Рассмотрим следующий код, который вызывает адрес _to получателя NFT.

Этот код выглядит безопасным, и разработчики могут подумать, что транзакция не может вернуться в блоке try/catch (спойлер: try/catch не ловит много случаев).

  • Try/catch возвращает правильное значение внешнего адреса.
  • Try/catch правильно улавливает возврат во внешнем методе onERC721Received.

Но…

  • Try/catch вылетает при вызове несуществующего адреса (например, address(0)) (в этом примере он защищен _to.isContract).
  • Try/catch вылетает, если в целевом контракте нет этого метода.
  • Try/catch вылетает, если целевой контракт возвращает неправильное количество аргументов.

Пример:

2. Наследование интерфейса нарушает компоновку.

Давайте посмотрим на следующий пример:

Ожидаемый макет в этом фрагменте выглядит следующим образом:

  • V1: p11, p12
  • V2: p11, p12, p21, p22

Но компиляция приведенного выше кода дает следующие схемы хранения:

  • V1: p11, p12
  • V2: p21, p11, p12, p22

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

3. Immutables не может быть инициализирован.

При попытке скомпилировать контракт, в котором constructor не инициализирует переменную состояния immutable, компилятор выводит:

TypeError: Construction control flow ends without initializing all immutable state variables.

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

Пример:

4. Staticcall функция.

staticcall возвращает false, когда вызов переходит к контракту с несуществующим интерфейсом или когда функция возвращается. Когда вызовы идут на адрес EOA — staticcall всегда будет возвращать true.

Пример:

Такое поведение ожидается для call, потому что вы можете call адрес EOA и отправить на него value. Но с staticcall call для адреса EOA не имеет никакого значения. Так что если у вас в контракте есть staticcalls — убедитесь, что вы вызываете контракт, так как переменная success всегда будет true.

5. Permit функция.

Интерфейс call не вернется к несуществующей функции, если call перейдет на контракт со старой версией компилятора (например, 0.4.X).

Например, контракт WETH не имеет функции permit().Но у него есть функция fallback, которая вызывается, когда функция вызывается, но не найдена. Функция fallback WETH — это deposit(), в данном случае она не делает ничего существенного, а только позволяет продолжить выполнение своей вызывающей функции, поскольку она не дает сбоев.

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

6. Скопируйте вместо перезаписи в Structs.

Давайте углубимся в следующий фрагмент:

Проблема в том, что следующий фрагмент:

Struct memory y = x;

на самом деле не создает новый struct в memory, все, что он делает, это создает указатель на struct. Если y изменено, это означает, что x тоже изменено.

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

7. View и pure могут изменять состояние в 0.4.X.

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

Disbursement.sol:102:9: Warning: Function declared as view, but this expression (potentially) modifies the state and thus requires non-payable (the default) or payable.
withdrawnTokens = 0;
^ — — — — — — -^

С версией компилятора 0.5.0 и выше компиляция завершится с ошибкой, а не просто с предупреждением.

ОШИБКИ

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

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

2) После этого откройте список с описанием ошибок компилятора Solidity, найдите ошибку в списке и проверьте информацию о ней.

3) При поиске ошибки в версии компилятора Solidity важно смотреть на условия, в которых возникает ошибка. Например. yulOptimizer: true или ABIEncoderV2: true.

  1. Копирование массивов bytes из памяти или данных вызовов в хранилище может привести к грязным значениям хранилища.

Представлено: 0.0.1.
Исправлено: 0.8.15.
Серьезность: низкая.
Ссылка: https://blog.soliditylang.org/2022/06/15/dirty-bytes -массив-в-хранилище-ошибка/

Копирование массивов bytes из memory или calldata в storage выполняется кусками по 32 байта, даже если длина не кратна 32. Таким образом, дополнительные bytes после конца массива могут быть скопированы из calldata или memory в storage. Эти грязные байты могут затем стать наблюдаемыми после .push() без аргументов в массив bytes в storage, т.е. такой push не приведет к нулевому значению в конце массива, как ожидалось. Эта ошибка влияет только на устаревший конвейер генерации кода, новый конвейер генерации кода через IR не затрагивается.

Пример:

После беглого поиска нашел пустые push в контрактах Лидо при инициализации контракта. Но push делали на array в storage, а не bytes, так что там все хорошо.

2. При использовании с memory байтовыми массивами результат функции abi.decode может зависеть от содержимого memory вне фактического декодируемого байтового массива.

Добавлено: 0.4.16.
Исправлено: 0.8.4.
Условия: ABIEncoderV2: true.
Уязвимость: очень низкая.
Ссылка: https://blog. soliditylang.org/2021/04/21/decoding-from-memory-bug/

Спецификация ABI использует указатели на области данных для всего, что имеет динамический размер. При декодировании данных из memory (вместо calldata) ABI decoder неправильно проверял некоторые из этих указателей. В частности, можно было использовать большие значения для указателей внутри массивов, так что вычисление смещения приводило к необнаруженному переполнению. Это может привести к декодированию этих указателей, нацеленных на области в memory за пределами фактической области. Таким образом, abi.decode могло возвращать разные значения для одного и того же закодированного массива байтов.

Пример:

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

3. Keccak кэширование.

Исправлено: 0.8.3.
Условия: optimizer: true.
Серьезность: средняя.
Ссылка: https://blog.soliditylang.org/2021/03/23/keccak -оптимизатор-ошибка/

Оптимизатор байт-кода неправильно повторно использовал ранее оцененные хэши Keccak-256. Вы вряд ли пострадаете, если не будете вычислять Keccak-256 хэшей в inline assembly.

Оптимизатор байт-кода Solidity имеет шаг, который может вычислять хэши Keccak-256, если содержимое memory известно во время компиляции. На этом шаге также есть механизм для определения того, что два хэша Keccak-256 равны, даже если значения в memory неизвестны во время компиляции. В этом механизме была ошибка, из-за которой Keccak-256 одного и того же содержимого memory, но разных размеров считались равными. В частности, keccak256(mpos1, length1) и keccak256(mpos2, length2) в некоторых случаях считались равными, если length1 и length2 при округлении до ближайшего кратного 32 были одинаковыми, и когда содержимое memory в mpos1 и mpos2 можно было сделать равным. Это может повлиять на вас, если вы вычислите несколько хэшей Keccak-256 одного и того же содержимого, но с разной длиной внутри файла inline assembly. Этой проблемы можно избежать, если в вашем коде используется keccak256 с длиной, не являющейся константой времени компиляции, или если она всегда кратна 32.

Пример:

Этот баг довольно легко находится без тестов, с помощью поиска я нашел библиотеку со старой версией компилятора, которая использовала keccak256 в inline-assembly с пользовательской переменной len. Итак, вот первая ошибка компилятора Solidity в финансовых контрактах Lido:

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

4. Пустая копия массива байтов.

Исправлено: 0.7.4.
Серьезность: средняя.
Ссылка: https://blog.soliditylang.org/2020/10/19/empty-byte-array-copy-bug/

Копирование пустого массива байтов (или строки) из memory или calldata в storage может привести к повреждению данных, если длина целевого массива впоследствии увеличивается без сохранения новых данных.

Подпрограмма, которая копирует массивы байтов из memory или calldata в storage, сохраняет несвязанные данные после исходного массива в слоте storage, если исходный массив пуст. Если впоследствии длину массива storage увеличить либо с помощью .push(), либо путем присвоения его атрибуту .length (только до версии 0.6.0), вновь созданные элементы массива байтов не будут инициализированы нулями, а будут содержать несвязанные данные. На вас не повлияет, если вы не присваиваете .length и не используете .push() в байтовых массивах, или используете только .push(<arg>) или вручную инициализируете новые элементы.

Пример:

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

5. Динамическая очистка массива.

Исправлено: 0.7.3.
Серьезность: средняя.
Ссылка: https://blog.soliditylang.org/2020/10/07/solidity-dynamic-array-cleanup-bug/

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

Рассмотрим массив с динамическим размером в storage, базовый тип которого достаточно мал, чтобы несколько значений можно было упаковать в один слот, например uint128[]. Определим его длину как l. Когда этот массив назначается из другого массива с меньшей длиной, скажем, m, слоты между элементами m и l должны быть очищены путем их обнуления. Однако эта очистка не была выполнена должным образом. В частности, после очистки слота, соответствующего m, было очищено только первое упакованное значение. Если размер этого массива изменится на длину, превышающую m, индексы, соответствующие нечистым частям слота, будут содержать исходное значение вместо 0. Изменение размера здесь выполняется путем присвоения массиву length с помощью push() или через inline assembly. На вас это не повлияет, если вы используете только .push(<arg>) или если вы присваиваете значение (даже нулевое) новым элементам после увеличения длины массива.

Пример:

Эта ошибка не была обнаружена ручным поиском, но она все еще может присутствовать в коде.

6. Неявная проверка значения вызова constructor.

Добавлено: 0.4.5.
Исправлено: 0.6.8.
Уровень серьезности: очень низкий.

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

Начиная с Solidity 0.4.5, код создания контракта без явного payable constructor должен содержать проверку callvalue, которая приводит к отмене создания контракта, если передано ненулевое значение. Однако эта проверка отсутствовала, если в контракте вообще не было определено явное constructor, но в контракте была база, которая определяет constructor. В этих случаях можно отправить значение в транзакции создания контракта или использовать inline assembly без возврата, даже если предполагается, что код создания не подлежит оплате.

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

7. Назначение кортежа Компоненты слота с несколькими стеками.

Добавлено: 0.1.6.
Исправлено: 0.6.6.
Уровень серьезности: очень низкий.

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

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

Пример:

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

8. Переполнение при создании массива памяти.

Представлено: 0.2.0.
Исправлено: 0.6.5.
Серьезность: низкая.
Ссылка: https://blog.soliditylang.org/2020/04/06/memory-creation -ошибка-переполнения/

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

Во время создания не выполнялись проверки переполнения во время выполнения для массивов длиной memory. В случаях, когда размер памяти массива указан в байтах, т. е. длина массива, умноженная на 32, больше 2²⁵⁶-1, выделение memory будет переполнено, что может привести к перекрытию областей memory. Длина массива по-прежнему сохраняется правильно, поэтому копирование или повторение такого массива приведет к нехватке газа.

Пример:

Мне удалось найти массив с пользовательской длиной, зависящей от переменной activeNodeOperatorCount, которая равна uint256. Так что теоретически возможен сценарий, когда количество активных операторов вырастет до значения более 2⁶⁴-1 и испортит memory, что является критической точкой безопасности для Лидо, но в то же время вероятность этого сценария очень низкий.

9. Private можно переопределить.

Представлено: 0.3.0
Исправлено: 0.5.17
Уровень серьезности: низкий

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

Хотя private методы базовых контрактов невидимы и не могут быть вызваны напрямую из производного контракта, все же можно объявить функцию с тем же именем и типом и, таким образом, изменить поведение функции базового контракта.

Я не нашел переопределения переменных private в коде.

10. Подписанный массив storage копия.

Введено: 0.4.7
Исправлено: 0.5.10
Серьезность: низкая/средняя
Ссылка https://blog.soliditylang.org/2019/06/25/solidity-storage-array -ошибки/

Назначение массива целых чисел со знаком массиву storage другого типа может привести к повреждению данных в этом массиве.

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

Если вы развернули контракты, которые используют массивы целых чисел со знаком в storage и либо напрямую присваивают

  • Литеральный массив, содержащий хотя бы одно отрицательное значение (x = [-1, -2, -3];)
  • Или существующий массив другого целочисленного типа со знаком

к нему, это приведет к повреждению данных в массиве storage.

Контракты, которые назначают только отдельные элементы массива (например, с x[2] = -1;), не затрагиваются.

Целочисленных массивов storage в коде нет, так что в Lido точно нет этого бага. Но, честно говоря, у меня не получилось воспроизвести баг в Remix.

11. Массив хранения ABI Encoder V2 с многослотовым элементом.

Введено: 0.4.16.
Исправлено: 0.5.10.
Серьезность: низкая.
Условия: ABIEncoderV2: true.
Ссылка: https://blog.soliditylang .org/2019/06/25/solidity-storage-array-bugs/

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

Когда массивы storage, в которых элементы занимают более одного слота storage, напрямую кодируются в вызовах внешних функций или с использованием abi.encode, их элементы считываются перекрывающимся образом, т. е. указатель элемента не перемещается должным образом между операциями чтения. Это не проблема, когда данные storage сначала копируются в переменную memory или если массив storage содержит только типы значений или массивы с динамическим размером.

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

Конкретный эффект, который вы увидите, заключается в том, что данные «сдвинуты» в закодированном массиве: если у вас есть массив типа uint[2][] и он содержит данные [[1, 2], [3, 4], [5, 6]], то он будет закодирован как [[1, 2], [2, 3], [3, 4]], потому что кодировщик перемещается только на один слот между элементов вместо двух.

В Lido Finance ABIEncoderV2 не используется в самых ранних версиях контрактов, поэтому эта ошибка отсутствует в контрактах.

12. Аргументы динамического конструктора Clipped ABI V2.

Представлено: 0.4.16.
Исправлено: 0.5.9.
Условия: ABIEncoderV2: true.
Уязвимость: очень низкая.

constructor контракта, который принимает structs или массивы, содержащие массивы динамического размера, возвращаются или декодируются в недопустимые данные.

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

В Lido Finance ABIEncoderV2 не используется в самых ранних версиях контрактов, поэтому этот баг в контрактах отсутствует.

13. Неинициализированный указатель функции в constructor.

Добавлено: 0.4.5.
Исправлено: 0.4.26.
Уровень серьезности: очень низкий.

Вызов неинициализированных указателей функций internal, созданных в constructor, не всегда возвращается и может привести к неожиданному поведению.

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

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

14. Неправильная подпись event в библиотеках.

Добавлено: 0.3.0.
Исправлено: 0.4.26.
Уровень серьезности: очень низкий.

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

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

Я не нашел в коде использование типов контрактов в событиях.

15. ABIEncoder V2 упаковано storage.

Введено: 0.4.19.
Исправлено: 0.4.26.
Серьезность: низкая.
Условия: ABIEncoderV2: true.
Ссылка: https://blog.soliditylang .org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/

Элементы structs и массивы короче 32 байт неправильно декодируются из хранилища при прямом кодировании (т. е. не через тип memory) с использованием ABIEncoderV2. Это может привести к повреждению самих значений, но также может перезаписать другие части закодированных данных.

В Lido Finance ABIEncoderV2 не используется в самых ранних версиях контрактов, поэтому этого бага там нет.

16. Очистка экспоненты.

Исправлено: 0.4.25.
Серьезность: средняя/высокая.
Ссылка: https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/

Использование оператора ** с показателем типа короче 256 бит может привести к неожиданным значениям.

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

Я не нашел в коде использования экспонент в старых версиях компилятора.

17. Событие struct неверные данные.

Представлено: 0.4.17.
Исправлено: 0.4.25.
Серьезность: очень низкая.
Ссылка: https://blog.soliditylang.org/2018/09/13/solidity- исправление-выпуск/

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

Structs не должны были поддерживаться в качестве параметров событий без нового кодировщика ABI. Тем не менее компилятор принял их, но закодировал их адрес memory вместо фактического значения. Даже с новым ABI encoder, structs нельзя индексировать параметры событий.

Я не нашел использования structs в событиях.

18. Переполнение заголовка перекодирования ABI с очисткой статического массива.

Введено: 0.5.8.
Исправлено: 0.8.16.
Условия ABIEncoderV2: true.
Серьезность: средняя.
Ссылка: https://blog.soliditylang. org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/

ABI-кодирование кортежа с массивом calldata статического размера в последнем компоненте испортит 32 начальных байта его первого динамически закодированного компонента.

Когда массив calldata статического размера закодирован с помощью ABI, компилятор всегда дополняет область данных до кратного 32 байтам и гарантирует, что байты заполнения обнулены. В некоторых случаях эта очистка всегда выполнялась путем записи ровно 32 байтов, независимо от того, сколько нужно было обнулить. Это было сделано в предположении, что данные, которые в конечном итоге займут область за концом массива, еще не были записаны, поскольку кодировщик обрабатывает компоненты кортежа в том порядке, в котором они были заданы. Хотя это предположение в основном верно, существует важный пограничный случай: компоненты кортежа с динамическим кодированием хранятся отдельно от компонентов со статическим размером в области, называемой *хвостом* кодирования, а хвост следует сразу за *головой*, что где размещаются компоненты статических размеров. Вышеупомянутая очистка, если выполнить ее для последнего компонента заголовка, перейдет в хвост и перезапишет до 32 байтов хранящегося там первого компонента нулями. Единственным типом массива, для которого очистка действительно могла привести к перезаписи, были массивы с uint256 или bytes32 в качестве типа базового элемента, и в этом случае размер поврежденной области всегда составлял ровно 32 байта. Проблема затрагивала кортежи любого уровня вложенности. Это также включает structs, которые закодированы как кортежи в ABI. Также обратите внимание, что списки параметров и возвращаемые значения функций, событий и ошибок кодируются как кортежи.

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

Пример:

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

19. Изменение местоположения данных в переопределении internal.

Введено: 0.6.9.
Исправлено: 0.8.14.
Серьезность: очень низкая.
Ссылка: https://blog.soliditylang.org/2022/05/17/data- ошибка наследования местоположения/

Можно было изменить расположение данных параметров или возвращаемых переменных с calldata на memory и наоборот при переопределении функций internal и public. Это приводило к генерации недопустимого кода при внутреннем вызове такой функции через virtual вызовов функций.

При вызове функций external не имеет значения, является ли расположение данных параметров calldata или memory, кодировка данных не меняется. Из-за этого изменение местоположения данных при переопределении функций external разрешено. Компилятор также неправильно разрешил изменить расположение данных для переопределения функций public и internal. Поскольку функции public могут вызываться как внутри, так и снаружи, это приводит к созданию недопустимого кода, когда такая неправильно переопределенная функция вызывается внутри посредством базового контракта. Вызывающий объект предоставляет указатель memory, но вызываемая функция интерпретирует его как указатель calldata или наоборот.

Пример:

Я пропустил эту ошибку, так как ее серьезность очень низкая.

20. Проверка размера перекодирования вложенного массива calldata в ABI

Представлено: 0.5.8.
Исправлено: 0.8.14.
Серьезность: очень низкая.
Ссылка: https://blog.soliditylang.org/2022/05/17/calldata- перекодировать-размер-проверить-ошибку/

ABI-перекодирование вложенных динамических массивов calldata не всегда выполняло надлежащие проверки размера по сравнению с размером calldata и могло читаться за пределами calldatasize().

Calldata проверка вложенных динамических типов откладывается до первого доступа к вложенным значениям. Такой доступ может, например, быть копией в memory или доступом к индексу или члену внешнего типа. Хотя в большинстве случаев проверка calldata правильно проверяет, что область данных вложенного массива полностью содержится в переданном calldata (т. е. в диапазоне [0, calldatasize()]), эта проверка может не выполняться, когда такие вложенные типы повторно кодируются ABI непосредственно из calldata. Например, это может произойти, если значение в calldata с вложенным динамическим массивом передается внешнему вызову, используется в abi.encode или генерируется как событие. В таких случаях, если область данных вложенного массива выходит за пределы calldatasize(), кодирование ABI не возвращается, а продолжает чтение значений за пределами calldatasize() (т. е. нулевых значений).

Пример:

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

21. Подписано immutables.

Введено: 0.6.5.
Исправлено: 0.8.9.
Серьезность: очень низкая.
Ссылка: https://blog.soliditylang.org/2021/09/29/signed- неизменяемые-ошибка/

Immutable переменные целочисленного типа со знаком короче 256 бит могут привести к значениям с недопустимыми старшими битами, если используется inline assembly.

При чтении immutable переменных целочисленного типа со знаком длиной менее 256 бит их старшие биты безусловно обнуляются. Правильной операцией было бы расширение значения по знаку, т. е. установка битов более высокого порядка в единицу, если бит знака равен единице. Это расширение знака выполняется Solidity непосредственно перед тем, когда это имеет значение, то есть когда значение сохраняется в memory, когда оно сравнивается или когда выполняется деление. Из-за этого, насколько нам известно, единственный способ получить доступ к значению в его нечистом состоянии — прочитать его через файл inline assembly.

Пример:

Lido Finance не использует подписанные immutables в storage.

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

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

Спасибо за внимание!

Статья подготовлена ​​аудитором OXORIO Владиславом Ярошуком в рамках работы над LIDO project.

Чтобы ничего не пропустить, подпишитесь на @oxorio и @gr_gred.