2

I have a smart-contract that inherits from ERC721, as follows:

contract myNFTsMinter is ERC721, Pausable, Ownable, ReentrancyGuard {
   ... 
}

In my javascript file (I'm using Hardhat + ethers.js) I instantiate my contract and then successfully call various functions from it - including functions it inherits from the ERC721 contract, such as balanceOf().

Here's how I do it:

await myNFTsMinterContractInstance.balanceOf("0x12345...").then((resultNFTsOwned) => {
   ... 
}

This works great - HOWEVER, when I call the safeTransferFrom() function, which is also from the ERC721 contract that my contract is inheriting from, I suddenly get the following error:

Uncaught (in promise) TypeError: myNFTsMinterContractInstance.safeTransferFrom is not a function

Why is that? What's happening here? How come it's recognizing one ERC721 function (balanceOf()) but not another (safeTransferFrom())?

I've found that the only work-around is to create an additional function in my own contract (meaning in the myNFTsMinter contract), which in turn calls the ERC721 contract's safeTransferFrom(), and only then I can leverage the functionality I need from within my JS file.

But obviously adding this function to my contract makes it fatter and more costly to deploy - and obviously this doesn't provide me with the insight and understanding of what's happening here in terms of the inheritance anomaly.

Any thoughts?

=================================

UPDATED QUESTION:

Responding to the answer given by Mila A:

Regarding the very first point you made in your answer, about how I initialize my ERC721 contract in my constructor, well I have NOT been doing it the way you’ve suggested. The way I’ve been doing it is like this:

   constructor() ERC721(“myERC721TokenContract”, “MYTK”) {
      … 
   }

That very different from how you're saying to do it.

And really, the whole thing starts in my deployment script, which looks like this:

   const myNFTsMinter = await ethers.getContractFactory("myNFTsMinter");
   const myNFTsMinterInstance = await myNFTsMinter.deploy();
   await myNFTsMinterInstance.deployed();

   console.log("\n\n>>FINISHED 'myNFTsMinterInstance' deployment!\n\n-'myNFTsMinterInstance' deployed to:", myNFTsMinterInstance.address);

So this deployment script is deploying my "myNFTsMinter" contract, and then the constructor of my "myNFTsMinter" contract deploys the ERC721 Contract on the fly the way it's shown in the code I just included above.

Is the way I'm doing it wrong?

To be clear: I don't have an already deployed ERC721 Contract sitting out there that I'm trying to create a reference to from my new contract. I'm trying to deploy both at the same time.

Should I not be doing that? What's the right way to do this?

2 Answers 2

1

First of all, make sure you follow this initialization convention:

constructor(address initialOwner)
        ERC721("MyToken", "MTK")
        Ownable(initialOwner)
    {}

The ERC721(...) call in the constructor is an alternative syntax for super(...).


Back to your problem. It seems like a possible overloading problem here. See https://github.com/ethers-io/ethers.js/issues/1160

That explains why you're only struggling with WRITE methods, and why READ methods work as expected.

It's more likely a problem with ethers.js than OpenZeppelin. You can anyways check the ABI of the compiled myNFTsMinter contract and see whether safeTransferFrom is available there.

So you can try it like this:

await myNFTsMinterContractInstance['safeTransferFrom(address,address,uint256)'](accountUser, newOwner, idNFT)

Conclusion: it's an ethers.js-specific bug targeting specifically WRITE methods that are not overriden on the lower inheritance level.


Further contents were updated to address Mark's concerns:

#1. Re:

 constructor() ERC721(“myERC721TokenContract”, “MYTK”) {
      … 
   }

I made this point because the latest OpenZeppelin implementation (v5.0.0) requires you to explicitly call the Ownable(...) constructor when implementing the inheriting contracts, because the compilation would throw an error.

See this code block from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/17a8955cd8ed2c9a269421a11c2e2774b796e305/contracts/access/Ownable.sol#L40:

/**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

That revert was added to prevent issues with contract ownership, e.g. when the initialOwner isn't expected to be the deployer, to prevent confusion of that kind.

So as long as you're using OZ 5.0.0, if you don't instantiate the Ownable(...) contract explicitly in the child contract, you will get an error during compilation: TypeError: No arguments passed to the base constructor. Specify the arguments or mark "MyToken" as abstract.


Are you using an older version of OZ in your project?


#2. Re your JS script, it looks good, I don't think any changes are required here, except a fix here where you need to pass the current contract owner:

   const myNFTsMinter = await ethers.getContractFactory("myNFTsMinter");
   const myNFTsMinterInstance = await myNFTsMinter.deploy(signer.getAddress());
   await myNFTsMinterInstance.deployed();

   console.log("\n\n>>FINISHED 'myNFTsMinterInstance' deployment!\n\n-'myNFTsMinterInstance' deployed to:", myNFTsMinterInstance.address);

You should pass the address that will be used as the initialOwner as a string.

2
  • 1
    OK you made some interesting points. I had to go and edit my original question to address what you said cause there isn't enough room here in this comments section to address your points properly. Please check out my original post - my new comments are at the bottom, and let me know your thoughts? I'd very much appreciate it!
    – Mark55
    Commented Feb 7 at 23:08
  • Sure, I read your new comments and updated my answer here. Please see above.
    – Mila A
    Commented Feb 8 at 13:00
0
  1. Check contract interface instance
  2. Check ABI
  3. If the function is OVERLOADED you SHOULD access it through contract['fnname(..abi_args)'](..args)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.