今天咱们来扒一扒Solidity里那个让人又爱又恨的家伙——多重角色管理!在区块链上写合约,权限这事儿可不是小打小闹,搞不好一个函数谁都能调,资金就哗哗流走,项目直接黄摊子!多重角色就是给合约装上层层关卡,像俄罗斯套娃一样,外层管理员管内层,内层又分铸币、暂停、销毁这些小角色,权限细到毛孔里!
今天咱们来扒一扒Solidity里那个让人又爱又恨的家伙——多重角色管理!在区块链上写合约,权限这事儿可不是小打小闹,搞不好一个函数谁都能调,资金就哗哗流走,项目直接黄摊子!多重角色就是给合约装上层层关卡,像俄罗斯套娃一样,外层管理员管内层,内层又分铸币、暂停、销毁这些小角色,权限细到毛孔里!
权限管理在Solidity里,核心就是控制谁能调用哪些函数。EVM没内置权限,得靠代码实现,通常用修饰符(modifier)或条件检查。OpenZeppelin的AccessControl是主力,它基于keccak256哈希生成角色ID,存储在映射里。角色ID是bytes32类型,比如keccak256("MINTER_ROLE"),每个角色对应一个权限集合。管理员(DEFAULT_ADMIN_ROLE)能分配和撤销角色,子角色只能执行特定操作。
多重角色意味着一个合约继承多个权限机制,比如Ownable的单一owner,再加AccessControl的多角色,最后嵌套Pausable的暂停权限。继承顺序影响函数解析,同名函数得用override明确指定。存储变量得注意布局,避免冲突——Solidity按声明顺序分配槽位,父合约变量在前。
拿个简单例子,合约继承ERC20(代币)、Ownable(单一权限)和AccessControl(多角色),mint用MINTER_ROLE,pause用PAUSER_ROLE。构造函数授予msg.sender管理员角色,_setupRole设置权限。调用grantRole时,检查getRoleAdmin(role),确保上级角色授权。
Gas上,角色检查每调用加~2k Gas,事件记录再加~2k,但安全性值回票价。EVM里,映射存储用SLOAD读角色,SSTORE更新权限,成本固定。
工具得齐全,用Hardhat建环境,写合约和测试。
跑命令初始化:
mkdir role-control-demo
cd role-control-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers
npx hardhat init
选TypeScript,装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录长这样:
role-control-demo/
├── contracts/
│ ├── SimpleRole.sol
│ ├── MultiRole.sol
│ ├── NestedRole.sol
│ ├── MultiSigRole.sol
│ ├── UpgradableRole.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── RoleControl.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
环境就位,接下来直接上代码!
权限管理从最简单的Ownable开始,这货就是给合约装个单一锁,只有owner能开。
contracts/SimpleRole.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract SimpleRoleToken is ERC20, Ownable {
constructor() ERC20("SimpleRoleToken", "SRT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function transferOwnership(address newOwner) public override onlyOwner {
super.transferOwnership(newOwner);
}
}
ERC20:代币转账功能。Ownable:onlyOwner修饰符锁定mint。mint:owner铸币给任意地址。transferOwnership:覆盖,移交权限。msg.sender是owner,初始铸100万代币。onlyOwner确保只有owner能铸币。Ownable自带renounceOwnership,可放弃权限。transferOwnership允许移交。owner私钥丢了就完蛋。mint:~30k Gas(SSTORE余额)。transferOwnership:~20k Gas。test/RoleControl.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { SimpleRoleToken } from "../typechain-types";
describe("SimpleRole", function () {
let token: SimpleRoleToken;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("SimpleRoleToken");
token = await TokenFactory.deploy();
await token.deployed();
});
it("lets owner mint tokens", async function () {
await token.mint(user1.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
});
it("blocks non-owner from minting", async function () {
await expect(token.connect(user1).mint(user1.address, ethers.utils.parseEther("500")))
.to.be.revertedWith("Ownable: caller is not the owner");
});
it("lets owner transfer ownership", async function () {
await token.transferOwnership(user1.address);
expect(await token.owner()).to.equal(user1.address);
await token.connect(user1).mint(user1.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
});
it("blocks non-owner from transferring ownership", async function () {
await expect(token.connect(user1).transferOwnership(user1.address))
.to.be.revertedWith("Ownable: caller is not the owner");
});
});
跑测试:
npx hardhat test
owner铸币成功,余额更新。owner调用mint或transferOwnership直接报错。owner能铸币。Ownable简单,但权限太集中,得加多角色。单一owner不保险,搞AccessControl分角色,铸币、暂停、销毁各管各的。
contracts/RoleAccess.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract RoleAccessToken is ERC20, AccessControl, ReentrancyGuard {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bool public paused;
event Minted(address indexed to, uint256 amount);
event Burned(address indexed from, uint256 amount);
event Paused(address indexed account);
event Unpaused(address indexed account);
constructor() ERC20("RoleAccessToken", "RAT") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
_setupRole(BURNER_ROLE, msg.sender);
_setupRole(PAUSER_ROLE, msg.sender);
_mint(msg.sender, 1000000 * 10**decimals());
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) nonReentrant {
_mint(to, amount);
emit Minted(to, amount);
}
function burn(uint256 amount) public onlyRole(BURNER_ROLE) nonReentrant {
_burn(msg.sender, amount);
emit Burned(msg.sender, amount);
}
function transfer(address to, uint256 amount) public override returns (bool) {
require(!paused, "Transfers paused");
return super.transfer(to, amount);
}
function pause() public onlyRole(PAUSER_ROLE) {
paused = true;
emit Paused(msg.sender);
}
function unpause() public onlyRole(PAUSER_ROLE) {
paused = false;
emit Unpaused(msg.sender);
}
}
ERC20、AccessControl、ReentrancyGuard。MINTER_ROLE铸币、BURNER_ROLE销毁、PAUSER_ROLE暂停。mint/burn:角色控制,防重入。transfer:检查暂停状态。pause/unpause:角色控制,触发事件。msg.sender拿所有角色。nonReentrant防外部调用漏洞。mint:~35k Gas(含事件)。pause:~5k Gas。test/RoleControl.test.ts(add):
import { RoleAccessToken } from "../typechain-types";
describe("RoleAccess", function () {
let token: RoleAccessToken;
let owner: any, minter: any, burner: any, pauser: any, user1: any;
beforeEach(async function () {
[owner, minter, burner, pauser, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("RoleAccessToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.grantRole(await token.MINTER_ROLE(), minter.address);
await token.grantRole(await token.BURNER_ROLE(), burner.address);
await token.grantRole(await token.PAUSER_ROLE(), pauser.address);
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("lets minter mint tokens", async function () {
await expect(token.connect(minter).mint(user1.address, ethers.utils.parseEther("500")))
.to.emit(token, "Minted")
.withArgs(user1.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("1500"));
});
it("lets burner burn tokens", async function () {
await expect(token.connect(burner).burn(ethers.utils.parseEther("500")))
.to.emit(token, "Burned")
.withArgs(burner.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(burner.address)).to.equal(0);
});
it("lets pauser pause transfers", async function () {
await expect(token.connect(pauser).pause())
.to.emit(token, "Paused")
.withArgs(pauser.address);
await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("100")))
.to.be.revertedWith("Transfers paused");
});
it("blocks unauthorized calls", async function () {
await expect(token.connect(user1).mint(user1.address, ethers.utils.parseEther("500")))
.to.be.revertedWith("AccessControl: account is missing role");
await expect(token.connect(user1).burn(ethers.utils.parseEther("500")))
.to.be.revertedWith("AccessControl: account is missing role");
await expect(token.connect(user1).pause())
.to.be.revertedWith("AccessControl: account is missing role");
});
});
多角色还不够,搞嵌套权限,超级管理员管普通管理员,普通管理员管具体角色。
contracts/NestedRoleAccess.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract NestedRoleAccessToken is ERC20, AccessControl {
bytes32 public constant SUPER_ADMIN_ROLE = keccak256("SUPER_ADMIN_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
event RoleGranted(address indexed account, bytes32 indexed role, address indexed granter);
event RoleRevoked(address indexed account, bytes32 indexed role, address indexed revoker);
event Minted(address indexed to, uint256 amount);
event Burned(address indexed from, uint256 amount);
constructor() ERC20("NestedRoleAccessToken", "NRAT") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(SUPER_ADMIN_ROLE, msg.sender);
_setRoleAdmin(ADMIN_ROLE, SUPER_ADMIN_ROLE);
_setRoleAdmin(MINTER_ROLE, ADMIN_ROLE);
_setRoleAdmin(BURNER_ROLE, ADMIN_ROLE);
_mint(msg.sender, 1000000 * 10**decimals());
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
emit Minted(to, amount);
}
function burn(uint256 amount) public onlyRole(BURNER_ROLE) {
_burn(msg.sender, amount);
emit Burned(msg.sender, amount);
}
function grantRole(bytes32 role, address account) public override {
require(getRoleAdmin(role) == SUPER_ADMIN_ROLE || getRoleAdmin(role) == ADMIN_ROLE, "Invalid admin role");
_grantRole(role, account);
emit RoleGranted(account, role, msg.sender);
}
function revokeRole(bytes32 role, address account) public override {
require(getRoleAdmin(role) == SUPER_ADMIN_ROLE || getRoleAdmin(role) == ADMIN_ROLE, "Invalid admin role");
_revokeRole(role, account);
emit RoleRevoked(account, role, msg.sender);
}
}
SUPER_ADMIN_ROLE、ADMIN_ROLE、MINTER_ROLE、BURNER_ROLE。_setRoleAdmin设置层级:SUPER_ADMIN_ROLE管ADMIN_ROLE,ADMIN_ROLE管MINTER_ROLE和BURNER_ROLE。grantRole/revokeRole:覆盖,检查上级角色。mint/burn:底层角色控制。msg.sender拿超级管理员。mint:~35k Gas。test/RoleControl.test.ts(add):
import { NestedRoleAccessToken } from "../typechain-types";
describe("NestedRoleAccess", function () {
let token: NestedRoleAccessToken;
let owner: any, superAdmin: any, admin: any, minter: any, burner: any;
beforeEach(async function () {
[owner, superAdmin, admin, minter, burner] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("NestedRoleAccessToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.grantRole(await token.SUPER_ADMIN_ROLE(), superAdmin.address);
});
it("lets super admin grant admin role", async function () {
await expect(token.connect(superAdmin).grantRole(await token.ADMIN_ROLE(), admin.address))
.to.emit(token, "RoleGranted")
.withArgs(admin.address, await token.ADMIN_ROLE(), superAdmin.address);
await expect(token.connect(admin).grantRole(await token.MINTER_ROLE(), minter.address))
.to.emit(token, "RoleGranted")
.withArgs(minter.address, await token.MINTER_ROLE(), admin.address);
await token.connect(minter).mint(minter.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(minter.address)).to.equal(ethers.utils.parseEther("500"));
});
it("lets admin revoke roles", async function () {
await token.connect(superAdmin).grantRole(await token.ADMIN_ROLE(), admin.address);
await token.connect(admin).grantRole(await token.BURNER_ROLE(), burner.address);
await token.transfer(burner.address, ethers.utils.parseEther("1000"));
await token.connect(admin).revokeRole(await token.BURNER_ROLE(), burner.address);
await expect(token.connect(burner).burn(ethers.utils.parseEther("500")))
.to.be.revertedWith("AccessControl: account is missing role");
});
it("blocks unauthorized role grants", async function () {
await expect(token.connect(minter).grantRole(await token.ADMIN_ROLE(), admin.address))
.to.be.revertedWith("AccessControl: account is missing role");
await token.connect(superAdmin).grantRole(await token.ADMIN_ROLE(), admin.address);
await expect(token.connect(admin).grantRole(await token.SUPER_ADMIN_ROLE(), superAdmin.address))
.to.be.revertedWith("Invalid admin role");
});
});
SUPER_ADMIN_ROLE分配ADMIN_ROLE,ADMIN_ROLE分配MINTER_ROLE。ADMIN_ROLE撤销角色,权限移除。关键角色分配用多签,防止单人失误。
contracts/MultiSigRoleAccess.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MultiSigRoleAccessToken is ERC20, AccessControl, ReentrancyGuard {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
address[] public signers;
uint256 public required;
uint256 public transactionCount;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
struct Transaction {
bytes32 role;
address account;
bool executed;
uint256 confirmationCount;
}
event SubmitRoleGrant(uint256 indexed txId, bytes32 indexed role, address indexed account);
event ConfirmRoleGrant(uint256 indexed txId, address indexed signer);
event ExecuteRoleGrant(uint256 indexed txId, bytes32 indexed role, address indexed account);
event RevokeConfirmation(uint256 indexed txId, address indexed signer);
modifier onlySigner() {
bool isSigner = false;
for (uint256 i = 0; i < signers.length; i++) {
if (signers[i] == msg.sender) {
isSigner = true;
break;
}
}
require(isSigner, "Not signer");
_;
}
constructor(address[] memory _signers, uint256 _required) ERC20("MultiSigRoleAccessToken", "MSRAT") {
require(_signers.length > 0, "Signers required");
require(_required > 0 && _required <= _signers.length, "Invalid required");
signers = _signers;
required = _required;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_mint(msg.sender, 1000000 * 10**decimals());
}
function submitRoleGrant(bytes32 role, address account) public onlySigner {
uint256 txId = transactionCount++;
transactions[txId] = Transaction({
role: role,
account: account,
executed: false,
confirmationCount: 0
});
emit SubmitRoleGrant(txId, role, account);
}
function confirmRoleGrant(uint256 txId) public onlySigner nonReentrant {
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 ConfirmRoleGrant(txId, msg.sender);
if (transaction.confirmationCount >= required) {
executeRoleGrant(txId);
}
}
function executeRoleGrant(uint256 txId) internal {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(transaction.confirmationCount >= required, "Insufficient confirmations");
transaction.executed = true;
_grantRole(transaction.role, transaction.account);
emit ExecuteRoleGrant(txId, transaction.role, transaction.account);
}
function revokeConfirmation(uint256 txId) public onlySigner {
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);
}
}
signers和required定义多签成员和确认数。submitRoleGrant:提交角色分配提案。confirmRoleGrant:确认提案,够票后执行。executeRoleGrant:调用_grantRole分配角色。revokeConfirmation:撤销确认。nonReentrant保护确认。test/RoleControl.test.ts(add):
import { MultiSigRoleAccessToken } from "../typechain-types";
describe("MultiSigRoleAccess", function () {
let token: MultiSigRoleAccessToken;
let owner: any, signer1: any, signer2: any, signer3: any, user1: any;
beforeEach(async function () {
[owner, signer1, signer2, signer3, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("MultiSigRoleAccessToken");
token = await TokenFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
await token.deployed();
});
it("executes role grant with multi-sig", async function () {
const MINTER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MINTER_ROLE"));
await token.connect(signer1).submitRoleGrant(MINTER_ROLE, user1.address);
await token.connect(signer2).confirmRoleGrant(0);
await expect(token.connect(signer3).confirmRoleGrant(0))
.to.emit(token, "ExecuteRoleGrant")
.withArgs(0, MINTER_ROLE, user1.address);
await token.connect(user1).mint(user1.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
});
it("blocks role grant without enough signatures", async function () {
const MINTER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MINTER_ROLE"));
await token.connect(signer1).submitRoleGrant(MINTER_ROLE, user1.address);
await token.connect(signer2).confirmRoleGrant(0);
expect(await token.balanceOf(user1.address)).to.equal(0);
});
it("allows revoking confirmation", async function () {
const MINTER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MINTER_ROLE"));
await token.connect(signer1).submitRoleGrant(MINTER_ROLE, user1.address);
await token.connect(signer2).confirmRoleGrant(0);
await token.connect(signer2).revokeConfirmation(0);
await token.connect(signer3).confirmRoleGrant(0);
expect(await token.balanceOf(user1.address)).to.equal(0);
});
});
scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner, signer1, signer2, signer3] = await ethers.getSigners();
const SimpleRoleFactory = await ethers.getContractFactory("SimpleRoleToken");
const simpleRole = await SimpleRoleFactory.deploy();
await simpleRole.deployed();
console.log(`SimpleRoleToken deployed to: ${simpleRole.address}`);
const RoleAccessFactory = await ethers.getContractFactory("RoleAccessToken");
const roleAccess = await RoleAccessFactory.deploy();
await roleAccess.deployed();
console.log(`RoleAccessToken deployed to: ${roleAccess.address}`);
const NestedRoleAccessFactory = await ethers.getContractFactory("NestedRoleAccessToken");
const nestedRoleAccess = await NestedRoleAccessFactory.deploy();
await nestedRoleAccess.deployed();
console.log(`NestedRoleAccessToken deployed to: ${nestedRoleAccess.address}`);
const MultiSigRoleAccessFactory = await ethers.getContractFactory("MultiSigRoleAccessToken");
const multiSigRoleAccess = await MultiSigRoleAccessFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
await multiSigRoleAccess.deployed();
console.log(`MultiSigRoleAccessToken deployed to: ${multiSigRoleAccess.address}`);
const UpgradableAccessFactory = await ethers.getContractFactory("UpgradableAccessToken");
const upgradableAccess = await UpgradableAccessFactory.deploy();
await upgradableAccess.deployed();
const initData = UpgradableAccessFactory.interface.encodeFunctionData("initialize");
const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
const proxy = await ProxyFactory.deploy(upgradableAccess.address, initData);
await proxy.deployed();
console.log(`UpgradableAccessToken deployed to: ${upgradableAccess.address}, Proxy: ${proxy.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat
onlyRole或onlySigner。nonReentrant。如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!