Контроль доступа: кто может вызвать функцию

Самая дешёвая для атакующего дыра — функция, которую забыли закрыть.

Контроль доступа — механизм, ограничивающий, какие адреса вправе вызывать привилегированные функции контракта (смена параметров, вывод средств, апгрейд).

Открытое по умолчанию

Ключевая интуиция: в смарт-контракте любая внешняя/публичная функция вызывается кем угодно, если вы явно не запретили. Нет «скрытых» функций. Если функция 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 и принцип наименьших привилегий.
  • Владелец — мультисиг/таймлок, а двухшаговая передача владения снижает риск ошибки.
Проверьте себя
1. Что произойдёт с публичной функцией без проверки вызывающего?
AЕё нельзя вызвать
BЕё сможет вызвать кто угодно
CЕё вызовет только владелец
DОна не скомпилируется
2. Почему один EOA-владелец — это риск?
AОн платит больше газа
BКомпрометация его приватного ключа отдаёт контроль над протоколом
CОн не может вызывать функции
DОн замедляет сеть
3. Зачем нужна двухшаговая передача владения?
AЧтобы ускорить транзакцию
BЧтобы новый владелец явно принял роль и нельзя было передать контроль на ошибочный адрес
CЧтобы снизить комиссию
DЧтобы скрыть владельца