Об интересном обновлении в Solidity 0.8.29 я узнал на днях из чатика аудиторов, спасибо @SovaSlava!
В этой версии языка появилась поддержка указания кастомных слотов хранилища смарт контрактов, как на скрине выше.
Контракты в Solidity по умолчанию размещают свои переменные состояния, начиная с нулевого слота хранилища, и далее последовательно, в соответствии с порядком определения и иерархией наследования. Однако, начиная с новой версией языка, появилась возможность явно задать начальный слот для размещения хранилища с помощью спецификатора layout at. Это позволяет разработчику управлять тем, с какого именно слота будут начинаться переменные состояния контракта, включая те, что унаследованы от базовых контрактов. Такая гибкость открывает новые возможности для сложных сценариев, особенно в системах с динамической логикой или при работе с прокси-паттернами, где требуется точный контроль над расположением данных в хранилище.
Рассмотрим конкретный пример:
contract C layout at 2**255 - 42 {
uint x;
}contract C layout at 2**255 - 42 задаёт начальный слот для хранилища как значение, равное 2 в степени 255 минус 42. Это число находится в верхней половине диапазона uint256, но всё ещё достаточно далеко от максимального значения, чтобы оставить место для размещения хотя бы нескольких переменных. Переменная uint x, будучи первой в контракте, будет размещена именно в этом слоте — то есть в слоте с номером 578...66. Это демонстрирует, что выражение в спецификаторе layout at может быть сложным, но при этом оно должно быть вычислимо на этапе компиляции.
Спецификатор layout at должен содержать выражение и результат должен быть значением типа uint256. Это может быть простая константа, сумма или битовый сдвиг, но не может включать вызовы функций или переменные. Компилятор проверяет, не выходит ли суммарный объём статических данных за пределы адресного пространства хранилища. Например, если базовый слот установлен слишком близко к type(uint256).max, и при этом контракт имеет много переменных, компилятор выдаст ошибку, чтобы предотвратить «переполнение» хранилища. Однако динамические структуры, такие как массивы переменной длины и маппинги, не подпадают под эту проверку, поскольку их данные размещаются не линейно, а по хеш-адресам, вычисляемым на основе ключей и базового слота.
Важно понимать, что спецификатор layout at можно указать только для самого верхнего контракта в дереве наследования, и он влияет на все переменные состояния во всей иерархии. Если контракт A наследуется B, а B — C, и только C имеет layout at, то все переменные из A, B и C будут сдвинуты на одну и ту же величину. При этом абстрактные контракты, интерфейсы и библиотеки не могут использовать этот механизм, так как они либо не имеют собственного хранилища, либо не предназначены для развертывания как независимые экземпляры. Также не затрагиваются временные переменные, помеченные как transient, поскольку они хранятся не в постоянном хранилище.
На практике этот механизм может быть использован в системах, где требуется изолировать хранилище контракта от других данных, например, в реализациях UUPS (Universal Upgradeable Proxy Standard), где логика обновления должна гарантированно не затрагивать определённые слоты. Другой пример — многомодульные системы, где каждый модуль размещается в выделенной области хранилища, и использование layout at позволяет избежать случайных пересечений. Однако следует быть осторожным: использование слотов вблизи верхней границы адресного пространства может затруднить будущие обновления или привести к коллизиям при использовании inline assembly для ручной записи данных. Также стоит помнить, что layout и at пока не являются зарезервированными словами, но их использование в качестве идентификаторов в будущем станет невозможным, поэтому лучше избегать их в именах переменных и функций уже сейчас.
#solidity
