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

Hardhat与Waffle中如何测试工厂合约返回地址对应的合约实例

问题描述

使用Hardhat + Waffle测试工厂合约场景下,工厂部署方法返回新合约地址,但deployContract方法不支持传入地址加载已部署合约,无法直接拿到合约实例调用方法做测试。相关代码如下:

被测合约

Domain合约

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Domain {

    string private publicKey;

    address[] public children;

    constructor(string memory _publicKey) {
        console.log("Deploying a domain using public key: ", _publicKey);
        publicKey = _publicKey;
    }

    function getChildren() public view returns (address[] memory){
        return children;
    }
}

DomainFactory工厂合约

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "./Domain.sol";
import "hardhat/console.sol";


contract DomainFactory {
    Domain[] private _domains;
    function createDomain(
                          string memory _publicKey
                          ) public returns (address){
        Domain domain = new Domain(
                                   _publicKey
                                   );
        _domains.push(domain);
        return address(domain);
    }
    function allDomains(uint256 limit, uint256 offset)
        public
        view
        returns (Domain[] memory coll)
    {
        return coll;
    }
}

测试代码

测试步骤

When('the holder of this public key creates a domain', async function () {
    this.domain = await this.factory.createDomain('<public_key>');
});

Then('a child of this domain has the name name', async function () {
    const children = this.domain.getChildren();
    const childrenWithName = children.find((child:any) => {
        return child.getNames().some((childName:any) => {
            return childName === 'name';
        })
    })

    expect(childrenWithName).to.be.an('array').that.is.not.empty;
});

World上下文定义

// world.ts

import { setWorldConstructor, setDefaultTimeout } from '@cucumber/cucumber'
import {deployContract, MockProvider, solidity} from 'ethereum-waffle';
import {use} from "chai";
import DomainContract from "../../../artifacts/contracts/Domain.sol/Domain.json";
import DomainFactoryContract from "../../../artifacts/contracts/DomainFactory.sol/DomainFactory.json";
import { Domain, DomainFactory } from "../../../typechain-types";
import {Wallet} from "ethers";



use(solidity);

setDefaultTimeout(20 * 1000);

class DomainWorld {
  public owner: string
  public wallets: Wallet[]
    public factory: DomainFactory | undefined
    public domain: Domain | undefined
    public ready: boolean = false
    private _initialized: Promise<boolean>
 

    async deployContractByAddress(address, ...args){
        return await deployContract(this.wallets[0], address, ...args);
    }
  constructor() {
    this.wallets = new MockProvider().getWallets();
    this.owner = this.wallets[0].address

    const that = this
    this._initialized = new Promise(async (resolve, reject) => {
        try {
            that.factory = (await deployContract(that.wallets[0], DomainFactoryContract, [])) as DomainFactory;
            that.ready = true
            resolve(true)
        }catch (err) {
            reject(err)
        }
    })
  }
}


setWorldConstructor(DomainWorld);

解决方案

不需要使用deployContract加载工厂部署的合约,deployContract的作用是部署全新合约,加载已部署的合约实例直接用合约地址+ABI+签名者实例化即可,步骤如下:

  1. 修正工厂方法调用逻辑:直接调用写方法返回的是交易响应对象,不是合约返回值,要拿到新部署的合约地址有两种方式:
    • 用callStatic模拟调用,提前得到部署地址
    • 等待交易上链后,从交易收据的事件中解析新合约地址(更稳妥,能确认部署真的成功)
  2. 用拿到的地址实例化合约对象,两种方式可选:
    • 用ethers原生Contract类,传入地址、合约ABI、签名钱包
    • 用typechain自动生成的合约工厂类的connect方法,不需要手动做类型断言
      修改后的When步骤代码示例:
import { Contract } from "ethers";
import { Domain__factory } from "../../../typechain-types";

When('the holder of this public key creates a domain', async function () {
    // 发送部署交易
    const tx = await this.factory.createDomain('<public_key>');
    // 等待交易确认
    const receipt = await tx.wait();
    
    // 拿到新部署的Domain合约地址,优先从事件解析,兜底用callStatic获取
    let newDomainAddress: string;
    const deployEvent = receipt.events?.find(e => e.event === 'DomainDeployed');
    if (deployEvent) {
        newDomainAddress = deployEvent.args?.domainAddress;
    } else {
        newDomainAddress = await this.factory.callStatic.createDomain('<public_key>');
    }

    // 实例化合约,二选一即可
    // 方式1:原生ethers实例化
    this.domain = new Contract(
        newDomainAddress,
        DomainContract.abi,
        this.wallets[0]
    ) as Domain;

    // 方式2:typechain工厂类连接实例,无需手动转类型
    // this.domain = Domain__factory.connect(newDomainAddress, this.wallets[0]);
});

注意事项:

  • 你之前写的deployContractByAddress方法逻辑错误,deployContract第二个参数是编译后的合约JSON产物/ABI,不是合约地址,这个方法可以直接删除。
  • 目前你的Domain合约中没有定义getNames方法,后续跑测试前需要补上对应逻辑,否则调用会报错。
  • 如果需要在工厂合约中抛出部署事件,可以在createDomain方法里加一行emit DomainDeployed(address(domain));,同时在合约顶部定义对应的event,方便链下解析地址。

内容的提问来源于stack exchange,提问作者David J.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 08:03:28