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代理、信标代理。
通过代理合约的管理员来升级逻辑合约。普通用户调用不会触发升级函数,只有管理员可以。
// 透明代理示例(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());
}
}
}
升级逻辑包含在逻辑合约中,而不是代理合约中。这样代理合约更轻量,但要求每次升级逻辑合约时都要包含升级功能。
// UUPS 实现合约示例
contract MyContract is ERC1967Upgrade {
function upgradeTo(address newImplementation) external onlyOwner {
_authorizeUpgrade(newImplementation);
_upgradeToAndCall(newImplementation, "", false);
}
// 实现合约的功能
function myFunction() external returns (uint) {
return 42;
}
}
多个代理合约共享一个信标合约,信标合约中存储当前逻辑合约地址。升级时只需更新信标合约,所有代理合约自动使用新的逻辑合约。
单一代理合约将不同功能拆分到多个逻辑合约(Facet),通过路由表动态调用。
特点:
支持模块化升级(替换/添加/删除功能)。
// 钻石代理合约示例
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代理)是最常用的方法。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!