EIP-214: STATICCALL — вызов, который обещает ничего не трогать
Контракты постоянно вызывают другие контракты просто ради чтения значения — цены, баланса, результата view-функции. Но обычный вызов позволяет вызываемому делать что угодно, включая изменение состояния или повторный вход в вас. STATICCALL — это вызов, для которого EVM гарантирует режим только для чтения: любая попытка изменить состояние внутри него откатывается. Маленькая гарантия, на которой незаметно держатся оракулы, view-функции и защита от reentrancy.
- модель вызовов EVM
- состояние и reentrancy
Byzantium, октябрь 2017. Третье знаковое изменение Byzantium (после REVERT и zk-прекомпайлов) — новый тип вызова. Контрактам постоянно нужно спросить что-то у другого контракта — текущую цену у оракула, баланс, результат view-функции. Но в EVM «спросить» и «сделать» использовали одну и ту же инструкцию: CALL запускает код вызываемого, и этот код может делать что угодно. Так что простое чтение из контракта, которому вы не полностью доверяете, означало вручить ему ключи. EIP-214 добавляет STATICCALL: вызов, для которого протокол гарантирует, что ничего не изменится. Давайте выведем его.
Проблема: CALL ради чтения даёт полную власть
Когда ваш контракт делает обычный CALL, чтобы получить значение, EVM выполняет код вызываемого, и ничто не мешает этому коду сделать больше, чем просто ответить. Он может выполнить SSTORE в своё собственное хранилище, испустить логи, создать контракты — или, что опаснее всего, устроить reentrancy Когда контракт, который вы вызвали, вызывает обратно вас же, прежде чем ваша первая функция завершилась, — потенциально застав вас в наполовину обновлённом состоянии. Классическая атака «опустошить хранилище». Чтение, способное запустить произвольный код, — это риск reentrancy. — вызвать обратно вас посреди выполнения, застав вас в противоречивом состоянии. Так что вызов ненадёжного контракта просто ради чтения числа был настоящей поверхностью атаки: сам акт вопроса позволял отвечающему выполнить произвольный код.
→ Шаг 2: вызов, который EVM принудительно делает только для чтения.
STATICCALL: изменения состояния откатываются — гарантированно
STATICCALL Опкод Byzantium 0xFA. Вызывает контракт так же, как CALL, но помечает весь фрейм вызова — и каждый его вложенный вызов — как статический: любой опкод, изменяющий состояние, встреченный внутри, откатывается. В результате получается вызов, гарантированно являющийся чтением. (опкод 0xFA) выполняет вызываемого точно так же, как CALL, но с одним железным правилом: на протяжении этого фрейма и всех его вложенных вызовов любой опкод, меняющий состояние, откатывается. SSTORE, CREATE, SELFDESTRUCT, LOG, CALL с передачей значения — всё запрещено. Вызываемый может читать сколько угодно (SLOAD, вычисления, вызовы других view-функций), чтобы получить ответ, но не может изменить хранилище, испустить события или переместить средства. Так что STATICCALL — это гарантированное чтение: вызываемый не может войти обратно и изменить что-то, потому что не может изменить вообще ничего.
→ Шаг 3: что открывает гарантия только для чтения.
Выигрыш: безопасные чтения, view-функции, защита от reentrancy
Гарантированный вызов только для чтения — маленький примитив с широким охватом. Он позволяет контракту читать ненадёжный оракул, не боясь, что тот изменит состояние или войдёт обратно. Именно так Solidity обеспечивает view / pure calls Функции Solidity, помеченные как view (читают состояние) или pure (не читают ничего), вызываются через STATICCALL, так что обещание компилятора «не изменяет состояние» на самом деле обеспечивается EVM, а не просто заявляется по договорённости. на уровне EVM — компилятор компилирует вызовы view-функций как STATICCALL, так что обещание «не изменяет состояние» именно обеспечивается, а не просто декларируется. И это фундаментальный инструмент для защиты от reentrancy: статический вызов не может войти обратно и записать что-то, так что пути чтения становятся безопасными по построению.