Solidity权限管理:让你的合约像铁门一样锁得死死的

Solidity合约的权限管理!在区块链世界里,智能合约就是你的金库,里面装着资金、数据,啥啥都值钱!要是权限没管好,等于把金库大门敞开,喊着“来抢吧”!权限管理就是给合约装上金刚锁,决定谁能开锁、谁能动钱、谁能改规则。权限管理的硬核逻辑先把基本功打扎实,权限管理是啥?简单说,就是控制谁能调用合

Solidity合约的权限管理!在区块链世界里,智能合约就是你的金库,里面装着资金、数据,啥啥都值钱!要是权限没管好,等于把金库大门敞开,喊着“来抢吧”!权限管理就是给合约装上金刚锁,决定谁能开锁、谁能动钱、谁能改规则。

权限管理的硬核逻辑

先把基本功打扎实,权限管理是啥?简单说,就是控制谁能调用合约里的关键函数。比如,谁能铸新代币?谁能暂停交易?谁能升级合约逻辑?在Solidity里,权限管理得靠代码实现,EVM可不会帮你管门。咱们得搞清楚几个重点:

  • 权限类型:可以是单一管理员(如Ownableowner),也可以是多角色(如AccessControl的角色),还能是多签(多个人同意才行)。
  • 核心问题
    • 权限集中:单人控制风险高,私钥丢了就GG。
    • 权限分散:多人或多角色管理,分配得不好容易乱套。
    • 外部调用:权限函数涉及转账或调用,可能被重入攻击。
    • 链上追踪:没事件记录,查谁干了啥都费劲。
  • 工具箱
    • Solidity 0.8.20:安全、稳定,支持修饰符。
    • OpenZeppelin:现成的OwnableAccessControlReentrancyGuard库。
    • Hardhat:测试环境,模拟各种场景。
  • EVM特性
    • 权限靠修饰符(如onlyOwner)或条件检查实现。
    • 状态变量(如角色映射)得注意存储布局。
  • 事件:每次权限操作都要发事件,方便链上查日志。

开发环境搭起来

先把工具准备好,用Hardhat建个开发环境,方便写代码和测试。

跑这些命令,初始化项目:

mkdir access-control-pro
cd access-control-pro
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers

初始化Hardhat,选TypeScript模板:

npx hardhat init

装点依赖,跑测试用:

npm install --save-dev ts-node typescript @types/node @types/mocha

项目目录长这样:

access-control-pro/
├── contracts/
│   ├── SimpleAccess.sol
│   ├── RoleAccess.sol
│   ├── MultiSigAccess.sol
│   ├── NestedRoleAccess.sol
│   ├── UpgradableAccess.sol
├── scripts/
│   ├── deploy.ts
├── test/
│   ├── AccessPro.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

环境OK,接下来直接上代码!

单人权限:简单粗暴的Ownable

先从最简单的开始,用OpenZeppelin的Ownable搞个单一管理员权限。

代码实操

contracts/SimpleAccess.sol

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SimpleAccessToken is ERC20, Ownable {
    constructor() ERC20("SimpleAccessToken", "SAT") Ownable() {
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function pauseTransfers() public onlyOwner {
        _pause();
    }

    function _pause() internal {
        // Simulate pause logic (e.g., set a flag)
    }
}

代码解析

  • 核心逻辑
    • 继承ERC20:搞个标准代币,能转账、查余额。
    • 继承Ownable:只有owner能调用关键函数。
    • mintowner可以给任意地址铸新代币。
    • pauseTransfers:模拟暂停转账,onlyOwner限制。
    • 构造函数:部署者是owner,初始铸100万代币。
  • 安全点
    • onlyOwner修饰符确保只有owner能操作。
    • Ownable自带transferOwnershiprenounceOwnership,方便权限移交。
  • 潜在问题
    • 单人控制风险大,owner私钥丢了或被黑,合约就危险。
    • 没事件记录,链上查操作日志不方便。
  • Gas成本
    • 部署:~500k Gas。
    • mint:~30k Gas。
    • pauseTransfers:~5k Gas(简单逻辑)。

测试一把

test/AccessPro.test.ts

import { ethers } from "hardhat";
import { expect } from "chai";
import { SimpleAccessToken } from "../typechain-types";

describe("SimpleAccess", function () {
  let token: SimpleAccessToken;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("SimpleAccessToken");
    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 pause transfers", async function () {
    await token.pauseTransfers(); // No revert, owner can call
  });

  it("blocks non-owner from pausing", async function () {
    await expect(token.connect(user1).pauseTransfers())
      .to.be.revertedWith("Ownable: caller is not the owner");
  });
});

跑测试:

npx hardhat test
  • 测试解析
    • owner能铸币,余额更新正确。
    • owner调用mintpauseTransfers直接报错。
    • 验证了onlyOwner的权限控制。
  • 问题:单人权限太集中,得升级到多角色。

多角色权限:AccessControl上场

单一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 PAUSER_ROLE = keccak256("PAUSER_ROLE");

    bool public paused;

    event Minted(address indexed to, 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(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 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);
    }
}

代码解析

  • 核心逻辑
    • 继承ERC20AccessControlReentrancyGuard
    • 定义MINTER_ROLEPAUSER_ROLE
    • mint:只有MINTER_ROLE能铸币,加nonReentrant防重入。
    • transfer:覆盖,检查暂停状态。
    • pause/unpausePAUSER_ROLE控制,触发事件。
    • 构造函数:msg.sender拿管理员和两个角色。
  • 安全点
    • 角色分离,铸币和暂停权限分开。
    • nonReentrant防止外部调用漏洞。
    • 事件记录关键操作,链上可查。
  • 潜在问题
    • 角色分配得手动管理,管理员得靠谱。
    • 暂停逻辑简单,可能需更复杂条件。
  • Gas成本
    • 部署:~600k Gas。
    • mint:~35k Gas(含事件)。
    • pause/~5k Gas。

测试一把

test/AccessPro.test.ts(add):

import { RoleAccessToken } from "../typechain-types";

describe("RoleAccess", function () {
  let token: RoleAccessToken;
  let owner: any, minter: any, pauser: any, user1: any;

  beforeEach(async function () {
    [owner, minter, 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.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 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("lets pauser unpause transfers", async function () {
    await token.connect(pauser).pause();
    await expect(token.connect(pauser).unpause())
      .to.emit(token, "Unpaused")
      .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("blocks non-role users", 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).pause())
      .to.be.revertedWith("AccessControl: account is missing role");
  });
});
  • 测试解析
    • MINTER_ROLE铸币成功,事件触发。
    • PAUSER_ROLE能暂停和恢复转账。
    • 非角色用户调用失败,权限锁死。
  • 优势:角色管理让权限更细化,适合团队。

多签权限:团队说了算

单人或单角色还是有风险,搞个多签机制,关键操作得多人同意。

代码实操

contracts/MultiSigAccess.sol

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MultiSigAccessToken is ERC20, Ownable, ReentrancyGuard {
    address[] public signers;
    uint256 public required;
    uint256 public transactionCount;
    mapping(uint256 => Transaction) public transactions;
    mapping(uint256 => mapping(address => bool)) public confirmations;

    struct Transaction {
        address to;
        uint256 amount;
        bool executed;
        uint256 confirmationCount;
    }

    event SubmitMint(uint256 indexed txId, address indexed to, uint256 amount);
    event ConfirmMint(uint256 indexed txId, address indexed signer);
    event ExecuteMint(uint256 indexed txId, address indexed to, uint256 amount);
    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("MultiSigAccessToken", "MSAT") Ownable() {
        require(_signers.length > 0, "Signers required");
        require(_required > 0 && _required <= _signers.length, "Invalid required");
        signers = _signers;
        required = _required;
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function submitMint(address to, uint256 amount) public onlySigner {
        uint256 txId = transactionCount++;
        transactions[txId] = Transaction({
            to: to,
            amount: amount,
            executed: false,
            confirmationCount: 0
        });
        emit SubmitMint(txId, to, amount);
    }

    function confirmMint(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 ConfirmMint(txId, msg.sender);
        if (transaction.confirmationCount >= required) {
            executeMint(txId);
        }
    }

    function executeMint(uint256 txId) internal {
        Transaction storage transaction = transactions[txId];
        require(!transaction.executed, "Transaction executed");
        require(transaction.confirmationCount >= required, "Not enough confirmations");
        transaction.executed = true;
        _mint(transaction.to, transaction.amount);
        emit ExecuteMint(txId, transaction.to, transaction.amount);
    }

    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);
    }
}

代码解析

  • 核心逻辑
    • signersrequired定义多签成员和最低确认数。
    • submitMintsigner提交铸币提案。
    • confirmMintsigner确认,够票后自动执行。
    • executeMint:铸币,触发事件。
    • revokeConfirmation:撤销确认。
  • 安全点
    • 多签机制防单人失误或恶意操作。
    • nonReentrant保护确认逻辑。
    • 事件记录提案、确认、执行。
  • 潜在问题
    • 多签成员得可信,勾结可能导致问题。
    • Gas成本高,需权衡。
  • Gas成本
    • 部署:~700k Gas。
    • 提交/确认:~10k Gas/次。
    • 执行:~30k Gas。

测试一把

test/AccessPro.test.ts(add):

import { MultiSigAccessToken } from "../typechain-types";

describe("MultiSigAccess", function () {
  let token: MultiSigAccessToken;
  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("MultiSigAccessToken");
    token = await TokenFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
    await token.deployed();
  });

  it("executes mint with enough signatures", async function () {
    await token.connect(signer1).submitMint(user1.address, ethers.utils.parseEther("1000"));
    await token.connect(signer2).confirmMint(0);
    await expect(token.connect(signer3).confirmMint(0))
      .to.emit(token, "ExecuteMint")
      .withArgs(0, user1.address, ethers.utils.parseEther("1000"));
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("1000"));
  });

  it("blocks mint without enough signatures", async function () {
    await token.connect(signer1).submitMint(user1.address, ethers.utils.parseEther("1000"));
    await token.connect(signer2).confirmMint(0);
    expect(await token.balanceOf(user1.address)).to.equal(0);
  });

  it("allows revoking confirmation", async function () {
    await token.connect(signer1).submitMint(user1.address, ethers.utils.parseEther("1000"));
    await token.connect(signer2).confirmMint(0);
    await token.connect(signer2).revokeConfirmation(0);
    await token.connect(signer3).confirmMint(0);
    expect(await token.balanceOf(user1.address)).to.equal(0);
  });
});
  • 测试解析
    • 2/3签名后铸币成功。
    • 单签名不触发铸币。
    • 撤销确认阻止执行。
  • 优势:多签分散风险,适合团队管理。

嵌套角色权限:权限分层

多角色还不够,搞个嵌套权限,管理员能管角色分配。

代码实操

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 MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    bool public paused;

    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 Paused(address indexed account);
    event Unpaused(address indexed account);

    constructor() ERC20("NestedRoleAccessToken", "NRAT") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(SUPER_ADMIN_ROLE, msg.sender);
        _setRoleAdmin(MINTER_ROLE, SUPER_ADMIN_ROLE);
        _setRoleAdmin(PAUSER_ROLE, SUPER_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 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);
    }

    function grantRole(bytes32 role, address account) public override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
        emit RoleGranted(account, role, msg.sender);
    }

    function revokeRole(bytes32 role, address account) public override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
        emit RoleRevoked(account, role, msg.sender);
    }
}

代码解析

  • 核心逻辑
    • 定义SUPER_ADMIN_ROLEMINTER_ROLEPAUSER_ROLE
    • SUPER_ADMIN_ROLE可分配/撤销MINTER_ROLEPAUSER_ROLE
    • mint/pause/unpause:角色控制,触发事件。
    • grantRole/revokeRole:覆盖,添加事件。
  • 安全点
    • 嵌套权限让SUPER_ADMIN_ROLE控制角色分配。
    • 事件记录角色变更和操作。
  • 潜在问题
    • SUPER_ADMIN_ROLE得可信,权限集中风险仍存。
  • Gas成本
    • 部署:~650k Gas。
    • 角色分配:~20k Gas。
    • mint:~35k Gas。

测试一把

test/AccessPro.test.ts(add):

import { NestedRoleAccessToken } from "../typechain-types";

describe("NestedRoleAccess", function () {
  let token: NestedRoleAccessToken;
  let owner: any, superAdmin: any, minter: any, pauser: any;

  beforeEach(async function () {
    [owner, superAdmin, minter, pauser] = 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 roles", async function () {
    await expect(token.connect(superAdmin).grantRole(await token.MINTER_ROLE(), minter.address))
      .to.emit(token, "RoleGranted")
      .withArgs(minter.address, await token.MINTER_ROLE(), superAdmin.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 super admin revoke roles", async function () {
    await token.connect(superAdmin).grantRole(await token.PAUSER_ROLE(), pauser.address);
    await token.connect(superAdmin).revokeRole(await token.PAUSER_ROLE(), pauser.address);
    await expect(token.connect(pauser).pause())
      .to.be.revertedWith("AccessControl: account is missing role");
  });

  it("blocks non-super admin from granting roles", async function () {
    await expect(token.connect(minter).grantRole(await token.MINTER_ROLE(), minter.address))
      .to.be.revertedWith("AccessControl: account is missing role");
  });
});
  • 测试解析
    • SUPER_ADMIN_ROLE能分配/撤销角色。
    • MINTER_ROLE铸币成功。
    • 非管理员无法分配角色。
  • 优势:嵌套权限适合复杂项目,管理更灵活。

可升级权限:带上UUPS

权限固定死了不灵活,搞个可升级合约,权限也能升级。

代码实操

contracts/UpgradableAccess.sol

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

contract UpgradableAccessToken is ERC20Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    bool public paused;

    event Minted(address indexed to, uint256 amount);
    event Paused(address indexed account);
    event Unpaused(address indexed account);

    function initialize() public initializer {
        __ERC20_init("UpgradableAccessToken", "UAT");
        __AccessControl_init();
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init();
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_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 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);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}

contracts/UpgradableAccessV2.sol

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

contract UpgradableAccessTokenV2 is ERC20Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    bool public paused;
    uint256 public mintFee = 1 * 10**16; // 1% fee

    event Minted(address indexed to, uint256 amount);
    event Paused(address indexed account);
    event Unpaused(address indexed account);

    function initialize() public initializer {
        __ERC20_init("UpgradableAccessTokenV2", "UATV2");
        __AccessControl_init();
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init();
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_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 {
        uint256 fee = (amount * mintFee) / 1e18;
        _mint(to, amount - fee);
        _mint(msg.sender, fee);
        emit Minted(to, amount - fee);
    }

    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);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}

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) {}
}

代码解析

  • 核心逻辑
    • UpgradableAccessToken:继承升级版库,支持代币、角色、UUPS代理。
    • initialize:初始化代币、角色、代理。
    • mint/transfer/pause:角色控制,防重入。
    • _authorizeUpgradeDEFAULT_ADMIN_ROLE控制升级。
    • UpgradableAccessTokenV2:加1%铸币费,保持存储布局。
  • 安全点
    • UUPS确保升级安全。
    • 角色管理不变,权限稳定。
    • 事件记录操作。
  • 潜在问题
    • 升级需保证存储一致。
    • 管理员权限需谨慎。
  • Gas成本
    • 部署:~800k Gas(含代理)。
    • 升级:~20k Gas。
    • mint:~40k Gas(含费)。

测试一把

test/AccessPro.test.ts(add):

import { UUPSProxy, UpgradableAccessToken, UpgradableAccessTokenV2 } from "../typechain-types";

describe("UpgradableAccess", function () {
  let proxy: UUPSProxy;
  let token: UpgradableAccessToken;
  let tokenV2: UpgradableAccessTokenV2;
  let owner: any, minter: any;

  beforeEach(async function () {
    [owner, minter] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("UpgradableAccessToken");
    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("UpgradableAccessTokenV2");
    tokenV2 = await TokenV2Factory.deploy();
    await tokenV2.deployed();

    const proxyAsToken = await ethers.getContractFactory("UpgradableAccessToken").then(f => f.attach(proxy.address));
    await proxyAsToken.grantRole(await proxyAsToken.MINTER_ROLE(), minter.address);
  });

  it("lets minter mint tokens", async function () {
    const proxyAsToken = await ethers.getContractFactory("UpgradableAccessToken").then(f => f.attach(proxy.address));
    await expect(proxyAsToken.connect(minter).mint(minter.address, ethers.utils.parseEther("1000")))
      .to.emit(proxyAsToken, "Minted")
      .withArgs(minter.address, ethers.utils.parseEther("1000"));
    expect(await proxyAsToken.balanceOf(minter.address)).to.equal(ethers.utils.parseEther("1000"));
  });

  it("upgrades and applies mint fee", async function () {
    const proxyAsToken = await ethers.getContractFactory("UpgradableAccessToken").then(f => f.attach(proxy.address));
    await proxyAsToken.upgradeTo(tokenV2.address);
    const proxyAsTokenV2 = await ethers.getContractFactory("UpgradableAccessTokenV2").then(f => f.attach(proxy.address));
    await proxyAsTokenV2.connect(minter).mint(minter.address, ethers.utils.parseEther("1000"));
    expect(await proxyAsTokenV2.balanceOf(minter.address)).to.equal(ethers.utils.parseEther("990"));
    expect(await proxyAsTokenV2.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("1000010"));
  });
});
  • 测试解析
    • MINTER_ROLE铸币成功。
    • 升级后加1%铸币费,权限保持。
  • 优势:权限可随功能升级,灵活性拉满。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  const [owner, signer1, signer2, signer3] = await ethers.getSigners();

  const SimpleAccessFactory = await ethers.getContractFactory("SimpleAccessToken");
  const simpleAccess = await SimpleAccessFactory.deploy();
  await simpleAccess.deployed();
  console.log(`SimpleAccessToken deployed to: ${simpleAccess.address}`);

  const RoleAccessFactory = await ethers.getContractFactory("RoleAccessToken");
  const roleAccess = await RoleAccessFactory.deploy();
  await roleAccess.deployed();
  console.log(`RoleAccessToken deployed to: ${roleAccess.address}`);

  const MultiSigAccessFactory = await ethers.getContractFactory("MultiSigAccessToken");
  const multiSigAccess = await MultiSigAccessFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
  await multiSigAccess.deployed();
  console.log(`MultiSigAccessToken deployed to: ${multiSigAccess.address}`);

  const NestedRoleAccessFactory = await ethers.getContractFactory("NestedRoleAccessToken");
  const nestedRoleAccess = await NestedRoleAccessFactory.deploy();
  await nestedRoleAccess.deployed();
  console.log(`NestedRoleAccessToken deployed to: ${nestedRoleAccess.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
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!