Difficulty Overriding safeTransferFrom in @openzeppelin/contracts-upgradeable@5.2.0

Dear OpenZeppelin Support Team,

I am writing to request assistance regarding an issue I am experiencing while developing an upgradeable ERC721 token intended to be soulbound (i.e., non-transferrable except for burning). My project is built using Hardhat, and I am using @openzeppelin/contracts-upgradeable@5.2.0 along with @openzeppelin/hardhat-upgrades. I have ensured that my contract imports only from the upgradeable package and have removed direct dependencies on @openzeppelin/contracts.

Despite these precautions, when I attempt to override the safeTransferFrom (and transferFrom) functions in my upgradeable contract to enforce soulbound behavior, I receive the following compilation error:

"Trying to override non-virtual function. Did you forget to add 'virtual'?"

After investigating, I discovered that in my installation of @openzeppelin/contracts-upgradeable version 5.2.0, the safeTransferFrom function in ERC721Upgradeable.sol is defined without the virtual keyword. It appears that the upgradeable package is internally referencing code from @openzeppelin/contracts, which lacks virtual declarations on safeTransferFrom (and related functions), thereby preventing me from overriding them in my contract.

My upgradeable contract is fully connected to and dependent upon the OpenZeppelin libraries, and I have verified that my package.json includes only the upgradeable package (with the non-upgradeable @openzeppelin/contracts removed). I have also reinstalled my node modules and cleared the lockfile to ensure a fresh environment. Despite these efforts, the compiler continues to reference a version of safeTransferFrom that is not overrideable.

Could you please advise:

  • Is this behavior expected in @openzeppelin/contracts-upgradeable@5.2.0 due to internal dependencies on non-upgradeable code?
  • If so, what is the recommended approach for enforcing soulbound behavior in an upgradeable ERC721 token? I ideally would like to override safeTransferFrom (or another appropriate hook such as _transfer or _beforeTokenTransfer) to block transfers while still allowing minting and burning.
  • Are there any workarounds or future plans to mark these functions as virtual so that they can be overridden?

Any guidance or recommendations you can provide would be greatly appreciated, as a soulbound token is a non-negotiable requirement for our application.

Thank you very much for your time and assistance.

Best regards,
Outlaw

Hi, welcome to the community! :wave:

I think you can override _beforeTokenTransfer to have a try.

Hi,

Thank you for the welcome, glad to be here!

I attempted the recommended approach of overriding _beforeTokenTransfer (as well as safeTransferFrom and _transfer) to enforce soulbound behavior. However, after careful investigation, I've found that in @openzeppelin/contracts-upgradeable version 5.2.0 the implementation of safeTransferFrom (and related transfer functions) is based on underlying code from @openzeppelin/contracts that is not marked virtual. This means these functions simply cannot be overridden in this release.

In concrete terms, the code in ERC721Upgradeable.sol from version 5.2.0 defines safeTransferFrom without the virtual keyword, which is why the override fails. It isn’t an issue with my contract code—it’s a limitation in how this specific version of the upgradeable library is implemented.

So, while the idea of overriding _beforeTokenTransfer is conceptually sound, it isn’t feasible with the current release. At this point, my options are to either implement a custom minimal ERC721 to enforce soulbound behavior or wait for a future update that marks these functions virtual.

I see, you are using the version 0.5.2 like this, https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/release-v5.2/contracts/token/ERC721/ERC721Upgradeable.sol

so I think you can override the function _update