无代理部署可治理升级的ERC20Upgradeable合约问题求助
问题分析与解决方案
核心问题拆解
你遇到的问题本质是对UUPS升级模式的逻辑理解偏差:
- 直接部署的V2逻辑合约因为构造函数中
_disableInitializers()的限制,无法调用initialize,导致合约owner为零地址——但逻辑合约本身不需要初始化,所有状态(包括owner)都存储在代理合约中,逻辑合约只是业务逻辑的实现载体。 - 错误提示的
Ownable: caller is not the owner,是因为你尝试对逻辑合约执行需要owner权限的操作,但逻辑合约本身未初始化,owner为零地址,而你根本不需要操作逻辑合约。
正确的升级流程
1. 编写兼容的V2合约
确保V2合约继承与V1一致的升级基类,且存储布局完全兼容(只能在状态变量末尾新增,不能修改原有顺序):
// GovernanceTokenV2.sol contract GovernanceTokenV2 is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, ERC20VotesUpgradeable, OwnableUpgradeable, UUPSUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } // 保留V1的initialize逻辑(升级时不会触发,代理已完成初始化) function initialize() initializer public { __ERC20_init("GovernanceToken", "GT"); __ERC20Permit_init("GovernanceToken"); __ERC20Votes_init(); __Ownable_init(); __UUPSUpgradeable_init(); } // 新增V2专属功能示例 function getVersion() external pure returns (string memory) { return "V2"; } // 保持UUPS升级权限控制(继承OwnableUpgradeable,仅代理存储中的owner可执行升级) function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }
2. 修改升级脚本
只需部署V2逻辑合约,然后通过治理流程让Timelock触发代理的升级操作:
// 00-deploy-upgraded-Token.ts const { getNamedAccounts, deployments, network } = hre; const { deploy, log } = deployments; const { deployer } = await getNamedAccounts(); // 替换为你的代理合约地址(原deployProxy返回的deployedProxy.address) const PROXY_ADDRESS = "0x..."; // 替换为你的治理合约地址 const GOVERNOR_ADDRESS = "0x..."; // 部署V2逻辑合约(无需初始化) const governanceTokenV2 = await ethers.getContractFactory("GovernanceTokenV2"); const v2Implementation = await governanceTokenV2.deploy(); await v2Implementation.deployed(); log(`V2逻辑合约部署完成:${v2Implementation.address}`); // 获取代理合约实例,构造升级交易数据 const proxyContract = await ethers.getContractAt("GovernanceToken", PROXY_ADDRESS); const upgradeCalldata = proxyContract.interface.encodeFunctionData("upgradeTo", [v2Implementation.address]); // 提交治理提案,让Timelock执行升级操作(示例基于OpenZeppelin Governor) const governor = await ethers.getContractAt("GovernorContract", GOVERNOR_ADDRESS); const proposalTx = await governor.propose( [PROXY_ADDRESS], // 目标合约:代理合约 [0], // 转账金额:0 [upgradeCalldata], // 调用数据:触发upgradeTo "升级GovernanceToken至V2版本" // 提案描述 ); await proposalTx.wait(1); log(`升级提案已提交,等待投票通过后由Timelock执行`);
关键注意事项
- 逻辑合约(V2)不需要初始化:所有状态(包括owner=Timelock)都存储在代理合约中,代理在V1部署时已完成初始化。
- UUPS升级权限由
_authorizeUpgrade控制:你的合约继承OwnableUpgradeable,因此只有代理存储中的owner(即Timelock)可以触发升级,符合治理流程要求。 - 必须保证存储布局兼容:新增状态变量只能放在现有变量末尾,否则会破坏代理中的数据结构。
内容的提问来源于stack exchange,提问作者Ritzy Dev
相关产品推荐
相关产品推荐

