Search code examples
ethereumsmartcontractshardhat

hardhat & waffle - deploy a contract from an address


I'm trying to test a factory contract using hardhat and waffle. I have a contract called 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;
    }
}

And a factory for deploying this contract:

//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;
    }
}

I have the following tests defined, where this refers to a context object defined in a "world" file (using cucumber.js.

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;
});

Ideally in the when step, I could define this.domain as the result of deploying a contract, and thereafter test the methods of the contract I deploy:

// 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);

My problem is, hardhat's deployContract function isn't expecting a contract address, which is what is returned by my DomainFactory's create method. How can I test contracts deployed via my factory if the return value is an address?


Solution

  • I've made a quick hardhat project for you to test it. Here are the highlights:

    The easiest way I find to get this returned valued of the contract offchain (and by offchain in this case, I mean inside your test environment) is by emitting an Event. So, I made the following change to your code.

    //SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.0;
    import "./Domain.sol";
    import "hardhat/console.sol";
    
    contract DomainFactory {
        Domain[] private _domains;
    
        event CreatedDomain(address domainAddress);
    
        function createDomain(string memory _publicKey) public returns (address) {
            Domain domain = new Domain(_publicKey);
            _domains.push(domain);
            emit CreatedDomain(address(domain));
            return address(domain);
        }
    
        function allDomains(uint256 limit, uint256 offset)
            public
            view
            returns (Domain[] memory coll)
        {
            return coll;
        }
    }
    
    

    I just simply emit a CreatedDomain() event containing the deployed Domain address.

    One more thing: if I remember correctly, you can only retrieve values direct from function returns offchain, if your function is of type view. Otherwise, you will need to emit the event and then find it later.

    In order to test the Domain deployed by the DomainFactory take a look at this test script:

    import { expect } from "chai";
    import { ethers } from "hardhat";
    import { Domain, DomainFactory } from "../typechain";
    
    describe("Domain", function () {
      let domainFactory: DomainFactory;
      let domain: Domain;
      let domainAddress: string;
    
      it("Should deploy a DomainFactory ", async () => {
        const DomainFactory = await ethers.getContractFactory("DomainFactory");
        domainFactory = await DomainFactory.deploy();
        await domainFactory.deployed();
      });
    
      it("deploy a Domain using DomainFactory ", async () => {
        const tx = await domainFactory.createDomain("public string here");
        const rc = await tx.wait();
        const event = rc.events?.find((event) => event.event === "CreatedDomain");
        const args = event?.args;
        if (args) domainAddress = args[0];
      });
    
      it("attach an abi interface to the deployed domain", async () => {
        const Domain = await ethers.getContractFactory("Domain");
        domain = await Domain.attach(domainAddress);
      });
    
      it("get data from Domain deployed by DomainFactory ", async () => {
        const res = await domain.getChildren();
        console.log(res);
      });
    });
    
    

    It deploys a DomainFactory then uses the createDomain() method, fetches the deployed address from the functino events, then use it to attach the ABI to the deployed Domain.

    Full code here: https://github.com/pedrohba1/stackoverflow/tree/main/Domain

    Anything else related to running it I will be adding in the comments.