How to correctly test the initialisation of a clone contract on Hardhat? (EIP-1167)

:1234: Code to reproduce


:computer: Environment

Hardhat 2.11.1, Chai 4.3.6, ethereum-waffle 3.4.4, solidity 0.8.8

So I have created an 'implementation contract' and a 'clone factory' which creates clones of the implementation contract. I have tested the functionality of the CloneFactory and it works in Remix.

However, I am now writing tests for the contract in Hardhat and it doesn't seem to be working correctly (Chai). The issue I am facing is that the cloned contract is not being correctly initialised.

Here is my Clone Factory:

contract CloneFactory {
    address public implementationContract;
    address[] public allClones;

    event NewClone(address indexed _instance);

    mapping(address => address) public list;

    constructor(address _implementation) {
        implementationContract = _implementation;
    }

    function createClone(address _whoopyCreator) payable external returns(address instance) {
        instance = Clones.clone(implementationContract);
        (bool success, ) = instance.call{value: msg.value}(abi.encodeWithSignature("initialize(address)", _creator));
        allClones.push(instance);
        list[_creator] = instance;
        emit NewClone(instance);
        return instance;
    }

Here is the initialiser function in my Implementation Contract:

   function initialize(address _creator) public initializer {
        creator = _creator;

Here is the test I am running (this is where I believe there is an issue):

it("Initialises contract correctly", async function () {

        Main = await ethers.getContractFactory("Main");
        main = await Main.deploy();
        await main.deployed();

        const dir = path.resolve(
            __dirname,
            "/Users/xxx/hardhat-smartcontract-main/artifacts/contracts/Main.sol/Main.json"
          )
        const file = fs.readFileSync(dir, "utf8")
        const json = JSON.parse(file)
        const abi = json.abi

        CloneFactory = await ethers.getContractFactory("CloneFactory");
        cf = await CloneFactory.deploy(main.address);
        await cf.deployed();
    
        let currentProvider = new web3.providers.HttpProvider('http://localhost:8545');
        let web3Provider = new ethers.providers.Web3Provider(currentProvider);
        const [addr1, addr2] = await ethers.getSigners();

        const tx = await cf.createClone(addr1.address, {    
          value: ethers.utils.parseUnits("0", "ether")
        });

        const txReceipt = await tx.wait(1)


        const cloneAddress = await txReceipt.events[0].args._instance
        const clonedContract = new ethers.Contract(cloneAddress, abi, web3Provider)
        const accountConnectedClone = clonedContract.connect(addr1)
        const creator = await accountConnectedClone.getCreator()
        expect(addr1.address).to.equal(creator)
    })

When I run this test, I get the following output:

AssertionError: expected '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb…' to equal '0x00000000000000000000000000000000000…'
      + expected - actual


      -0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
      +0x0000000000000000000000000000000000000000

Basically, the creator is not being saved as the address of the caller of the 'createClone' function (which means it is not being initialized. I don't think there is an issue with the Clone Factory since this works correctly in Remix.

My suspicion is this has something to do with either the ABI or with the ethers/web3 provider or with the hardhat testnet, however I could be wrong.

There doesn't seem to be much info online about testing clone or proxy smart contracts so I hope someone here can help me out.

Thanks in advance!

EthereumBoy

You are not verifying that the call was successful!

You should see a Solidity warning right at this line.

Ideally you should cast the address value to your interface and invoke the function using classic Solidity syntax, and that should prevent most errors:

instance = Clones.clone(implementationContract);
IImplementationinterface(instance).initialize(creator);

Hey Frangio,
Thanks for the response!

As recommended on other forums, I did later verify to see whether the call was successful with the following line:
require(success==true, "initialize did not return true");

I ended up with the following error:
"Error: VM Exception while processing transaction: reverted with reason string 'initialize did not return true'"

After a bit of tweaking, this is another error which seems to come up: "Error: Transaction reverted: function call to a non-contract account"

Hey Frangio,

Just tried using an interface and I'm getting the error: "Error: Transaction reverted: function call to a non-contract account"

Any idea what might be the issue?

Hey Frangio,
I suspect that the issue has to do with the presence of the Chainlink VRF Coordinator. The transaction goes through when I remove the coordinator. However, for some reason when I remove the Chainlink Coordinator from the initialize function, I am not able to read the 'instance' which is returned from the 'createClone' function.

Please share the code for your implementation contract.

Hey Frangio,

Got the test to work. I made the changes you recommended of initializing the VRF in the constructor over here: https://forum.openzeppelin.com/t/how-to-use-chainlink-vrf-and-chainlink-upkeep-while-making-clones-eip-1167/31644

Also, it turns out I was testing for the existance of the CloneAddress at the wrong place. I was initially testing this:

const cloneAddress = await txReceipt.events[0].args._instance
        expect(cloneAddress).to.exist

When I tested here I received an error: "Cannot read undefined instance".

I then changed it to:

const cloneAddress = await txReceipt.events[1].args._instance
        expect(cloneAddress).to.exist

And this went through.

Thanks for all your help!!

1 Like