Pausable контракт
Как добавить emergency-паузу в Solidity-контракт: paused, whenNotPaused, whenPaused и admin-функции pause/unpause.
Pausable останавливает опасные действия
Pausable — это паттерн emergency-остановки. Контракт хранит флаг paused и через modifiers запрещает часть функций, пока команда разбирает инцидент, баг или неправильную настройку.
Пауза не должна автоматически блокировать всё. Часто новые депозиты и mint останавливают, а вывод средств оставляют доступным или делают отдельный emergencyWithdraw.
Пишем vault с паузой
Создайте файл contracts/EmergencyVault.sol:
// 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-логике, пауза не поможет.
Что дальше
- AccessControl — система ролей — выдайте отдельную
PAUSER_ROLEвместо полного owner-доступа - Модификаторы — modifiers — разберите
whenNotPausedиwhenPaused - Checks-Effects-Interactions паттерн — держите безопасный порядок внутри withdraw-функций