【solidity 进阶】合约的升级方式及核心原理

solidity智能合约一旦部署便不可更改,但是通过特定的设计觅食可以实现“升级”的效果。

solidity 智能合约一旦部署便不可更改,但是通过特定的设计觅食可以实现“升级”的效果。以下是主流的合约升级方式以及核心原理解析。

一、合约的迁移

核心思想是:这种情况一般是废弃旧合约,将旧合约的功能和数据迁移到新的合约。 缺点:迁移状态数据可能非常昂贵(Gas费用高),尤其是状态变量很多的情况下。

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

// ======================
// 旧合约 (V1)
// ======================
contract OldToken {
    string public name = "OldToken";
    string public symbol = "OTK";
    uint8 public decimals = 18;

    mapping(address => uint256) private _balances;
    uint256 private _totalSupply;

    address public owner;
    bool public paused;

    event Transfer(address indexed from, address indexed to, uint256 value);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor(uint256 initialSupply) {
        owner = msg.sender;
        _mint(msg.sender, initialSupply);
    }

    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(!paused, "Contract paused");
        _transfer(msg.sender, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "Transfer from zero");
        require(to != address(0), "Transfer to zero");
        require(_balances[from] >= amount, "Insufficient balance");

        _balances[from] -= amount;
        _balances[to] += amount;
        emit Transfer(from, to, amount);
    }

    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "Mint to zero");
        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }
}

// ======================
// 新合约 (V2)
// ======================
contract NewToken {
    string public name = "NewToken";
    string public symbol = "NTK";
    uint8 public decimals = 18;

    mapping(address => uint256) private _balances;
    uint256 private _totalSupply;

    address public owner;
    bool public paused;

    // 新增功能:交易手续费
    uint256 public transferFee = 10; // 0.1% 手续费 (10 = 0.1%)
    address public feeCollector;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event FeeCollected(address indexed from, uint256 amount);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        feeCollector = msg.sender;
    }

    // 迁移后初始化的函数(由迁移合约调用)
    function initializeAfterMigration(
        address[] calldata accounts,
        uint256[] calldata balances,
        uint256 totalSupply_
    ) external onlyOwner {
        require(_totalSupply == 0, "Already initialized");

        _totalSupply = totalSupply_;
        for (uint256 i = 0; i < accounts.length; i++) {
            _balances[accounts[i]] = balances[i];
            emit Transfer(address(0), accounts[i], balances[i]);
        }
    }

    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(!paused, "Contract paused");

        // 计算手续费
        uint256 fee = (amount * transferFee) / 10000;
        uint256 netAmount = amount - fee;

        _transfer(msg.sender, to, netAmount);

        // 收取手续费
        if (fee > 0) {
            _transfer(msg.sender, feeCollector, fee);
            emit FeeCollected(msg.sender, fee);
        }

        return true;
    }

    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "Transfer from zero");
        require(to != address(0), "Transfer to zero");
        require(_balances[from] >= amount, "Insufficient balance");

        _balances[from] -= amount;
        _balances[to] += amount;
        emit Transfer(from, to, amount);
    }

    function setTransferFee(uint256 newFee) public onlyOwner {
        require(newFee <= 1000, "Fee too high"); // 最大10%
        transferFee = newFee;
    }

    function setFeeCollector(address newCollector) public onlyOwner {
        require(newCollector != address(0), "Invalid address");
        feeCollector = newCollector;
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }
}

// ======================
// 迁移合约
// ======================
contract TokenMigrator {
    OldToken public oldToken;
    NewToken public newToken;

    address public owner;
    bool public migrationCompleted;

    event MigrationStarted();
    event MigrationCompleted(uint256 accountsMigrated, uint256 totalSupply);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier migrationNotCompleted() {
        require(!migrationCompleted, "Migration already completed");
        _;
    }

    constructor(address oldTokenAddress) {
        owner = msg.sender;
        oldToken = OldToken(oldTokenAddress);
    }

    // 部署新代币合约
    function deployNewToken() public onlyOwner {
        require(address(newToken) == address(0), "New token already deployed");
        newToken = new NewToken();
    }

    // 开始迁移(暂停旧合约)
    function startMigration() public onlyOwner migrationNotCompleted {
        require(address(newToken) != address(0), "New token not deployed");

        // 暂停旧合约
        oldToken.pause();
        emit MigrationStarted();
    }

    // 迁移单个账户余额
    function migrateAccount(address account) public migrationNotCompleted {
        require(oldToken.paused(), "Migration not started");

        uint256 balance = oldToken.balanceOf(account);
        if (balance > 0) {
            // 在实际应用中,这里可能需要转移代币所有权
            // 本示例假设迁移合约有权限修改新代币状态
            // 注意:在生产环境中,需要确保安全的权限控制
        }
    }

    // 批量迁移账户(实际迁移函数)
    function migrateAccounts(
        address[] calldata accounts,
        uint256[] calldata balances,
        uint256 totalSupply
    ) public onlyOwner migrationNotCompleted {
        require(oldToken.paused(), "Migration not started");
        require(accounts.length == balances.length, "Invalid input");

        // 初始化新代币状态
        newToken.initializeAfterMigration(accounts, balances, totalSupply);

        migrationCompleted = true;
        emit MigrationCompleted(accounts.length, totalSupply);
    }

    // 获取旧合约总供应量(辅助函数)
    function getOldTotalSupply() public view returns (uint256) {
        return oldToken.totalSupply();
    }

    // 获取旧合约账户余额(辅助函数)
    function getOldBalance(address account) public view returns (uint256) {
        return oldToken.balanceOf(account);
    }
}

二、代理模式

这是目前最流行的升级方式,它的核心思想是用户与代理合约交互,代理合约调用委托(delegatecall)给逻辑合约。当需要升级的时候,部署新版本逻辑合约,将代理合约指向新的逻辑合约地址即可。

关键点:

  • 存储(状态变量)保存在代理合约中,逻辑合约仅提供代码执行。
  • 升级时只需更新代理合约中的逻辑地址,用户数据和地址不变。

代理模式的主流实现包括:透明代理、UUPS代理、信标代理。

1. 透明代理

通过代理合约的管理员来升级逻辑合约。普通用户调用不会触发升级函数,只有管理员可以。

// 透明代理示例(OpenZeppelin 风格)
contract TransparentProxy is Proxy {
    constructor(address _logic, address _admin) Proxy(_logic, _admin) {}

    // 只有管理员可以调用管理函数
    function _admin() internal view returns (address) {
        return StorageSlot.getAddressSlot(ADMIN_SLOT).value;
    }

    // 普通用户调用会直接转发到实现合约
    fallback() external payable {
        if (msg.sender == _admin()) {
            // 处理管理逻辑
        } else {
            // 转发到实现合约
            _delegate(_implementation());
        }
    }
}

2. UUPS 代理

升级逻辑包含在逻辑合约中,而不是代理合约中。这样代理合约更轻量,但要求每次升级逻辑合约时都要包含升级功能。

// UUPS 实现合约示例
contract MyContract is ERC1967Upgrade {
    function upgradeTo(address newImplementation) external onlyOwner {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCall(newImplementation, "", false);
    }

    // 实现合约的功能
    function myFunction() external returns (uint) {
        return 42;
    }
}

3. 信标代理

多个代理合约共享一个信标合约,信标合约中存储当前逻辑合约地址。升级时只需更新信标合约,所有代理合约自动使用新的逻辑合约。

三、钻石模式

单一代理合约将不同功能拆分到多个逻辑合约(Facet),通过路由表动态调用。

特点:

  • 解决合约大小限制(24KB)。
  • 支持模块化升级(替换/添加/删除功能)。

    // 钻石代理合约示例
    contract Diamond {
    // 存储函数选择器到实现合约的映射
    mapping(bytes4 => address) internal selectorToImplementation;
    
    // 添加/替换功能
    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external onlyOwner {
        // 实现逻辑
    }
    
    // 函数调用转发
    fallback() external payable {
        address facet = selectorToImplementation[msg.sig];
        require(facet != address(0), "Function not found");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
    }

    四、可升级合约框架

    使用现有的可升级合约框架,如OpenZeppelin的Upgrades Plugins(支持透明代理和UUPS代理)等,这些框架提供了工具和模板来简化升级过程。

五、升级的流程及注意事项

// 使用 ethers.js 进行合约升级的示例
async function upgradeContract() {
    // 部署新的实现合约
    const MyContractV2 = await ethers.getContractFactory("MyContractV2");
    const myContractV2 = await MyContractV2.deploy();
    await myContractV2.deployed();

    // 获取代理合约实例
    const proxyAddress = "0x..."; // 代理合约地址
    const proxy = await ethers.getContractAt("MyContractV1", proxyAddress);

    // 升级代理合约指向的实现
    const owner = await ethers.getSigner(ownerAddress);
    await proxy.connect(owner).upgradeTo(myContractV2.address);

    console.log("合约已升级到版本 2");
}

注意事项

  • 存储布局兼容性:新合约的存储变量必须与旧合约兼容,避免数据损坏。
  • 初始化函数:升级后 初始化函数:升级后可能需要重新初始化某些状态。
  • 事件和接口连续性:确保升级不影响外部系统对合约的交互。
  • 安全审查:升级前必须对新合约进行全面审计。
  • 代理合约权限:严格控制代理合约的管理权限,防止未经授权的升级。

    六、总结

    合约升级虽然提供了灵活性,但也带来了安全风险(例如,升级权限被滥用)和复杂性。因此,在设计可升级合约时,需要仔细考虑升级机制的安全性,并尽量减少升级的必要性。

在实际应用中,代理模式(尤其是透明代理和UUPS代理)是最常用的方法。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mengbuluo222
mengbuluo222
0x9Ff1...FaA5
前端开发求职中... 8年+开发经验,拥有丰富的开发经验,擅长VUE、React开发。