Ownable — контроль доступа
Как устроен паттерн Ownable в Solidity: owner, onlyOwner, transferOwnership и защита admin-функций.
Ownable даёт одному адресу admin-доступ
Ownable — это паттерн контроля доступа, где у контракта есть один владелец. Только этот адрес может выполнять admin-функции: менять параметры, ставить паузу, выводить ETH или передавать владение.
Паттерн подходит для простых контрактов с одним администратором. Если нужны роли вроде MINTER, PAUSER, TREASURER, одного owner уже мало.
Пишем Ownable-контракт
Создайте файл contracts/ConfigurableVault.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
abstract contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
error OnlyOwner(address caller);
error ZeroAddress();
modifier onlyOwner() {
if (msg.sender != owner) {
revert OnlyOwner(msg.sender);
}
_;
}
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert ZeroAddress();
}
owner = initialOwner;
emit OwnershipTransferred(address(0), initialOwner);
}
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) {
revert ZeroAddress();
}
address previousOwner = owner;
owner = newOwner;
emit OwnershipTransferred(previousOwner, newOwner);
}
}
contract ConfigurableVault is Ownable {
uint256 public feeBps;
uint256 public collectedFees;
bool public paused;
mapping(address => uint256) public deposits;
event Deposited(address indexed user, uint256 amount, uint256 fee);
event FeeUpdated(uint256 oldFeeBps, uint256 newFeeBps);
event PauseUpdated(bool paused);
event Withdrawn(address indexed owner, address indexed to, uint256 amount);
error Paused();
error ZeroAmount();
error FeeTooHigh(uint256 feeBps);
error EthTransferFailed();
constructor(uint256 initialFeeBps) Ownable(msg.sender) {
if (initialFeeBps > 1_000) {
revert FeeTooHigh(initialFeeBps);
}
feeBps = initialFeeBps;
}
function deposit() external payable {
if (paused) {
revert Paused();
}
if (msg.value == 0) {
revert ZeroAmount();
}
uint256 fee = (msg.value * feeBps) / 10_000;
uint256 netAmount = msg.value - fee;
deposits[msg.sender] += netAmount;
collectedFees += fee;
emit Deposited(msg.sender, netAmount, fee);
}
function setFeeBps(uint256 newFeeBps) external onlyOwner {
if (newFeeBps > 1_000) {
revert FeeTooHigh(newFeeBps);
}
uint256 oldFeeBps = feeBps;
feeBps = newFeeBps;
emit FeeUpdated(oldFeeBps, newFeeBps);
}
function setPaused(bool newPaused) external onlyOwner {
paused = newPaused;
emit PauseUpdated(newPaused);
}
function withdrawFees(address payable to) external onlyOwner {
if (to == address(0)) {
revert ZeroAddress();
}
uint256 amount = collectedFees;
if (amount == 0) {
revert ZeroAmount();
}
collectedFees = 0;
(bool success, ) = to.call{value: amount}("");
if (!success) {
revert EthTransferFailed();
}
emit Withdrawn(msg.sender, to, amount);
}
}Этот пример отделяет доступ от бизнес-логики. Ownable хранит владельца и modifier onlyOwner, а ConfigurableVault использует его для admin-функций.
Как это работает
abstract contract Ownable — базовый контракт, который нельзя использовать сам по себе. Его наследуют другие контракты.
address public owner — адрес текущего владельца. Solidity автоматически создаёт getter owner().
modifier onlyOwner() — проверяет msg.sender. Если функцию вызывает не владелец, транзакция откатывается с OnlyOwner(msg.sender).
constructor(address initialOwner) — задаёт первого владельца при деплое. В ConfigurableVault это msg.sender, то есть адрес, который деплоит контракт.
contract ConfigurableVault is Ownable — наследует переменную owner, modifier onlyOwner и функцию transferOwnership.
function setFeeBps(...) external onlyOwner — admin-функция. Обычный пользователь не сможет изменить комиссию.
transferOwnership(address newOwner) — передаёт управление другому адресу. После этого старый owner теряет доступ к функциям с onlyOwner.
Где применять Ownable
Ownable подходит для настроек, которые должен менять один администратор: fee, treasury-адрес, pause-флаг, лимиты, адреса внешних контрактов.
Ownable не подходит, если в системе несколько независимых прав. Например, один адрес должен mint, другой должен pause, третий должен выводить средства. Для этого используют роли, а не одного владельца.
Owner-адрес должен быть защищён. В production это часто multisig, а не обычный EOA-кошелёк одного человека.
Частые ошибки
Оставить owner обычным личным кошельком → если приватный ключ потеряют или украдут, контрактом сможет управлять злоумышленник. Для важных контрактов используйте multisig.
Передать ownership на нулевой адрес → admin-функции станут недоступны. Проверяйте newOwner != address(0).
Защитить не все admin-функции → если забыть onlyOwner на setFeeBps или withdrawFees, вызвать функцию сможет любой адрес.
Что дальше
- Модификаторы — modifiers — разберите, как работает
onlyOwner - Кастомные ошибки — Custom Errors — возвращайте понятные ошибки доступа
- Events — объявление и emit — записывайте смену owner и admin-действия в logs