китаев.tech

Ownable — контроль доступа

Как устроен паттерн Ownable в Solidity: owner, onlyOwner, transferOwnership и защита admin-функций.

Ownable даёт одному адресу admin-доступ

Ownable — это паттерн контроля доступа, где у контракта есть один владелец. Только этот адрес может выполнять admin-функции: менять параметры, ставить паузу, выводить ETH или передавать владение.

Паттерн подходит для простых контрактов с одним администратором. Если нужны роли вроде MINTER, PAUSER, TREASURER, одного owner уже мало.


Пишем Ownable-контракт

Создайте файл contracts/ConfigurableVault.sol:

solidity
// 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, вызвать функцию сможет любой адрес.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.