Soulbound NFT migration from v4.9 to v5.0

I am trying to migrate the current soulbound functionality of my NFT-ERC721 contract from v.4.9. to v5.0. Q:Can you give me a hint on how to ensure the soulbound functionaility in v5.0 ?

On openzeppelin version 4.9. Ive used:

function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override virtual {
  require(from == address(0) || to == address(0), "Token transfer not allowed");
  super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override virtual {
   if (from == address)) {
       emit Attest(to, firstTokenId);
   } else if ( to == address(0)) {
       emit Revert(to, firstTokenId);
    super._afterTokenTransfer(from, to, firstTokenId, batchSize);

The new v5.0 introduced the _update function. The documentation (press release?: source) mentions: "In 5.0, token hooks were removed from ERC20, ERC721, and ERC1155, favoring a single _update function. This can also be overridden for custom behaviors in any mint, transfer, or burn operation."

With the example:

I dont know how to ensure the soulbound functionality, additionally I dont understand the auth functionalitiy by ERC721.

1 Like

Hey, I am also stuck in a situation similar to this. Did you get ahead of this blocker, if yes, could you please share how to resolve this?


One solution would be to override the transferFrom() method and revert, since starting from v5.0, the safeTransferFrom() method also relies on it under the hood - e.g.:

function transferFrom(address from, address to, uint256 tokenId) public virtual override(ERC721, IERC721) {
  revert ERC721TransferNotAllowed();

FYI: have a look at these lines of code.

Simply override the _update from ERC721. When a transfer is attempted, the if statement checks if the token has an existing owner (from != address(0) ) and is not being sent to a zero address (to != address(0) ). If both conditions are met, it implies a valid transfer attempt.

In our scenario, the function is configured to fail any transfer attempt to a non-zero address, as indicated by the revert("Soulbound: Transfer failed"); statement. This ensures that transfers will be successful only when directed to the zero address, aligning with the burnable functionality of the token.

In other words, you will have a soulbound token which is burnable.

     * @dev Internal function to handle token transfers.
     * Restricts the transfer of Soulbound tokens.

    function _update(address to, uint256 tokenId, address auth)
        override(ERC721, ERC721Pausable)
        returns (address)
        address from = _ownerOf(tokenId);
        if (from != address(0) && to != address(0)) {
            revert("Soulbound: Transfer failed");

        return super._update(to, tokenId, auth);

If you don't want to have a burnable token, you can adjust the conditions so that a mint is enabled but any other transfer fails.

    if (from != address(0)) {
        revert("Soulbound: Transfer failed");  // Prevent all transfers, making the token soulbound and non-burnable