自定义权限modifier导致Solidity合约测试用例运行失败
问题原因
报错来自三处代码逻辑问题:
- 合约构造函数里硬编码的
_addr_1、_addr_2是无效占位符,没有对应Hardhat测试环境里随机生成的测试账户地址,测试部署时授权映射AuthAccounts根本没有把你调用方法用的owner地址加入白名单,触发权限校验拦截。 - 你贴出的
addWinner函数漏加了自定义的onlyAuthAccounts修饰器——如果真的没加修饰器根本不会触发权限报错,说明你实际运行的代码加了修饰器但贴代码时遗漏,要注意权限修饰器必须显式标注在目标函数上才会生效。 - 你没有继承OpenZeppelin的Ownable合约,也没有自行实现
owner()方法,第一个测试用例调用leagueWiners.owner()本身就会因为方法不存在直接报错,和权限问题无关。
修复方案
第一步:调整合约代码
把硬编码授权地址的逻辑改成构造函数传参,方便部署(含测试部署)时灵活配置授权地址,同时给addWinner补上权限修饰器。修正后的合约代码如下:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.5; contract LeagueWinners { struct Winner { bool exists; bool claimed; uint256 reward; } mapping(address=>Winner) public winners; mapping (address => bool) private AuthAccounts; modifier onlyAuthAccounts() { require(AuthAccounts[msg.sender], "Auth: caller is not the authorized"); _; } // 构造函数接收两个授权地址作为入参,不再硬编码固定地址 constructor (address _auth1, address _auth2) { AuthAccounts[_auth1] = true; AuthAccounts[_auth2] = true; } // 补上权限修饰器 function addWinner(address _address, uint256 _amount ) public onlyAuthAccounts { Winner storage winner = winners[_address]; winner.exists = true; winner.reward = _amount; } }
第二步:调整测试代码
Hardhat启动测试时会默认生成10个临时测试签名账户,部署合约时把要用于测试的授权地址传入构造函数,删掉合约未实现的owner()相关校验逻辑,补充必要的结果断言。修正后的测试代码如下:
const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("LeagueWinners", function () { let leagueWinners; let auth1, auth2, nonAuth, winnerAddr; before(async () => { // 从测试环境取测试账户:前两个做授权账户,第三个做非授权测试账户,第四个做获奖者地址 [auth1, auth2, nonAuth, winnerAddr] = await ethers.getSigners(); const LeagueWinners = await ethers.getContractFactory("LeagueWinners"); // 部署时传入两个授权地址 leagueWinners = await LeagueWinners.deploy(auth1.address, auth2.address); await leagueWinners.deployed(); }); it("should block non-authorized account from adding winner", async function () { // 验证非授权账户调用会被拦截 await expect( leagueWinners.connect(nonAuth).addWinner(winnerAddr.address, ethers.utils.parseEther("50")) ).to.be.revertedWith("Auth: caller is not the authorized"); }); it("should allow authorized account to add winner", async function () { // 用授权账户调用添加获奖者方法 const addTx = await leagueWinners .connect(auth1) .addWinner( winnerAddr.address, ethers.utils.parseEther("50") // 用官方工具做单位转换,避免手写长串零数错位数 ); await addTx.wait(); // 验证获奖者信息正确上链 const winnerInfo = await leagueWinners.winners(winnerAddr.address); expect(winnerInfo.exists).to.equal(true); expect(winnerInfo.reward).to.equal(ethers.utils.parseEther("50")); }); });
注意事项
- 硬编码固定地址的写法仅适合正式网部署时写死多签/管理员地址,测试环境的账户地址是每次启动临时生成的,硬编码的固定地址永远匹配不上测试账户,必然触发权限报错。
- 测试逻辑要和合约实现对齐,合约没实现的方法不要强行调用,否则会报方法不存在的错误。
- 链上金额默认单位是wei,尽量用
ethers.utils.parseEther这类官方工具做单位转换,不要手动写一长串数字,避免位数错误。
内容的提问来源于stack exchange,提问作者Gourav Naik
相关产品推荐
相关产品推荐

