Ether.js – Impersonating a Contract When Calling Another in Tests

dapp-developmentdappsethers.jssolidity

I'm new to Solidity dev and writing tests for a dapp (Hardhat, Chai, ethers.js, Smockit) where two contracts, Product and Market, coexist. I'm trying to run a test for a function in Market that can only be called from a Product instance.

Here's the Market contract. It holds a reference to a Product, and has a foo method that will revert if not called from that said Product.

pragma solidity ^0.8.4;

contract Market {
    public address productAddr;

    constructor(address addr) {
        productAddr = addr;
    }

    function foo() external {
        require(msg.sender == productAddr, "nope!");
        // do something...
    }
}

Accordingly, while testing the foo method, I'd need to ensure the call is made from a Product contract instance, otherwise, it'll revert. How do I do that with ethers.js?

I have tried using .connect, which expects a Signer or Provider as parameter, like so…

const signers = await ethers.getSigners();
const ProductFactory = await ethers.getContractFactory('Product', signers[0]);
const MarketFactory = await ethers.getContractFactory('Market', signers[0]);
const product = await ProductFactory.deploy();
const market = await MarketFactory.deploy(product.address); // store product address

market.connect(product.signer).foo();  // not working :-(

…but it reverts, as product.signer returns a reference to signers[0], not a reference to the contract I'd like to present as caller, obviously.

How should I proceed? Any help would be greatly appreciated and would broaden my knowledge.

Best Answer

contract.signer returns the signer of the contract deployer account, not the signer of the contract's own address. It's going to be really difficult to impersonate an account address, as the impersonated account would need to contain ETH to pay for the any transaction generated through it. So I would imaging you would first need to expect what the contract address would be (maybe using CREATE2), and then send it some ETH, then create the contract at that address you sent ETH to and impersonate it to generate that transaction. If you do it the other way around, the contract address most likely would have a fallback function to refuse accepting any sent ETH. You'll need to use Hardhat to be able to impersonate an account, here's how to do it: https://hardhat.org/hardhat-network/reference/#hardhat-impersonateaccount