You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hardhat测试中向合约转奖励代币及claimPrize报错解决

问题排查与修复方案

你遇到的Address: call to non-contract报错不是你猜测的合约无代币余额导致,是合约代码、测试代码存在多个基础错误叠加导致,所有问题点和修复方式如下:

核心报错原因

  • 测试代码中硬编码的奖励代币地址0xAddr.....是无效占位地址,Hardhat本地测试链上该地址未部署任何合约,调用该地址的代币转账方法时直接触发非合约地址调用报错。
  • 除此之外你的代码还存在多个会导致测试/运行失败的问题:
    • Solidity合约未导入IERC20、SafeERC20依赖,safeTransfer方法无定义
    • 合约内Winner结构体定义的奖励字段名为reward,但claim逻辑中错误引用了不存在的tokenAmount字段
    • 构造函数中引用了未定义的_addr_1/_addr_2变量,合约无法正常编译
    • addWinner方法未加权限校验,任意地址都可私自添加获奖地址
    • 合约未实现owner()方法,测试用例中校验owner的逻辑会直接失败
    • 测试代码添加获奖者时传入的是字符串"winner1.address",不是实际的账户地址变量
    • 测试中非获奖者校验逻辑调用了不存在的claimReward()方法,合约实际方法名为claimPrize()
    • 测试环境未部署测试用ERC20代币合约,也未给奖励合约转入任何测试代币

修复后的合约代码

先修正Solidity合约的所有语法、逻辑问题:

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract LeagueWinners {
    using SafeERC20 for IERC20;

    struct Winner {
        bool exists;
        bool claimed;
        uint256 reward;
    }

    mapping(address=>Winner) public winners;
    mapping (address => bool) private AuthAccounts;
    IERC20 private rewardTokenAddr;
    address public owner;

    modifier onlyAuthAccounts() {
        require(AuthAccounts[msg.sender], "Auth: caller is not the authorized");
        _;
    }

    constructor (address _rewardTokenAddr) {
        rewardTokenAddr = IERC20(_rewardTokenAddr);
        owner = msg.sender;
        AuthAccounts[msg.sender] = true;
    }

    function addWinner(address _address, uint256 _amount ) public onlyAuthAccounts {
        Winner storage winner = winners[_address];
        winner.exists = true;
        winner.reward = _amount;
    }

    function claimPrize() public {
        Winner storage winner = winners[msg.sender];
        require(winner.exists, "Not a winner");
        require(!winner.claimed, "Winner already claimed");
        winner.claimed = true;
        rewardTokenAddr.safeTransfer(msg.sender, winner.reward);
    }
}

测试场景下给合约转入奖励代币的方法

本地测试不要用主网/测试网的真实代币地址,先在项目中新增一个测试用模拟ERC20合约,部署后再通过标准ERC20转账方法给奖励合约充值测试代币。

  1. 先在contracts/mocks/目录下新建MockERC20.sol作为测试用代币:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor() ERC20("Test Reward Token", "TRD") {}
    // 公开mint方法方便测试生成测试币
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}
  1. 修复后的完整测试代码如下:
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("LeagueWinners", function () {
  let leagueWinners, rewardToken, owner, winner1, winner2, nonwinner;
  const SINGLE_WINNER_REWARD = ethers.utils.parseEther("50"); // 单个获奖者奖励50代币
  const TOTAL_REWARD_POOL = ethers.utils.parseEther("1000"); // 总奖励池1000代币
  
  before(async () => {
    [owner, winner1, winner2, nonwinner] = await ethers.getSigners();
    
    // 部署测试用ERC20代币
    const RewardTokenFactory = await ethers.getContractFactory("MockERC20");
    rewardToken = await RewardTokenFactory.deploy();
    await rewardToken.deployed();

    // 部署奖励合约,传入测试代币地址
    const LeagueWinnersFactory = await ethers.getContractFactory("LeagueWinners");
    leagueWinners = await LeagueWinnersFactory.deploy(rewardToken.address);
    await leagueWinners.deployed();

    // 给奖励合约充值测试代币:先给owner mint足够测试币,再转总奖励额度到奖励合约地址
    await rewardToken.mint(owner.address, TOTAL_REWARD_POOL);
    await rewardToken.connect(owner).transfer(leagueWinners.address, TOTAL_REWARD_POOL);
  });

  it("Should set deployer as contract owner", async function () {
    expect(await leagueWinners.owner()).to.equal(owner.address);
  });

  it("Should add winner with correct reward amount", async function () {
    await leagueWinners.connect(owner).addWinner(winner1.address, SINGLE_WINNER_REWARD);
    const winnerInfo = await leagueWinners.winners(winner1.address);
    expect(winnerInfo.exists).to.equal(true);
    expect(winnerInfo.reward).to.equal(SINGLE_WINNER_REWARD);
  });

  it("Should revert when non-winner tries to claim", async function () {
    await expect(
      leagueWinners.connect(nonwinner).claimPrize()
    ).to.be.revertedWith("Not a winner");
  });

  it("Should allow winner to claim prize successfully", async function () {
    const beforeClaimBalance = await rewardToken.balanceOf(winner1.address);
    await leagueWinners.connect(winner1).claimPrize();
    const winnerInfo = await leagueWinners.winners(winner1.address);
    const afterClaimBalance = await rewardToken.balanceOf(winner1.address);
    expect(winnerInfo.claimed).to.equal(true);
    expect(afterClaimBalance.sub(beforeClaimBalance)).to.equal(SINGLE_WINNER_REWARD);
  });

  it("Should revert when winner tries to claim twice", async function () {
    await expect(
      leagueWinners.connect(winner1).claimPrize()
    ).to.be.revertedWith("Winner already claimed");
  });
});

内容的提问来源于stack exchange,提问作者Gourav Naik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 23:12:35