0x01 為什么需要暫停功能
當一個協(xié)議有下面這些考慮時,一般就需要添加暫停功能了:
協(xié)議本身有一定的中心化屬性
比如大部分中間人機制的跨鏈橋合約把篓,RWA 這種需要鏈上鏈下互動的合約婉刀,都離不開一些偏中心化角色的參與贤重。既然有參與的權(quán)利飞蛹,就要為資金安全承擔一定的責任漾肮,暫停功能可以在合約出現(xiàn)問題時起一定的防護作用厂抖。協(xié)議未來有持續(xù)演進升級的需要
升級過程中有可能需要對某些功能進行暫停。安全協(xié)作需要
筆者參與的一些項目克懊,資金大戶有明確需求忱辅,他們可以監(jiān)控鏈上狀態(tài)七蜘,當發(fā)現(xiàn)有異常的時候,可以直接使用暫停功能來暫停協(xié)議的運行墙懂。
0x02 常見的暫停功能設(shè)計
用的最多的就是 OpenZeppelin 提供的 Pausable.sol 模版了橡卤。很多時候這個模版已經(jīng)很好用了,但當協(xié)議變的比較復(fù)雜之后损搬,這個模版有個最大的限制就出來了:粒度比較粗碧库。一旦設(shè)置了暫停,就是全局暫停巧勤,意味著所有使用 whenPaused
這個 modifier 修飾的函數(shù)都將無法調(diào)用嵌灰。但有的時候我們需要更細粒度的控制,比如對于借貸協(xié)議來說颅悉,某種情況下只需要暫停借款沽瞭,某種情況下只需要暫停某個借貸池,如果沒有一個通用的設(shè)計剩瓶,就需要在業(yè)務(wù)層進入侵入性設(shè)計驹溃,將暫停功能和業(yè)務(wù)功能混在一起。
0x03 細粒度暫停
最近看到這篇文章 提到的細粒度暫停設(shè)計延曙,感覺相當不錯豌鹤。
所謂細粒度暫停,是把暫停功能分為三個級別:
- 全局暫停
- 合約級別的暫停
- 函數(shù)級別的暫停
這樣枝缔,我們可以根據(jù)需要布疙,非常高效的進行暫停操作。比如對于借貸協(xié)議魂仍,當需要暫停借款時拐辽,可以只設(shè)置對借款函數(shù)的暫停拣挪,需要暫停某個借貸池的時候擦酌,直接對那個借貸池合約設(shè)置暫停。
0x04 細粒度暫停的基本實現(xiàn)
- 我們用一個合約
GlobalPauseController
來管理相關(guān)狀態(tài)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract GlobalPPauseController is Ownable {
bool public globalPause;
mapping(address => bool) public contractPause;
mapping(address => mapping(bytes4 => bool)) public functionUnpause;
event GlobalPauseSet(bool status);
event ContractPauseSet(address indexed contractAddress, bool status);
event FunctionUnpauseSet(address indexed contractAddress, bytes4 indexed functionSig, bool status);
function setGlobalPause(bool _status) external onlyOwner {
globalPause = _status;
emit GlobalPauseSet(_status);
}
function setContractPause(address _contract, bool _status) external onlyOwner {
contractPause[_contract] = _status;
emit ContractPauseSet(_contract, _status);
}
function setFunctionUnpause(address _contract, bytes4 _functionSig, bool _status)
external
onlyOwner
{
functionUnpause[_contract][_functionSig] = _status;
emit FunctionUnpauseSet(_contract, _functionSig, _status);
}
/// @dev When the protocol or a contract is paused, we cannot unpause a function, so return `false`
/// @dev Otherwise check if the given function is unpaused.
function isPaused(address _contract, bytes4 _functionSig)
external
view
returns (bool)
{
if (!globalPause && !contractPause[_contract]) {
return false;
}
return !functionUnpause[_contract][_functionSig];
}
}
這個合約允許通過 owner 權(quán)限來設(shè)置全局/合約級別/合約函數(shù)級別的暫停狀態(tài)菠劝,isPaused 來判斷是不是需要暫停赊舶。這個合約我感覺有兩點可以根據(jù)需要寫的更靈活一些:
i. Ownerable 可以換為 AccessControl 進行更細粒度的權(quán)限控制
ii. 可以添加一個全局的函數(shù)暫停狀態(tài) globalFunctionPause, 設(shè)置非特定合約實例的函數(shù)暫停,還是以借貸協(xié)議為例的話赶诊,這樣要暫停借貸協(xié)議所有池子的某個功能時就會更方便
- 創(chuàng)建一個供其它合約繼承使用的類似 OpenZeppelin Pausable 的合約
abstract contract Pausable {
GlobalPauseController public gpc;
error Paused();
constructor(address _gpc) {
gpc = GlobalPauseController(_gpc);
}
modifier whenNotPaused() {
if(gpc.isPaused(address(this), msg.sig))
revert Paused();
_;
}
}
這個合約最主要的就是提供了 whenNotPaused
修飾器來判斷是否要暫停當前函數(shù)
- 使用 Pausable 合約笼平,下面是個 Demo
contract LendingPool is Pausable {
mapping(address => uint256) public balances;
constructor(address _pauseController) Pausable(_gpc) {}
function deposit() external payable whenNotPaused {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external whenNotPaused {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}