Проблема дорогого постоянного состояния
Контракту иногда нужно сохранить значение только на время одной транзакции. Например, временно отметить, что вызов уже выполняется, передать промежуточный флаг между несколькими контрактами или не допустить повторный вход в чувствительную функцию. До появления специального механизма разработчики могли использовать обычный storage, но его запись и очистка создают расходы на постоянное состояние, либо память, которая доступна только в рамках конкретного исполнения контракта. EIP-1153 вводит transient storage – отдельное хранилище, значения которого живут до конца транзакции.
Новые опкоды называются TSTORE и TLOAD. Первый записывает временное значение в слот, второй читает его. По модели это похоже на словарь ключ-значение с областью хранения, отдельной от обычного storage. В отличие от memory, transient storage имеет правила доступа, связанные с контекстом выполнения контрактов, и может быть полезен при взаимодействии нескольких вызовов внутри транзакции. Его смысл и поведение определяет именно спецификация виртуальной машины, а не название переменной в Solidity.
Чем он отличается от memory и storage
Storage EVM сохраняется между транзакциями и является частью постоянного состояния контракта. Обновление таких данных влияет на состояние сети и подчиняется правилам стоимости чтения и записи. Memory временная, но обычно принадлежит текущему исполнению функции и сбрасывается при завершении вызова. Transient storage занимает промежуточное концептуальное место: оно доступно как хранилище в пределах транзакции, а затем автоматически очищается. Разработчик не должен вручную выполнять отдельную транзакцию очистки после обычного завершения.
Это не означает, что transient storage бесплатен или не нуждается в анализе газа. Операции имеют стоимость, а поведение при вложенных и повторных вызовах зависит от контекста контракта. При делегированном вызове важно понимать, какой контракт является владельцем пространства transient storage. Ошибка в предположении о контексте может привести к коллизии или неверной логике блокировки. Поэтому низкоуровневое применение нужно покрывать тестами, включающими разные последовательности вызовов.
Пример: блокировка повторного входа
Распространённая задача для временного слота – хранить флаг reentrancy guard. При входе в критическую функцию контракт проверяет слот, выставляет его в единицу, выполняет действия, затем очищает флаг. Если в ходе выполнения внешний вызов пытается повторно войти в ту же защищённую область, проверка обнаруживает занятое состояние и прекращает операцию. Поскольку состояние автоматически заканчивается вместе с транзакцией, отдельный постоянный слот может быть не нужен для такого краткоживущего флага.
Transient storage не является универсальной защитой от reentrancy. Разработчику всё равно нужно точно определить границу критической области, обновлять внутреннее состояние в безопасном порядке и проверять все внешние вызовы. Некоторые функции могут намеренно вызывать друг друга, и чрезмерно широкая блокировка ломает композицию. Контракт должен учитывать, какие операции могут быть вызваны повторно через callback, токен, hook или другой протокол. Сам факт использования TSTORE в исходнике не доказывает, что весь контракт защищён.
Коммуникация между вызовами внутри одной транзакции
Другой сценарий – перенос временных параметров между несколькими частями исполнения. Например, внешний контракт может поместить в transient storage контекст, который затем прочитает вызываемый путь, после чего значение исчезнет в конце той же транзакции. Это бывает полезно для оптимизации сложных цепочек и для синхронизации hook-подобной логики. Однако такая архитектура создаёт скрытую связь между модулями: чтобы понять поведение, аудитор должен знать не только входы функции, но и все записи в соответствующий слот по транзакционному пути.
Если одна библиотека и другая часть контракта используют одинаковые ключи, возможно непреднамеренное перекрытие данных. Хорошая практика – использовать тщательно определённые slot identifiers, описывать владельца и назначение значения, избегать неявных зависимостей и проверять делегированные вызовы. Особенно внимательно изучайте прокси и модули, где код одного контракта исполняется в контексте другого адреса. Память, обычный storage и transient storage имеют разные области жизни и пространства, и их нельзя подменять друг другом без анализа.
Совместимость сети и компилятора
EIP-1153 активируется правилами конкретной сети и её EVM-форка. Если контракт с TSTORE и TLOAD отправить в сеть, где эти опкоды не поддерживаются, выполнение может завершиться ошибкой. Поэтому разработчик обязан установить целевые сети и версии инструментов, проверить конфигурацию компилятора и тестовой среды, а также убедиться, что выбранный fork соответствует production. Возможность скомпилировать исходник локально сама по себе не подтверждает, что код запустится в любой совместимой с EVM сети.
Также стоит проверять версии библиотек и готовых guard-модулей: наличие одноимённой функции не гарантирует одинаковый байткод или требования к сети. Не копируйте пример из блога без проверки применимости к вашему compiler target. Если проект работает в нескольких сетях, включите в матрицу тестов каждую из них либо используйте совместимую реализацию для сетей без поддержки опкодов.
Как аудитору проверить TSTORE и TLOAD
Начните с поиска всех мест чтения и записи transient slot. Для каждого ключа выясните, кто его записывает, в каком контексте, когда он сбрасывается и какая ветка исполнения может оставить значение до конца транзакции. Проследите внешние вызовы, callback, повторный вход и делегированные вызовы. Затем проверьте, что разные модули не переиспользуют один и тот же ключ с несовместимым смыслом. Симуляции должны включать успешные и откатывающиеся вызовы, вложенность и повторную последовательность операций в одной транзакции.
Нужно отдельно помнить о поведении при revert: изменения состояния, сделанные внутри откатившегося фрейма, следуют правилам отката EVM. Анализируйте весь граф вызовов, а не только локальную функцию. Тест, который проверяет значение в отдельной транзакции, может пропустить баг, возникающий лишь при нескольких вызовах в одном атомарном исполнении. Для практики разбора низкоуровневой ошибки пригодится материал о чтении трассировки revert EVM-транзакции](/kak-prochitat-revert-trace-evm-tranzakcii).
Когда механизм действительно нужен
Transient storage полезен для короткоживущих флагов и состояния, которое должно быть общим для нескольких вызовов в одной транзакции, но не требуется после её завершения. Если значение нужно сохранить между транзакциями, выбирайте обычный storage. Если оно используется только локально внутри одного исполнения и не должно быть доступно другим фреймам, обычно достаточно memory или переменной. Чем меньше скрытого состояния, тем легче тестировать и сопровождать контракт.
Главный вопрос при проектировании – не «можно ли заменить storage на TSTORE», а «какой срок жизни и область видимости нужны этому значению». После ответа сравните стоимость, совместимость, поведение при reentrancy и сложность аудита. EIP-1153 добавил точный инструмент в набор EVM, но безопасность зависит от корректного понимания модели исполнения. Для продакшена следует опираться на спецификацию, тесты целевых сетей и аудит конкретного контракта.
Сопоставляйте transient storage с остальными областями состояния EVM во время ревью. Обычное storage сохраняется между транзакциями, а локальные данные могут находиться в памяти текущего вызова. Контекст работы ноды тоже влияет на обработку сетевых операций и транзакций; базовое различие между архивными и полными узлами показывает, почему отдельные свойства инфраструктуры нельзя выводить только из Solidity. Для прикладной диагностики ошибки выполнения изучите, как прочитать трассировку revert EVM. Эти материалы не заменяют EIP-1153, но дают практический контекст для проверки поведения контракта и среды.



