Solidity合约的暂停和恢复!区块链上跑的智能合约,资金和数据都在链上,遇到紧急情况,比如发现漏洞、黑客攻击,或者需要维护,咋办?暂停功能就是救命稻草!它能让合约“刹车”,阻止关键操作,等修好再恢复。暂停与恢复的核心概念先搞清楚几个关键点:暂停功能:暂时禁用合约的关键功能(如转账、存款)
Solidity合约的暂停和恢复!区块链上跑的智能合约,资金和数据都在链上,遇到紧急情况,比如发现漏洞、黑客攻击,或者需要维护,咋办?暂停功能就是救命稻草!它能让合约“刹车”,阻止关键操作,等修好再恢复。
先搞清楚几个关键点:
Pausable和访问控制库。delegatecall需考虑暂停状态传递。咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从基础暂停到多签和条件暂停,逐步实现安全的暂停与恢复机制。
用Hardhat搭建开发环境,写和测试合约。
mkdir pause-resume-demo
cd pause-resume-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers
初始化Hardhat:
npx hardhat init
选择TypeScript项目,安装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
pause-resume-demo/
├── contracts/
│ ├── BasicPause.sol
│ ├── RoleBasedPause.sol
│ ├── MultiSigPause.sol
│ ├── ConditionalPause.sol
│ ├── UpgradablePause.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── PauseResume.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./"
},
"include": ["hardhat.config.ts", "scripts", "test"]
}
hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
npx hardhat node
先从简单的暂停机制开始,用OpenZeppelin的Pausable。
contracts/BasicPause.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract BasicPauseToken is ERC20, Ownable, Pausable {
constructor() ERC20("BasicPauseToken", "BPT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
super.transfer(to, amount);
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}
Pausable、Ownable和ERC20。transfer:用whenNotPaused修饰符,暂停时禁用。pause:调用_pause,设置暂停状态,仅owner可调用。unpause:调用_unpause,恢复正常,仅owner可调用。onlyOwner限制暂停/恢复权限。Pausable用whenNotPaused保护关键函数。balanceOf)。pause/~5k Gas(SSTORE)。whenNotPaused增加~1k Gas/调用。test/PauseResume.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { BasicPauseToken } from "../typechain-types";
describe("BasicPause", function () {
let token: BasicPauseToken;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("BasicPauseToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should pause and block transfers", async function () {
await token.pause();
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
});
it("should unpause and allow transfers", async function () {
await token.pause();
await token.unpause();
await token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"));
expect(await token.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("999100"));
});
it("should restrict pause to owner", async function () {
await expect(token.connect(user1).pause()).to.be.revertedWith("Ownable: caller is not the owner");
});
});
跑测试:
npx hardhat test
transfer失败。transfer成功,余额正确更新。owner无法暂停,验证权限控制。用角色管理支持多用户控制暂停。
contracts/RoleBasedPause.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract RoleBasedPauseToken is ERC20, Pausable, AccessControl {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
event Pause(address indexed account);
event Unpause(address indexed account);
constructor() ERC20("RoleBasedPauseToken", "RBPT") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(PAUSER_ROLE, msg.sender);
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
super.transfer(to, amount);
}
function pause() public onlyRole(PAUSER_ROLE) {
_pause();
emit Pause(msg.sender);
}
function unpause() public onlyRole(PAUSER_ROLE) {
_unpause();
emit Unpause(msg.sender);
}
}
AccessControl,定义PAUSER_ROLE。pause/unpause:限制为PAUSER_ROLE,触发Pause/Unpause事件。transfer:用whenNotPaused保护。msg.sender管理员和暂停者角色。PAUSER_ROLE。onlyRole限制权限。test/PauseResume.test.ts(add):
import { RoleBasedPauseToken } from "../typechain-types";
describe("RoleBasedPause", function () {
let token: RoleBasedPauseToken;
let owner: any, pauser: any, user1: any;
beforeEach(async function () {
[owner, pauser, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("RoleBasedPauseToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.grantRole(await token.PAUSER_ROLE(), pauser.address);
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should allow pauser to pause", async function () {
await expect(token.connect(pauser).pause())
.to.emit(token, "Pause")
.withArgs(pauser.address);
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
});
it("should allow pauser to unpause", async function () {
await token.connect(pauser).pause();
await expect(token.connect(pauser).unpause())
.to.emit(token, "Unpause")
.withArgs(pauser.address);
await token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"));
expect(await token.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("999100"));
});
it("should restrict pause to pauser role", async function () {
await expect(token.connect(user1).pause()).to.be.revertedWith("AccessControl: account is missing role");
});
});
PAUSER_ROLE用户可暂停/恢复,触发事件。PAUSER_ROLE用户无法操作。transfer正常运行。为暂停加多签机制,需多人同意。
contracts/MultiSigPause.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MultiSigPauseToken is ERC20, Ownable, Pausable {
address[] public pausers;
uint256 public required;
uint256 public transactionCount;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
struct Transaction {
bool isPause;
bool executed;
uint256 confirmationCount;
}
event SubmitPause(uint256 indexed txId, bool isPause);
event ConfirmPause(uint256 indexed txId, address indexed pauser);
event ExecutePause(uint256 indexed txId, bool isPause);
event RevokeConfirmation(uint256 indexed txId, address indexed pauser);
event Pause(address indexed account);
event Unpause(address indexed account);
modifier onlyPauser() {
bool isPauser = false;
for (uint256 i = 0; i < pausers.length; i++) {
if (pausers[i] == msg.sender) {
isPauser = true;
break;
}
}
require(isPauser, "Not pauser");
_;
}
constructor(address[] memory _pausers, uint256 _required) ERC20("MultiSigPauseToken", "MSPT") Ownable() {
require(_pausers.length > 0, "Pausers required");
require(_required > 0 && _required <= _pausers.length, "Invalid required");
pausers = _pausers;
required = _required;
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
super.transfer(to, amount);
}
function submitPause(bool isPause) public onlyPauser {
uint256 txId = transactionCount++;
transactions[txId] = Transaction({
isPause: isPause,
executed: false,
confirmationCount: 0
});
emit SubmitPause(txId, isPause);
}
function confirmPause(uint256 txId) public onlyPauser {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(!confirmations[txId][msg.sender], "Already confirmed");
confirmations[txId][msg.sender] = true;
transaction.confirmationCount++;
emit ConfirmPause(txId, msg.sender);
if (transaction.confirmationCount >= required) {
executePause(txId);
}
}
function executePause(uint256 txId) internal {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(transaction.confirmationCount >= required, "Insufficient confirmations");
transaction.executed = true;
if (transaction.isPause) {
_pause();
emit Pause(msg.sender);
} else {
_unpause();
emit Unpause(msg.sender);
}
emit ExecutePause(txId, transaction.isPause);
}
function revokeConfirmation(uint256 txId) public onlyPauser {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(confirmations[txId][msg.sender], "Not confirmed");
confirmations[txId][msg.sender] = false;
transaction.confirmationCount--;
emit RevokeConfirmation(txId, msg.sender);
}
}
pausers和required控制多签。submitPause:提交暂停/恢复提案。confirmPause:确认提案,达标后执行。executePause:调用_pause或_unpause,触发事件。revokeConfirmation:撤销确认。test/PauseResume.test.ts(add):
import { MultiSigPauseToken } from "../typechain-types";
describe("MultiSigPause", function () {
let token: MultiSigPauseToken;
let owner: any, pauser1: any, pauser2: any, pauser3: any, user1: any;
beforeEach(async function () {
[owner, pauser1, pauser2, pauser3, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("MultiSigPauseToken");
token = await TokenFactory.deploy([pauser1.address, pauser2.address, pauser3.address], 2);
await token.deployed();
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should execute pause with multi-sig", async function () {
await token.connect(pauser1).submitPause(true);
await token.connect(pauser2).confirmPause(0);
await expect(token.connect(pauser3).confirmPause(0))
.to.emit(token, "Pause")
.withArgs(pauser3.address);
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
});
it("should execute unpause with multi-sig", async function () {
await token.connect(pauser1).submitPause(true);
await token.connect(pauser2).confirmPause(0);
await token.connect(pauser3).confirmPause(0);
await token.connect(pauser1).submitPause(false);
await token.connect(pauser2).confirmPause(1);
await expect(token.connect(pauser3).confirmPause(1))
.to.emit(token, "Unpause")
.withArgs(pauser3.address);
await token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"));
expect(await token.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("999100"));
});
it("should not execute without enough confirmations", async function () {
await token.connect(pauser1).submitPause(true);
await token.connect(pauser2).confirmPause(0);
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"))).to.not.be.reverted;
});
it("should allow revoking confirmation", async function () {
await token.connect(pauser1).submitPause(true);
await token.connect(pauser2).confirmPause(0);
await token.connect(pauser2).revokeConfirmation(0);
await token.connect(pauser3).confirmPause(0);
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"))).to.not.be.reverted;
});
});
transfer失败。transfer成功。根据特定条件自动暂停(如余额异常)。
contracts/ConditionalPause.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ConditionalPauseToken is ERC20, Ownable, Pausable {
uint256 public maxBalance = 100000 * 10**18; // Max 100k tokens per address
event Pause(address indexed account, string reason);
event Unpause(address indexed account);
constructor() ERC20("ConditionalPauseToken", "CPT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
super.transfer(to, amount);
if (balanceOf(to) > maxBalance) {
_pause();
emit Pause(msg.sender, "Max balance exceeded");
}
}
function pause() public onlyOwner {
_pause();
emit Pause(msg.sender, "Manual pause");
}
function unpause() public onlyOwner {
_unpause();
emit Unpause(msg.sender);
}
}
maxBalance限制单地址最大持仓。transfer:检查接收者余额,超限自动暂停。pause/unpause:手动控制,仅owner。Pause/Unpause事件,记录原因。onlyOwner限制手动操作。test/PauseResume.test.ts(add):
import { ConditionalPauseToken } from "../typechain-types";
describe("ConditionalPause", function () {
let token: ConditionalPauseToken;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("ConditionalPauseToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.transfer(user1.address, ethers.utils.parseEther("50000"));
});
it("should auto-pause on max balance", async function () {
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("60000")))
.to.emit(token, "Pause")
.withArgs(user1.address, "Max balance exceeded");
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
});
it("should allow manual pause and unpause", async function () {
await token.pause();
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
await token.unpause();
await token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"));
expect(await token.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("950100"));
});
});
maxBalance触发暂停。结合代理模式支持暂停。
contracts/UpgradablePause.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract UpgradablePauseToken is ERC20Upgradeable, PausableUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
event Pause(address indexed account);
event Unpause(address indexed account);
function initialize() public initializer {
__ERC20_init("UpgradablePauseToken", "UPT");
__Pausable_init();
__Ownable_init();
__UUPSUpgradeable_init();
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
super.transfer(to, amount);
}
function pause() public onlyOwner {
_pause();
emit Pause(msg.sender);
}
function unpause() public onlyOwner {
_unpause();
emit Unpause(msg.sender);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
contracts/UpgradablePauseV2.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract UpgradablePauseTokenV2 is ERC20Upgradeable, PausableUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
uint256 public transferFee = 1 * 10**16; // 1% fee
event Pause(address indexed account);
event Unpause(address indexed account);
function initialize() public initializer {
__ERC20_init("UpgradablePauseTokenV2", "UPTV2");
__Pausable_init();
__Ownable_init();
__UUPSUpgradeable_init();
_mint(msg.sender, 1000000 * 10**decimals());
}
function transfer(address to, uint256 amount) public virtual override whenNotPaused {
uint256 fee = (amount * transferFee) / 1e18;
super.transfer(to, amount - fee);
super.transfer(owner(), fee);
}
function pause() public onlyOwner {
_pause();
emit Pause(msg.sender);
}
function unpause() public onlyOwner {
_unpause();
emit Unpause(msg.sender);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
contracts/UUPSProxy.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {}
}
ERC20Upgradeable、PausableUpgradeable、UUPSUpgradeable。initialize:初始化代币和暂停状态。transfer:用whenNotPaused保护。pause/unpause:触发事件,仅owner。ERC1967Proxy,支持UUPS升级。UUPSUpgradeable确保升级安全。PausableUpgradeable支持暂停。test/PauseResume.test.ts(add):
import { UUPSProxy, UpgradablePauseToken, UpgradablePauseTokenV2 } from "../typechain-types";
describe("UpgradablePause", function () {
let proxy: UUPSProxy;
let token: UpgradablePauseToken;
let tokenV2: UpgradablePauseTokenV2;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("UpgradablePauseToken");
token = await TokenFactory.deploy();
await token.deployed();
const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
const initData = TokenFactory.interface.encodeFunctionData("initialize");
proxy = await ProxyFactory.deploy(token.address, initData);
await proxy.deployed();
const TokenV2Factory = await ethers.getContractFactory("UpgradablePauseTokenV2");
tokenV2 = await TokenV2Factory.deploy();
await tokenV2.deployed();
await (await ethers.getContractFactory("UpgradablePauseToken")).attach(proxy.address)
.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should pause and block transfers", async function () {
const proxyAsToken = await ethers.getContractFactory("UpgradablePauseToken").then(f => f.attach(proxy.address));
await proxyAsToken.pause();
await expect(proxyAsToken.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
});
it("should upgrade and retain pause functionality", async function () {
const proxyAsToken = await ethers.getContractFactory("UpgradablePauseToken").then(f => f.attach(proxy.address));
await proxyAsToken.pause();
await proxyAsToken.upgradeTo(tokenV2.address);
const proxyAsTokenV2 = await ethers.getContractFactory("UpgradablePauseTokenV2").then(f => f.attach(proxy.address));
await expect(proxyAsTokenV2.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Pausable: paused");
await proxyAsTokenV2.unpause();
await proxyAsTokenV2.connect(user1).transfer(owner.address, ethers.utils.parseEther("100"));
expect(await proxyAsTokenV2.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("999099")); // 1% fee
});
});
transfer失败。scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner, pauser1, pauser2, pauser3] = await ethers.getSigners();
const BasicPauseFactory = await ethers.getContractFactory("BasicPauseToken");
const basicPause = await BasicPauseFactory.deploy();
await basicPause.deployed();
console.log(`BasicPauseToken deployed to: ${basicPause.address}`);
const RoleBasedPauseFactory = await ethers.getContractFactory("RoleBasedPauseToken");
const roleBasedPause = await RoleBasedPauseFactory.deploy();
await roleBasedPause.deployed();
console.log(`RoleBasedPauseToken deployed to: ${roleBasedPause.address}`);
const MultiSigPauseFactory = await ethers.getContractFactory("MultiSigPauseToken");
const multiSigPause = await MultiSigPauseFactory.deploy([pauser1.address, pauser2.address, pauser3.address], 2);
await multiSigPause.deployed();
console.log(`MultiSigPauseToken deployed to: ${multiSigPause.address}`);
const ConditionalPauseFactory = await ethers.getContractFactory("ConditionalPauseToken");
const conditionalPause = await ConditionalPauseFactory.deploy();
await conditionalPause.deployed();
console.log(`ConditionalPauseToken deployed to: ${conditionalPause.address}`);
const UpgradablePauseFactory = await ethers.getContractFactory("UpgradablePauseToken");
const upgradablePause = await UpgradablePauseFactory.deploy();
await upgradablePause.deployed();
const initData = UpgradablePauseFactory.interface.encodeFunctionData("initialize");
const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
const proxy = await ProxyFactory.deploy(upgradablePause.address, initData);
await proxy.deployed();
console.log(`UpgradablePauseToken deployed to: ${upgradablePause.address}, Proxy: ${proxy.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat
pause/unpause为特定角色或多签。Pause/Unpause事件,记录原因。跑代码,体验Solidity暂停与恢复的硬核玩法吧!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!