Контроль доступа в смарт-контракте: Ownable, роли и мультиподпись
Контроль доступа в смарт-контракте решает, кто может вызывать опасные функции: выпуск токенов, паузу, вывод средств, смену параметров и обновление логики. Для одного администратора подходит Ownable2Step из OpenZeppelin, для нескольких обязанностей — AccessControl с отдельной ролью на каждую. Сами права в боевой сети должны принадлежать не личному кошельку, а мультиподписи, а для самых опасных действий — таймлоку с задержкой. Ошибки в правах — одна из самых частых причин потерь, потому что такой контракт работает ровно так, как написан: отдаёт всё тому, у кого ключ.
Что именно нужно защищать
Начните не с кода, а со списка. Выпишите все функции, которые меняют деньги, права или параметры, и для каждой ответьте: кто должен её вызывать, как часто, и что случится, если её вызовет злоумышленник. Этот список потом становится частью документации для аудита.
| Функция | Чем опасна в чужих руках | Типичное решение |
|---|---|---|
Выпуск токенов (mint) | Неограниченная эмиссия, обесценивание | Роль MINTER_ROLE у мультиподписи, лимит на выпуск |
Пауза (pause) | Блокировка всех пользователей | Отдельная роль PAUSER_ROLE, снимать паузу — другая роль |
| Вывод средств из контракта | Прямая кража | Мультиподпись плюс таймлок, лимиты |
| Смена оракула или параметров | Подмена цены, ликвидации | Таймлок с задержкой, проверки диапазонов |
| Обновление логики (upgrade) | Полная замена правил | Мультиподпись плюс таймлок |
| Выдача ролей | Захват всех остальных прав | Администратор ролей с задержкой смены |
Ownable и Ownable2Step
Ownable — самый простой вариант: у контракта один владелец, функции с модификатором onlyOwner может вызвать только он. В OpenZeppelin 5 владелец передаётся в конструктор явно: constructor(address initialOwner) Ownable(initialOwner) {}. Это сделано, чтобы владельцем по ошибке не стал адрес, с которого шёл деплой.
У обычного Ownable есть неприятная особенность: transferOwnership сразу передаёт права, и опечатка в адресе отдаёт контракт в никуда. Ownable2Step добавляет подтверждение: новый владелец должен вызвать acceptOwnership(). Если адрес ошибочный, подтверждения не будет, и права останутся у старого владельца. Для боевых контрактов с одним администратором берите именно Ownable2Step.
AccessControl: роли вместо одного владельца
AccessControl позволяет завести несколько ролей, у каждой — свои держатели и свой администратор. Роль — это идентификатор вида bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");. Функция защищается модификатором onlyRole(MINTER_ROLE). Главная роль DEFAULT_ADMIN_ROLE (значение 0x00) по умолчанию управляет всеми остальными.
- Разделяйте обязанности: тот, кто ставит паузу, не должен быть тем, кто выводит средства.
- Давайте роли контрактам и мультиподписям, а не сотрудникам лично.
- Фиксируйте событиями каждую выдачу и отзыв роли — OpenZeppelin делает это сам (
RoleGranted,RoleRevoked). - Ведите реестр: какой адрес держит какую роль и почему. Ончейн это видно, но без реестра неясно, кто за адресом стоит.
AccessControlDefaultAdminRules
Расширение AccessControlDefaultAdminRules делает главную роль безопаснее. У неё всегда один держатель, передача идёт в два шага, а между запросом и принятием действует задержка, которую задают при деплое. Если ключ администратора скомпрометирован и злоумышленник пытается передать права себе, у команды есть время это заметить. Расширение также не позволяет случайно отказаться от главной роли без процедуры.
Какой вариант выбрать
| Ситуация | Подходит | Почему |
|---|---|---|
| Простой контракт, один администратор | Ownable2Step | Минимум кода, защита от опечатки при передаче |
| Несколько обязанностей и исполнителей | AccessControl | Разделение прав, роли можно выдавать контрактам |
| Продукт с деньгами пользователей | AccessControlDefaultAdminRules плюс мультиподпись | Задержка смены главного администратора |
| Опасные изменения параметров и логики | TimelockController как держатель роли | Пользователи видят изменение заранее |
| Контракт после запуска не должен меняться | Отказ от прав | Нечего украсть, но и нечего исправить |
Мультиподпись: кто держит ключи
Мультиподпись — это кошелёк-контракт, который исполняет транзакцию только после подписей нескольких владельцев, например 3 из 5. Самый распространённый вариант в EVM-сетях — Safe. Если держателем роли или владельцем контракта сделать мультиподпись, кража одного ключа уже не даёт контроля над контрактом.
- Подписанты — разные люди с разными устройствами, лучше с аппаратными кошельками.
- Порог выбирают так, чтобы потеря одного-двух ключей не блокировала работу, а сговор двух людей не давал контроля.
- Перед подписью каждый проверяет, что именно подписывает: адрес контракта, функцию, параметры.
- Есть регламент замены подписанта при увольнении или утере устройства.
- Тестовая транзакция проходит после каждого изменения состава подписантов.
Настройку мультиподписи для компании и регламенты подписания мы делаем в рамках решения для казначейства. Принципы похожи на обычную работу с доступами: о том, почему после увольнения сотрудника доступ нужно отзывать сразу, мы писали в статье про токены, которые остались после отключения сотрудника.
Таймлок: время на реакцию
TimelockController из OpenZeppelin ставит операции в очередь и исполняет их не раньше заданной задержки. Мультиподпись предлагает операцию, таймлок держит её, например, двое суток, затем любой разрешённый исполнитель её выполняет. Пользователи и мониторинг видят запланированное изменение и успевают отреагировать. Для экстренной паузы таймлок обычно не используют: она должна срабатывать сразу, но снимать её стоит уже через задержку.
Права в обновляемых контрактах
Если контракт стоит за прокси, права работают через инициализатор, а не конструктор: __Ownable_init(initialOwner) или __AccessControl_init() вызываются в initialize. Отдельное право — обновление логики, оно задаётся в _authorizeUpgrade. Это самое сильное право в системе: тот, кто им владеет, может заменить все остальные проверки. Поэтому его держатель — мультиподпись с таймлоком, а не та же роль, что ставит паузу. Подробности — в статье о proxy и UUPS.
Принцип минимальных прав
Каждая роль должна давать ровно то, что нужно для её задачи, и ничего сверх. Бот, который раз в сутки начисляет награды, получает только роль начисления с лимитом суммы, а не права администратора. Если ключ бота утечёт, ущерб ограничится лимитом. Этот же принцип работает в обычной инфраструктуре: о том, как устроить ключи интеграций и их ротацию, мы писали в статье о ротации API-ключа.
Типичные ошибки
- Владелец контракта — личный кошелёк разработчика, который «потом передаст».
DEFAULT_ADMIN_ROLEостался у адреса деплоя после передачи остальных ролей.- Функция без модификатора прав, потому что её «никто не знает». Все функции контракта видны в байт-коде.
- Проверка прав через
tx.originвместоmsg.sender. - Инициализатор обновляемого контракта не вызван, и права забирает первый, кто его вызовет.
- Роль выдана внешнему контракту, который сам не защищён.
Проверка прав после деплоя
После развёртывания обязательно проверьте ончейн: кто owner(), кто держит каждую роль, нет ли прав у адреса деплоя, правильная ли задержка у таймлока. Запишите результаты в документацию проекта. О порядке деплоя и передачи прав — в статье о деплое и верификации. Изменения ролей стоит отслеживать постоянно, это часть мониторинга контрактов. Если нужна помощь со схемой прав, обращайтесь к команде разработки смарт-контрактов.
Вопросы и ответы
Чем Ownable отличается от AccessControl?
Ownable даёт один уровень прав: владелец может всё, остальные ничего. AccessControl позволяет завести несколько ролей с разными держателями и разделить обязанности.
Зачем нужен Ownable2Step?
Он требует, чтобы новый владелец подтвердил передачу прав. Если при передаче ошиблись адресом, права не уйдут в никуда, а останутся у текущего владельца.
Что такое мультиподпись простыми словами?
Это кошелёк, из которого транзакция уходит только после подписей нескольких владельцев, например трёх из пяти. Кража одного ключа не даёт злоумышленнику контроля.
Что будет, если вызвать renounceOwnership?
Контракт навсегда останется без владельца, и все функции с onlyOwner станут недоступны. Если отказ от прав не планируется, эту функцию лучше переопределить так, чтобы она откатывалась.
Нужен ли таймлок для небольшого проекта?
Для функций, которые меняют логику, оракулы и вывод средств, таймлок полезен даже в небольшом проекте. Он даёт пользователям и команде время заметить нежелательное изменение.
Читайте также
Смарт-контракт простыми словами: что это и когда бизнесу он не нужен
БлокчейнSolidity с нуля: из чего состоит смарт-контракт в 2026 году
БлокчейнСколько стоит разработка смарт-контракта и из чего складывается цена
БлокчейнАпгрейдируемые смарт-контракты: proxy, UUPS, плюсы и риски
БлокчейнТестирование смарт-контрактов: Foundry, Hardhat, фаззинг, инварианты
БлокчейнЧек-лист перед аудитом смарт-контракта: что подготовить команде