使用Hardhat和OpenZeppelin Upgrades加载代理合约触发CALL_EXCEPTION回退的原因
问题分析与解决办法
核心问题定位
升级版本的ERC1155合约通过loadFixture加载后调用balanceOf触发CALL_EXCEPTION,但直接在fixture中调用正常,非升级版无此问题——说明问题出在升级合约的部署/快照还原逻辑上,和合约业务逻辑无关。
可能的原因及修复步骤
1. Fixture中升级合约部署流程不规范
升级合约必须通过OpenZeppelin的deployProxy正确部署并执行初始化,若fixture里部署代码不标准,会导致loadFixture还原后代理合约未正确关联实现合约或未完成初始化。
修复示例代码:
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers"); const { upgrades } = require("hardhat"); async function deployNftFixture() { const [owner] = await ethers.getSigners(); // 用deployProxy标准流程部署升级合约,指定初始化函数和参数 const Nft1155Upgradeable = await ethers.getContractFactory("Nft1155Upgradeable"); const nft = await upgrades.deployProxy(Nft1155Upgradeable, ["ipfs://your-base-uri/"], { initializer: "initialize" }); await nft.deployed(); return { nft, owner }; } // 测试用例写法 describe("NFT测试", function () { it("直接调用fixture内合约正常", async function () { const { nft, owner } = await deployNftFixture(); const balance = await nft.balanceOf(owner.address, 1); // 断言逻辑 }); it("loadFixture后调用正常", async function () { const { nft, owner } = await loadFixture(deployNftFixture); const balance = await nft.balanceOf(owner.address, 1); // 断言逻辑 }); });
2. Hardhat快照与升级合约的兼容性问题
Hardhat的loadFixture依赖网络快照,而OpenZeppelin升级插件部署时会修改网络状态(比如存储代理的实现地址),快照还原可能导致代理状态异常。
修复方案:
- 所有升级合约的部署、初始化逻辑都放在fixture内部完成,不要在fixture外修改合约状态
- 执行
npx hardhat clean清除编译缓存后重新测试 - 确认依赖版本匹配:
@openzeppelin/hardhat-upgrades和@openzeppelin/contracts-upgradeable版本需兼容(当前4.7.3的合约对应upgrades插件^1.20.0+版本即可,你的1.21.0版本没问题,可尝试重新安装依赖)
3. 合约初始化函数逻辑/权限问题
如果initialize函数执行失败,代理合约会处于未初始化状态,调用任何函数都会触发回退。
检查点:
- 确保初始化函数正确调用了所有父合约的初始化方法:
function initialize(string memory uri) public initializer { __ERC1155_init(uri); __ERC1155Burnable_init(); __ERC1155Supply_init(); __AccessControlEnumerable_init(); // 自定义初始化逻辑 }
- 确认
initialize函数仅被调用一次(initializer修饰符会阻止重复调用)
4. Ethers合约实例关联错误
loadFixture还原后,要确认合约实例关联的是代理合约地址而非实现合约地址。
验证代码:
it("验证合约地址正确性", async function () { const { nft } = await loadFixture(deployNftFixture); console.log("代理合约地址:", nft.address); // 获取实现合约地址,确认代理关联正常 const implAddress = await upgrades.erc1967.getImplementationAddress(nft.address); console.log("实现合约地址:", implAddress); });
内容的提问来源于stack exchange,提问作者delete
相关产品推荐
相关产品推荐

