NomicFoundation / hardhat-ignition

Hardhat Ignition is a declarative deployment system that enables you to deploy your smart contracts without navigating the mechanics of the deployment process.
https://hardhat.org/ignition
MIT License
108 stars 26 forks source link

Ignition's useModule and/or module loading does not work properly while running tests. #801

Closed luismasuelli closed 2 months ago

luismasuelli commented 2 months ago

What happened?

This happens when I try to test my contracts (which makes use of the hardhat network configuration): modules are not preserved in the hardhat network. Edit: no, I'm not talking about preserving in-disk, but about in-memory preserving while running a single test file in the hardhat network -- this is still not happening.

If, by chance, I have several modules depending on the same module and I need to use all of them, that other module would be loaded multiple times. Edit: or, as in this case, I need to get a module and also the module it depends on.

This involves versions in my project are:

"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"chai": "^4.3.7",
"hardhat": "^2.22.10",

While the locked / effective ones are:

├── @nomicfoundation/hardhat-toolbox@5.0.0 ├── chai@4.5.0 └── hardhat@2.22.10

While I understand that keeping artifacts for hardhat network led to inconsistencies (e.g. manually needing later to delete de deployments/chain-31337 directory or stuff like that), it seems that the in-memory keeping of those artifacts is not being done or has some kind of error..

Minimal reproduction steps

Consider the following layout:

  1. Two contracts Foo and Bar, where Bar depends on Foo:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract FooContract {
    constructor(){}

    function helloWorld() public view returns (string memory) {
        return "Hello World";
    }
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "./FooContract.sol";

contract BarContract {
    address public fooContract;

    constructor(address foo){
        fooContract = foo;
    }

    function extendedHelloWorld() public view returns (string memory) {
        return string(abi.encodePacked("Foo:", FooContract(fooContract).helloWorld()));
    }
}
  1. Two ignition modules: One for Foo, and one for Bar:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

module.exports = buildModule("FooContract", (m) => {
  const contract = m.contract("FooContract", []);
  return { contract };
});
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const FooContract = require("./FooContract");

module.exports = buildModule("BarContract", (m) => {
  const { contract: fooContract } = m.useModule(FooContract);
  const contract = m.contract("BarContract", [fooContract]);
  return { contract };
});
  1. A dumb test (test/FooBar.js) file where I use those modules in a fixture:
const { expect } = require("chai");
const {
    time,
    loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const FooContract = require("../ignition/modules/FooContract");
const BarContract = require("../ignition/modules/BarContract");

/**
 * Fixture function to deploy some contracts.
 * @returns {Promise<*>} The fixture data (async function).
 */
async function deployFooAndBar() {
    const { contract: foo } = await ignition.deploy(FooContract);
    const { contract: bar } = await ignition.deploy(BarContract);
    // These 3 exist only for debugging.
    console.log(foo);
    console.log(await bar.fooContract());
    console.log(bar);
    return { foo, bar };
}

describe("FooBar", () => {
    /**
     * Here, we keep foo contract.
     */
    let foo = null;

    /**
     * Here, we keep bar contract.
     */
    let bar = null;

    before(async () => {
        let { foo: foo_, bar: bar_ } = await loadFixture(deployFooAndBar);
        foo = foo_;
        bar = bar_;
    });

    it("must pass - empty test", async () => {});
});
  1. But there's a problem (possibly related to the new ignition versions - this is why I'm here). If I run this command:
npx hardhat test test/FooBar.js --network localhost

Then everything goes fine. For example the 3 console.log commands show this:

Contract {
  target: '0x8af21B3aB487f6ea74CD7EC35cc7F09Ce97Da5f6',
  interface: Interface {
    fragments: [ [ConstructorFragment], [FunctionFragment] ],
    deploy: ConstructorFragment {
      type: 'constructor',
      inputs: [],
      payable: false,
      gas: null
    },
    fallback: null,
    receive: false
  },
  runner: HardhatEthersSigner {
    _gasLimit: 30000000,
    address: '0x4867D8f4144114c192240a4eC5F4F43918c5a55C',
    provider: HardhatEthersProvider {
      _hardhatProvider: [LazyInitializationProviderAdapter],
      _networkName: 'localhost',
      _blockListeners: [],
      _transactionHashListeners: Map(0) {},
      _eventListeners: []
    }
  },
  filters: {},
  fallback: null,
  [Symbol(_ethersInternal_contract)]: {}
}
0x8af21B3aB487f6ea74CD7EC35cc7F09Ce97Da5f6
Contract {
  target: '0x6346404f4aB00239B605b8Fad4214D40254BbA3D',
  interface: Interface {
    fragments: [ [ConstructorFragment], [FunctionFragment], [FunctionFragment] ],
    deploy: ConstructorFragment {
      type: 'constructor',
      inputs: [Array],
      payable: false,
      gas: null
    },
    fallback: null,
    receive: false
  },
  runner: HardhatEthersSigner {
    _gasLimit: 30000000,
    address: '0x4867D8f4144114c192240a4eC5F4F43918c5a55C',
    provider: HardhatEthersProvider {
      _hardhatProvider: [LazyInitializationProviderAdapter],
      _networkName: 'localhost',
      _blockListeners: [],
      _transactionHashListeners: Map(0) {},
      _eventListeners: []
    }
  },
  filters: {},
  fallback: null,
  [Symbol(_ethersInternal_contract)]: {}
}

This is OK, since deploying a multiple time the same module should be idempotent and return the same direction. In this case, 0x8af21B3aB487f6ea74CD7EC35cc7F09Ce97Da5f6 appears standalone (product of logging .fooContract() call as well) and also as the target of the first contract. THIS IS OK.

However, running deployment on the hardhat network seems to not preserve artifacts on any deployment module. So far, to make a recap, let's consider what my fixture does:

  1. Loads the Foo module.
  2. Loads the Bar module, which includes first also depending on the Foo module.

In localhost network nothing is wrong here, but since the ignition artifacts are not preserved... the Foo module is invoked twice. So what I see when running tests in the hardhat network I see this:

Contract {
  target: '0xC3cAc7d3d5fa475A9d25eC99cbbC7a5F2671745a',
  interface: Interface {
    fragments: [ [ConstructorFragment], [FunctionFragment] ],
    deploy: ConstructorFragment {
      type: 'constructor',
      inputs: [],
      payable: false,
      gas: null
    },
    fallback: null,
    receive: false
  },
  runner: HardhatEthersSigner {
    _gasLimit: 30000000,
    address: '0x4867D8f4144114c192240a4eC5F4F43918c5a55C',
    provider: HardhatEthersProvider {
      _hardhatProvider: [LazyInitializationProviderAdapter],
      _networkName: 'hardhat',
      _blockListeners: [],
      _transactionHashListeners: Map(0) {},
      _eventListeners: []
    }
  },
  filters: {},
  fallback: null,
  [Symbol(_ethersInternal_contract)]: {}
}
0x21e1345fa2DCA637f252432Ac5C864Face2777fe
Contract {
  target: '0x0066Ee97acEc0523225dD82E6eDB64928792e0e2',
  interface: Interface {
    fragments: [ [ConstructorFragment], [FunctionFragment], [FunctionFragment] ],
    deploy: ConstructorFragment {
      type: 'constructor',
      inputs: [Array],
      payable: false,
      gas: null
    },
    fallback: null,
    receive: false
  },
  runner: HardhatEthersSigner {
    _gasLimit: 30000000,
    address: '0x4867D8f4144114c192240a4eC5F4F43918c5a55C',
    provider: HardhatEthersProvider {
      _hardhatProvider: [LazyInitializationProviderAdapter],
      _networkName: 'hardhat',
      _blockListeners: [],
      _transactionHashListeners: Map(0) {},
      _eventListeners: []
    }
  },
  filters: {},
  fallback: null,
  [Symbol(_ethersInternal_contract)]: {}
}

The problems this brings is that many tests will fail because there are many contracts wrongfully created and interacted with instead of a single contract (of the same type) when such condition is needed... and this happens even inside a single test file, even inside a single before() hook invocation, and even inside a single fixture invocation.

  1. Just in case: I cannot get the contract at bar.fooContract() address in the regular way (i.e. a .getContractAt call) SINCE THE ARTIFACT WAS NOT BUILT. Hacky ways are supported, yes, but not the typical / regular ones when you just do a .contractAt on ignition or ethers (i.e. I need to manually build and retrieve an artifact in a non-quick way).

Search terms

No response

luismasuelli commented 2 months ago

I'm closing this one. I understand it's not a bug but a design choice. My bad.