Безопасность Web3: с нуляЧасть 1 из 12SmartAlert5 мин чтения

Что такое смарт-контракт: код, состояние, события и кто что может вызвать

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

*Часть 1 из 12 в серии «Безопасность Web3: с нуля». Серия объясняет, как устроены контракты, как их ломают и за чем можно следить. Знать Solidity не нужно.*

Мы пишем разборы взломов, и почти в каждом одни и те же два абзаца пояснений: «шлюз — это прокси», «порог — число в хранилище». Здесь мы собрали эти пояснения в одном месте. Часть 1 — основа: что лежит по адресу контракта и что оттуда видит мониторинг, а что нет.

Код и состояние по одному адресу

Смарт-контракт — программа, записанная в блокчейн по определённому адресу. У неё две части1:

  • Код. Скомпилированные инструкции, зафиксированные при развёртывании. Потом их никто не правит. (В части 3 разберём случай, когда адрес остаётся, а код за ним всё же меняется.)
  • Состояние. Собственное хранилище контракта: балансы, адрес владельца, настройки, счётчики. Меняется именно оно.

Маленький пример: контракт с комиссией и владельцем.

solidity
contract Vault {
    address public owner;
    uint256 public feeBps;          // комиссия в базисных пунктах, 100 = 1%

    event FeeChanged(uint256 oldFee, uint256 newFee);

    constructor() { owner = msg.sender; }

    function setFee(uint256 newFee) external {
        require(msg.sender == owner, "not owner");
        emit FeeChanged(feeBps, newFee);
        feeBps = newFee;
    }
}

owner и feeBps — состояние. setFee — код, который его меняет. Всё остальное в статье — вариации на эти строки.

Вызовы: кто что может вызвать

Любое действие в сети — это транзакция: её подписывает аккаунт и отправляет на адрес. Если транзакция вызывает функцию, контракт видит вызывающего как msg.sender2.

Функции бывают двух видов:

  • Читающие (view) возвращают данные и ничего не меняют. Их может вызвать кто угодно, бесплатно и без транзакции. feeBps() как раз такая: Solidity сам создаёт геттер для каждой public-переменной.
  • Пишущие меняют состояние. Нужны транзакция и газ, и выполнятся они, только если контракт пустит вызывающего.

«Пустит» — это код, который написал разработчик. В setFee это одна строка: require(msg.sender == owner). Нет проверки — комиссию поставит кто угодно. Проверка есть, но в owner не тот адрес — комиссию поставит не тот человек. Этому посвящена вся часть 2.

Транзакция может и откатиться (revert). Если require не прошёл, все изменения вызова отменяются, но сама транзакция попадает в блок со статусом «неуспешна», и газ платит отправитель. Неудачная попытка вызвать админскую функцию — всё равно видимый факт: кто-то пробовал.

События: журнал, который пишет контракт

Состояние хранится внутри контракта. События — другое. emit FeeChanged(...) добавляет запись в лог транзакции3. Что важно для мониторинга:

  • Контракт события не читает, только пишет. Они нужны внешнему миру: приложениям, эксплорерам, мониторингу.
  • События дешёвые. Разработчики объявляют ими важные изменения: Transfer на каждое движение токена, OwnershipTransferred, Upgraded, Paused.
  • Событие — это лишь обещание разработчика. Компилятор не заставит setFee вызвать emit FeeChanged. Забыли emit — состояние изменилось, а лог молчит.

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

События и состояние — два разных вопроса

Эта мысль проходит через всю серию.

Вопрос
Событие«Что только что произошло?»
Состояние«Что верно прямо сейчас?»
Где лежит
СобытиеЛоги транзакций
СостояниеХранилище контракта
Как увидеть
СобытиеЧитать логи каждого блока
СостояниеВызвать view-функцию
Слабое место
СобытиеНикто не выпустил событие
СостояниеНикто не спросил

Чистый пример — наш разбор Ronin. Команда моста обновила шлюз, событие Upgraded было ожидаемым и верным. А внутри контракта число, от которого зависит, сколько подписей операторов нужно для вывода, стало равно 0. Событием об этом никто не сообщил. Показал это только вызов minimumVoteWeight(), а первый вывод случился через 48 минут.

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

У нативной монеты событий нет

Ещё одна дыра. Перевод токена выпускает Transfer, и за ним можно следить. Нативный ETH (или BNB), входящий на адрес и уходящий с него, не выпускает ничего. Увидеть его можно только по балансу. Это важно для мостов и казначейств, которые держат собственную монету сети.

Что видит внешний наблюдатель

Итого, у наблюдателя за контрактом четыре источника:

  1. События из логов контракта.
  2. Состояние через вызовы view.
  3. Балансы адреса: нативной монеты и токенов.
  4. Сами транзакции: кто отправил, прошла ли, какую функцию вызвала.

Публичные сети отдают всё это бесплатно и по каждому контракту. Не хватает того, кто читает это в три часа ночи.

Как поймать

Какие правила SmartAlert закрывают какой источник, как продукт работает сейчас4:

  • События. При добавлении контракта SmartAlert читает его ABI и предлагает шаблоны правил по найденным событиям: OwnershipTransferred, Upgraded, Paused, RoleGranted и другим. Рецепты: смена владельца, обновление прокси, пауза.
  • Состояние. Правило «Состояние контракта» по расписанию вызывает view-функцию и срабатывает по условию: например, paused() вернула true (рецепт) или число упало ниже нижней границы. Это правило поймало бы порог Ronin. Доступно на Pro и выше.
  • Балансы. Правило по балансу в долларах следит и за нативной монетой, и за токенами (рецепт).
  • Упавшие транзакции. Правило по доле упавших транзакций видит откаты, в том числе чужой неудачный вызов админской функции.

Два ограничения, прямо. Первое: если критичное значение меняется без события и у него нет view-функции, ни одно правило за ним не уследит. Добавьте для него геттер. Второе: алерт не мгновенный. В Ethereum SmartAlert читает блоки после 12 подтверждений, поэтому уведомление приходит примерно через 2,5–4 минуты после транзакции.

Дальше, часть 2: Владельцы, роли и админские ключи: где в контракте дверь.

Источники

  1. 1.ethereum.org, Introduction to smart contracts. ↩
  2. 2.Документация Solidity, Block and transaction properties (msg.sender). ↩
  3. 3.Документация Solidity, Events. ↩
  4. 4.SmartAlert, задержка алерта в Ethereum: 12 подтверждений (около 144 с) плюс опрос и интервал проверки правила; см. разбор Ronin, сноска 8. ↩

Попробуйте бесплатно

Достаточно адреса. Первые 14 дней действуют лимиты Pro, а правила проверяются на истории до включения.

Ещё статьи