Function _setTokenURI() in ERC721 is gone with pragma ^0.8.0

I’ve been using function _setTokenURI(_tokenId, _tokenURI) from ERC721 to set the URI in the new NFT, which updates the mapping _tokenURIs allowing to retrieve its content through function tokenURI(uint256 tokenId).

However, since pragma ^0.8.0, these functions have been removed and there is now the function tokenURI(uint256 tokenId) that returns either the concatenation of function _baseURI() with the tokenID or an empty string “”. Therefore:

  1. How do we store the URI?
  2. How do we retrieve the URI and not its concatenation with the tokenID?
  3. Do we have to override function _baseURI() and return there a mapping that we have to create to store the URI? (as we had in version 0.7.x or below).

I think a code example of setting and retrieving the URI with the ERC721 pragma 0.8 changes would be highly appreciated (besides the examples already existing where only the tokenID and name are set).

2 Likes

Hi @sjuanati,

OpenZeppelin Contracts 4.0 Beta was released this week.
It is a Beta release and there may be small breaking changes prior to the stable release. There isn't documentation on this yet.

Feedback on the Beta is greatly appreciated.

When you extend ERC721 you can add this functionality however you need. You can override tokenURI with your required logic.

You could use the implementation in OpenZeppelin Contracts 3.x. as a guide.

You can override tokenURI with your required logic when you extend ERC721.

You only need to override _baseURI if you want to use this information in your tokenURI function, such as concatenating _baseURI + _tokenURI. If you are only storing a _tokenURI then you could just return a _tokenURI.

2 Likes

Hi @abcoathup,

Many thanks for your reply. I assume the goal of this change is to give the user more flexibility to manage the baseURI and tokenURI in a more flexible way, such as having arrays of URIs for the same token ID, etc.

I would like to share a contract I just created by trying to follow your feedback:

pragma solidity ^0.8.1;

    import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol';
    import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol';

    contract Example is ERC721, Ownable {
        
        using Strings for uint256;
        
        // Optional mapping for token URIs
        mapping (uint256 => string) private _tokenURIs;

        // Base URI
        string private _baseURIextended;


        constructor(string memory _name, string memory _symbol)
            ERC721(_name, _symbol)
        {}
        
        function setBaseURI(string memory baseURI_) external onlyOwner() {
            _baseURIextended = baseURI_;
        }
        
        function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
            require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
            _tokenURIs[tokenId] = _tokenURI;
        }
        
        function _baseURI() internal view virtual override returns (string memory) {
            return _baseURIextended;
        }
        
        function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
            require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

            string memory _tokenURI = _tokenURIs[tokenId];
            string memory base = _baseURI();
            
            // If there is no base URI, return the token URI.
            if (bytes(base).length == 0) {
                return _tokenURI;
            }
            // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
            if (bytes(_tokenURI).length > 0) {
                return string(abi.encodePacked(base, _tokenURI));
            }
            // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
            return string(abi.encodePacked(base, tokenId.toString()));
        }
        

        function mint(
            address _to,
            uint256 _tokenId,
            string memory tokenURI_
        ) external onlyOwner() {
            _mint(_to, _tokenId);
            _setTokenURI(_tokenId, tokenURI_);
        }
    }

I assume the implementation may vary depending on the needs. In this case, the same baseURI would apply for all tokens, and just one tokenURI would be assigned per tokenID.

Please let us know if this is aligned with the goal of the Contracts 4.0 or there are parts that could be improved or are redundant.

1 Like

Hello @sjuanati

Your Example implementation looks fine. You succesfully re-enabled the features that used to be part of the “basic” ERC721 implementation, and with were removed (just like the Enumerability part) to make simple ERC721 lighter and thus less expensive to deploy.
Do you think this URI management should be proposed, in the repo, as an extension to ERC721 ?

3 Likes

Hi @Amxx

Thanks for your feedback :wink:

I definitely think it is a good idea to keep the URI management as an extension in order to smooth the transition to v4.0. This would allow developers to use the extension for most of the cases, but still customise its management in case of a different requirement.

1 Like

Hi @sjuanati,

I like the idea of an extension for storing token URI, especially for projects wanting to use decentralized storage.

Having a very quick look at your example (I may not have have spotted everything, and I am a community manager and not a security researcher):

  • I would only include functionality that you need (e.g. in tokenURI).

  • You could use AccessControl rather than ownable, so you could define a MINTER_ROLE and a SET_BASE_URI_ROLE: https://docs.openzeppelin.com/contracts/3.x/access-control.

  • In the require checks, the message should be updated to use the name of your contract, e.g. : Example: URI set of nonexistent token

  • I prefer to use Counters to increment the token ID, but this costs extra gas, so may be better to generate the token ID off chain.


As an aside you can: Format code in the forum

1 Like

@sjuanati I’m curious what do your token URIs look like? Have you considered URIs of the form baseURI + "/" + tokenId?

1 Like

Hi @abcoathup,

You are right with your suggestions. I actually used a very simple contract version to show the essentials for the base and uri management, but it lacks all the rest :wink:

As for the counter, perhaps I prefer to use just an internal counter and increment when minting; however, if tokens can be burnt, then Counter.sol adds the right check to avoid going below zero.

1 Like

Hi @frangio ,

Yes! I am certainly using a baseURI to provide an IPFS address and then adding the tokenIds, so that all points to a JSON file.

Below is an output example (for tokenId = 2):

https://gateway.pinata.cloud/ipfs/QmYFmJgQGH4uPHRYN15Xdv4aLd9o4Aq63y1e4GgN6kj5aK/2

2 Likes

In that kind of setup what role does _setTokenURI have? Isn’t it sufficient to use the new default mechanism?

1 Like

I see two use-cases in which _setTokenURI might be useful:

  1. If we need to build the URI considering a different name than the sequential tokenId (for instance, adding category or group prefix before the tokenId, or even using identifiers such as UUID-like).

  2. If we need to have different URIs per token type instead of having every token using the same baseURI (eg: using a path for hi-resolution pics and a different path for low-resolution pics associated with the NFT).

In summary, if the baseURI + '/' + tokenId is sufficient, I agree the current implementation should be fine. If this is not enough, then we either override function tokenURI() or use an extension if implemented.

2 Likes

@sjuanati

The latest preview (4.0.0-beta.1) includes ERC721URIStorage, an ERC721 extension, that should answer this issue.

Let us know if this is satisfactory or if you need something else.

2 Likes

I’m having trouble using the example on 4.0.0-beta.1’s documented example.

DeclarationError: Undeclared identifier.
  --> PATH/GOES/HERE/file.sol:49:9:
   |
49 |         _setTokenURI(newItemId, tokenURI);
   |         ^^^^^^^^^^^^

If I import and derive ERC721URIStorage on that file I get multiple override errors. Where should I be importing and including ERC721URIStorage? Sorry, I’m pretty new to this.

I’ve tried extension ERC721URIStorage from the latest preview (4.0.0-beta1) and everything seems to be working just fine!

@AnkerFaster I leave here a minimalistic code to try the extension, in case it helps:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.0/contracts/token/ERC721/extensions/ERC721URIStorage.sol';

contract Test is ERC721URIStorage {
    string baseURI;
    
    constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {}
    
    function mint(
        address account,
        uint256 tokenId
    ) external {
        _mint(account, tokenId);
    }
    
    function setTokenURI(
        uint256 tokenId, 
        string memory tokenURI
    ) external {
        _setTokenURI(tokenId, tokenURI);
    }
    
    function setBaseURI(string memory baseURI_) external {
        baseURI = baseURI_;
    }
    
    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }
}
2 Likes

Feel free to ask for help with anything!

I imagine you did something like contract ... is ERC721, ERC721URIStorage ?

This is good, but it will result in the override errors you mentioned. They can be worked around following the recommendations in the error message, but they're pretty verbose. If you instead inherit ERC721URIStorage only (as in @sjuanati's post above!) you will get all of ERC721 functionality without all the override errors.

1 Like

i think the example code here needs to be updated ERC721 - OpenZeppelin Docs as it gives a setTokenURI error.

1 Like

Hi @scottsuhy,

Welcome to the community :wave:

Good spot. I have created an issue for this: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2614

You would be very welcome to create a PR to resolve.

1 Like

I wish I could @abcoathup . I’m just getting started with Solidity. Give me a few weeks and I may be ready to help on that front.

1 Like

Hi @scottsuhy,

Welcome to the world of Solidity!

It is never too early to start contributing to open source. :smile:
Updating documentation is a great way to start. My pitch is in: Solidity learning resources.

Thanks for the help! Sorry for the late reply. That cleared everything up :slight_smile: It also helped me out on another issue and another contract. I’m not used to working in heavily OOP styled code. Thanks again!

1 Like