К основному содержимому

Введите слово из вопроса или названия статьи.

3 · Под капотом

Шаблоны контроля доступа в смарт-контрактах: owner, роли и стороны сделки

Как контракты решают, кому что можно: Ownable и Ownable2Step, роли, проверка сторон сделки, паузы и минимальные права. Разбор на примере эскроу.

3 мин чтенияОбновлено:
В этой статье

Каждая функция контракта отвечает на вопрос: «кому это позволено?» Ошибка в ответе - самая частая причина потери денег: функция вывода без проверки, забытая роль, украденный ключ владельца. Здесь разберём основные шаблоны и посмотрим, как их применяет контракт эскроу. Для контекста про подписи и адреса - криптография с открытым ключом.

Откуда контракт знает, кто вызывает

Внутри функции доступен msg.sender - адрес, который непосредственно вызвал контракт. Он не подделывается: подпись транзакции проверена сетью до исполнения (как это работает). Весь контроль доступа строится на сравнении msg.sender с ожидаемым адресом или ролью.

Шаблон 1. Единственный владелец (Ownable)

Простейшая схема: адрес owner, и функции с модификатором onlyOwner.

solidity
function setTokenAllowed(address token, bool allowed) external onlyOwner {
    allowedToken[token] = allowed;
}

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

Шаблон 2. Двухэтапная передача владения (Ownable2Step)

В простом Ownable передача владельца в одном вызове: опечатка в адресе - и контроль утрачен навсегда. Ownable2Step требует двух шагов:

  1. Текущий владелец вызывает transferOwnership(newOwner).
  2. Новый владелец вызывает acceptOwnership().

Пока второй шаг не сделан, ничего не меняется. Опечатка не страшна, потому что по неверному адресу никто не «примет». Контракт эскроу использует именно эту схему.

Шаблон 3. Роли (AccessControl)

Если у контракта несколько административных обязанностей, давать все права одному адресу рискованно. AccessControl вводит роли - идентификаторы (bytes32), которые можно выдавать и отзывать:

solidity
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

function pause() external onlyRole(PAUSER_ROLE) { ... }

Принцип минимальных прав: каждой роли и каждому адресу - только то, что ему необходимо. Оператору паузы не нужно право выводить комиссии.

Шаблон 4. Стороны сделки

В эскроу главная авторизация - не владелец, а участники конкретной сделки. Права определяются данными сделки:

ДействиеКто можетКак проверяется
Принять офертуконтрагентmsg.sender == (оплачена ? продавец : покупатель)
Подтвердить получениепокупательmsg.sender == d.buyer
Открыть спорпокупатель или продавецmsg.sender равен d.buyer или d.seller
Вынести решениеназначенный гарантmsg.sender == d.arbiter

Ошибки возвращаются как NotAuthorized(). Подробнее о потоке - в жизненном цикле.

Шаблон 5. Проверка состояния как часть доступа

Авторизация - это не только «кто», но и «когда». Каждая функция сделки требует нужного статуса:

solidity
_requireStatus(d, Status.Active);

Нельзя подтвердить получение по оферте, нельзя открыть спор по закрытой сделке. Это конечный автомат (разбор).

Шаблон 6. Функции «для всех» ради живучести

Иногда лучший контроль доступа - его отсутствие. Например, после истечения срока оферты cancelOffer может вызвать любой. Зачем? Чтобы деньги не зависели от активности сторон: если покупатель потерял ключ, а продавец исчез, кто-то посторонний (в том числе сервис) может вернуть средства. Возврат всё равно идёт только покупателю, поэтому злоупотребить функцией нельзя.

Это принцип проектирования: «кто угодно может запустить, но результат определён заранее».

Шаблон 7. Пауза

Pausable позволяет владельцу остановить часть функций при инциденте. В эскроу пауза блокирует только создание и принятие оферт:

solidity
function createOffer(...) external payable whenNotPaused nonReentrant { ... }

Выход из активной сделки всегда работает. Владелец не может заморозить чьи-то деньги (почему это важно).

Что может и чего не может владелец эскроу

МожетНе может
Разрешить или запретить токен для новых сделокЗабрать средства активных сделок
Отметить гаранта «проверенным»Менять результат сделки
Поставить на паузу новые сделкиОстановить выплаты
Забрать накопленные комиссииЗабрать зарезервированное (reserved)
Забрать «ничьи» токеныИзменить код контракта

Гарантия строится на инварианте: баланс контракта не меньше всего, что он должен (reserved). Административные функции могут тронуть лишь превышение. Подробнее - в разборе конечного автомата.

Типичные ошибки

  • Забытый onlyOwner на административной функции.
  • tx.origin вместо msg.sender.
  • Публичная функция инициализации, которую может вызвать кто угодно (прокси).
  • renounceOwnership вызван случайно: контракт остаётся навсегда без управления.
  • Бэкдоры: скрытые роли в коде (аудиты).
  • Слишком широкие права одной роли.

Что почитать дальше

Поговорим об атаках, которые обходят даже верный контроль доступа: реентерабельность и pull-платежи и каталог уязвимостей.

Упоминается в статьях

Готовы попробовать?

Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.