Контроль доступа: кто может вызвать функцию
Самая дешёвая для атакующего дыра — функция, которую забыли закрыть.
Контроль доступа — механизм, ограничивающий, какие адреса вправе вызывать привилегированные функции контракта (смена параметров, вывод средств, апгрейд).
Открытое по умолчанию
Ключевая интуиция: в смарт-контракте любая внешняя/публичная функция вызывается кем угодно, если вы явно не запретили. Нет «скрытых» функций. Если функция setOwner или mint не защищена проверкой вызывающего, её вызовет первый встречный — и станет владельцем или напечатает себе токены. Забытый модификатор — одна из самых частых и самых дорогих ошибок в истории DeFi.
// УЯЗВИМО: критическая функция без проверки вызывающего
function setOwner(address newOwner) external {
owner = newOwner; // кто угодно делает себя владельцем
}Базовая защита: onlyOwner
Простейший паттерн — модификатор, который пускает только заранее назначенного владельца:
// БЕЗОПАСНЕЕ: ограничение по владельцу
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setFee(uint256 fee) external onlyOwner {
feeBps = fee;
}OpenZeppelin предоставляет готовые Ownable и AccessControl (роли): не нужно изобретать своё. Роли позволяют разделить полномочия — отдельная роль на паузу, отдельная на казначейство — по принципу наименьших привилегий.
Как работает под капотом: проверенные кирпичи
Хороший контроль доступа — это не «if в начале функции», а системный набор правил: каждая привилегированная функция помечена явной ролью; смена ролей сама защищена; владелец — желательно мультиподпись или таймлок, а не один приватный ключ (его компрометация = захват протокола). Часто применяют двухшаговую передачу владения (новый владелец должен «принять» роль), чтобы случайно не передать контроль на неверный адрес.
// Принцип наименьших привилегий
MINTER_ROLE -- только печать токенов
PAUSER_ROLE -- только пауза
ADMIN_ROLE -- управление ролями (под таймлоком/мультисигом)Частые ошибки
- Забытый модификатор на одной из критических функций. Проверяйте КАЖДУЮ функцию, меняющую состояние/деньги.
- Конструктор/инициализатор без защиты в апгрейдах. Если
initialize()можно вызвать повторно, атакующий перехватит владельца. - Один EOA-владелец. Компрометация ключа = всё потеряно; используйте мультисиг/таймлок.
Итоги
- Любая внешняя функция открыта всем, пока вы явно не ограничили доступ.
- Защищайте КАЖДУЮ привилегированную функцию (мин —
onlyOwner, лучше роли). - Используйте готовые
Ownable/AccessControlи принцип наименьших привилегий. - Владелец — мультисиг/таймлок, а двухшаговая передача владения снижает риск ошибки.