китаев.tech

Pausable контракт

Как добавить emergency-паузу в Solidity-контракт: paused, whenNotPaused, whenPaused и admin-функции pause/unpause.

Pausable останавливает опасные действия

Pausable — это паттерн emergency-остановки. Контракт хранит флаг paused и через modifiers запрещает часть функций, пока команда разбирает инцидент, баг или неправильную настройку.

Пауза не должна автоматически блокировать всё. Часто новые депозиты и mint останавливают, а вывод средств оставляют доступным или делают отдельный emergencyWithdraw.


Пишем vault с паузой

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

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EmergencyVault {
    address public immutable owner;
    bool public paused;

    mapping(address => uint256) public balances;

    event Paused(address indexed account);
    event Unpaused(address indexed account);
    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event EmergencyWithdrawn(address indexed user, uint256 amount);

    error OnlyOwner(address caller);
    error ContractPaused();
    error ContractNotPaused();
    error ZeroAmount();
    error NotEnoughBalance(uint256 available, uint256 required);
    error EthTransferFailed();

    modifier onlyOwner() {
        if (msg.sender != owner) {
            revert OnlyOwner(msg.sender);
        }

        _;
    }

    modifier whenNotPaused() {
        if (paused) {
            revert ContractPaused();
        }

        _;
    }

    modifier whenPaused() {
        if (!paused) {
            revert ContractNotPaused();
        }

        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function pause() external onlyOwner whenNotPaused {
        paused = true;

        emit Paused(msg.sender);
    }

    function unpause() external onlyOwner whenPaused {
        paused = false;

        emit Unpaused(msg.sender);
    }

    function deposit() external payable whenNotPaused {
        if (msg.value == 0) {
            revert ZeroAmount();
        }

        balances[msg.sender] += msg.value;

        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external whenNotPaused {
        _withdraw(msg.sender, amount);

        emit Withdrawn(msg.sender, amount);
    }

    function emergencyWithdraw() external whenPaused {
        uint256 amount = balances[msg.sender];

        if (amount == 0) {
            revert ZeroAmount();
        }

        _withdraw(msg.sender, amount);

        emit EmergencyWithdrawn(msg.sender, amount);
    }

    function _withdraw(address user, uint256 amount) private {
        if (amount == 0) {
            revert ZeroAmount();
        }

        uint256 balance = balances[user];

        if (balance < amount) {
            revert NotEnoughBalance(balance, amount);
        }

        balances[user] = balance - amount;

        (bool success, ) = payable(user).call{value: amount}("");

        if (!success) {
            revert EthTransferFailed();
        }
    }
}

Этот контракт принимает ETH, разрешает обычный вывод только в рабочем режиме и даёт пользователю вывести весь баланс через emergencyWithdraw, когда контракт поставлен на паузу.


Как это работает

bool public paused — флаг состояния. Если true, функции с whenNotPaused откатываются.

modifier whenNotPaused() — защищает обычные пользовательские действия: deposit и withdraw.

modifier whenPaused() — разрешает функцию только во время паузы. В примере так работает emergencyWithdraw.

pause() external onlyOwner whenNotPaused — включить паузу может только owner, и только если контракт ещё не на паузе.

unpause() external onlyOwner whenPaused — выключить паузу может только owner, и только если пауза уже включена.

_withdraw(...) — приватная общая логика вывода. Она использует Checks-Effects-Interactions: проверка, уменьшение balances, затем отправка ETH.


Что ставить на паузу

Обычно на паузу ставят действия, которые увеличивают риск: deposit, mint, borrow, swap, открытие позиции, изменение параметров пользователем.

Не всегда нужно ставить на паузу вывод средств. Если проблема не связана с withdraw-логикой, пользователям лучше оставить способ забрать активы.

Admin-функции тоже нужно выбирать аккуратно. Иногда pause должен быть доступен быстрому PAUSER_ROLE, а unpause — только multisig после проверки причины инцидента.


Pausable vs удаление функции

Пауза не удаляет код и не меняет ABI. Она только добавляет проверку перед выполнением функции.

Если контракт уже задеплоен без upgrade-механизма, добавить Pausable позже нельзя. Поэтому emergency-паузу проектируют до деплоя, особенно для контрактов с пользовательскими средствами.

Пауза — не замена аудиту. Она уменьшает ущерб во время инцидента, но не исправляет баг сама по себе.


Частые ошибки

Поставить на паузу withdraw без emergency-вывода → пользователи не смогут забрать средства даже в безопасном сценарии.

Дать pause/unpause одному EOA → потеря или компрометация ключа заблокирует контракт или откроет его раньше времени. Для важных контрактов используйте роли и multisig.

Считать pause защитой от всех атак → если уязвимая функция остаётся доступной или баг в admin-логике, пауза не поможет.


Что дальше

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