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:
How do we store the URI?
How do we retrieve the URI and not its concatenation with the tokenID?
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).
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.
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.
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 ?
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.
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
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.
I see two use-cases in which _setTokenURI might be useful:
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).
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.
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 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.
It is never too early to start contributing to open source.
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 It also helped me out on another issue and another contract. I’m not used to working in heavily OOP styled code. Thanks again!