We had the following mapping to manage the URIs for ERC721 tokens in OpenZeppelin contracts until pragma ^0.7:

// Optional mapping for token URIs
mapping (uint256 => string) private _tokenURIs;

Therefore, we had a function to pass the token ID and URI in order to store the URI, so the call would be:

_setTokenURI(_tokenId, _tokenURI);

However, I see this whole functionality and associated checks to manage the URI is gone within OpenZeppelin contracts on pragma ^0.8.

Do you know whether it is now expected that we implement the URI management on our own? should it be OK to bring (copy & paste) those features from 0.7 into 0.8? if not, any recommendation?


I raised this question to the OpenZeppelin forum and they quickly replied with the following feedback:

  • It is a Beta release and there may be small breaking changes prior to the stable release. There isn’t documentation on this yet.
  • To store & retrieve the URI, you can override tokenURI with your required logic.
  • 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.
  • The URI management from previous versions could be still included as an extension in the final version (to be decided).

In any case, I shared with them an example on how the ERC721 should be implemented with the current v4.0 version using pragma ^0.8.x and it looked fine to them:

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_);

You probably want to use ERC721URIStorage, which is an extension of ERC721 that implements _setTokenURI(tokenId, _tokenURI)


Do you need to use a newer Solitude compiler? If not, stick with the pragma that works for you, by getting rid of the caret and specify a fixed version.

  • Yes, I do. I already used previous versions without any issue, but that's why I raise the question. This answer does not solve the problem as it is saying 'don't use the new version and go back to the previous one'. Feb 25 at 10:50

The ERC721Metadata specification specifies the following function declaration: It doesn't matter how you implement it.

function tokenURI(uint256 _tokenId) external view returns (string);

In the previous version of openzepplin, it was implemented as a mapping because it used a method of storing tokenURI by tokenId. In the latest version, the baseURI and tokenId are combined, so mapping is no longer necessary. The latest version declares both tokenURI and _baseURI as virtual as shown below, so it can be overridden in the inherited contract.

function tokenURI(uint256 tokenId) public view virtual override returns (string memory)

function _baseURI() internal view virtual returns (string memory)

It is improved in a more extensible way than fixing the implementation.

This is just an improvement of the implementation method, it doesn't matter whether the Solidity version is 0.7 or 0.8. So it's okay to bring your implementation from 0.7 to 0.8.

  • Thanks for the answer @smarteasy. However, still not fully clear to me: if I override function _baseURI(), I'll have to create the old mapping to return the URI for that tokenID (otherwise, it will return ""). In addition, function tokenURI will return the concatenation of tokenID and URI, but what I need is the URI only, so not sure how I should use such as concatenation. In summary, I still don't see the benefit of this change and looks like we now have to implement the mapping ourselves? Feb 25 at 18:01
  • The benefits are clear. We can create a batch of tokenURIs for each token with baseURI and tokenId. In your case, just use you're implementation. And override tokenURI(uint256 tokenId).
    – smarteasy
    Feb 25 at 23:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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