Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC721 "automatic" token URI #1745

Closed
abcoathup opened this issue May 10, 2019 · 19 comments · Fixed by #1970 or #2174
Closed

ERC721 "automatic" token URI #1745

abcoathup opened this issue May 10, 2019 · 19 comments · Fixed by #1970 or #2174
Labels
breaking change Changes that break backwards compatibility of the public API. contracts Smart contract code. feature New contracts, functions, or helpers.
Milestone

Comments

@abcoathup
Copy link
Contributor

abcoathup commented May 10, 2019

🧐 Motivation
Current ERC721 minting with token URI requires manually specifying the URI per token.

To reduce gas cost, the OpenSea example uses concatenation of a base token URI and the tokenID. (TradeableERC721Token.sol)

📝 Details

We want to provide an ERC721 extension ERC721DefaultURI with a base URI parameter, which overrides ERC721Metadata's tokenURI() with the concatenation. To discuss the API please comment on the forum thread about this.

@nventuro nventuro added contracts Smart contract code. feature New contracts, functions, or helpers. labels May 10, 2019
@frangio frangio changed the title Add support for ERC721 token URI concatenation of base token URI and tokenID ERC721 extension with "automatic" token URI May 14, 2019
nventuro pushed a commit that referenced this issue May 27, 2019
* Feature Issue #1745

* Feature Issue #1745 remove whitespace in contract

* Feature Issue #1745 fix Solidity linter issues

* Feature Issue #1745 fix JS lint issues

* Update contracts/drafts/Strings.sol

Co-Authored-By: Nicolás Venturo <nicolas.venturo@gmail.com>

* Update contracts/drafts/Strings.sol

Co-Authored-By: Nicolás Venturo <nicolas.venturo@gmail.com>

* Update contracts/drafts/Strings.sol

Co-Authored-By: Nicolás Venturo <nicolas.venturo@gmail.com>

* Updates based on PR feedback

* Remove trailing whitespace

* Update tests based on @nventuro feedback

* Removed return name

* Rename length as suggested

* Rename temp variables in uint256ToString

* Renamed bytes variable to buffer

* Change concatenate to use abi.encodePacked

* Moved OraclizeAPI reference to unit256ToString

* Add emoji concat test

* Remove concatenate

* Remove concatenate from StringsMock and test

* Rename function to fromUint256

* Update StringsMock.sol
@frangio
Copy link
Contributor

frangio commented Jul 13, 2019

Note that for concatenation of the base URI with the tokenId string we will use abi.encodePacked.

@logeekal
Copy link
Contributor

As discussed in #1163 , I can take this up.

@pointtoken
Copy link

@logeekal Will your implementation make the baseTokenURI settable? I think this would be good, in the event that the URL needs to change. Is this code in a branch anywhere?

@logeekal
Copy link
Contributor

Hello @pointtoken, I will start implementation today so no code branch as of now. And I am assuming that it should be settable so that everyone can set there own URI probably in constructor of their contract so that they do not have to do that with every mint operation.

What do you think @abcoathup, In Blockhorses I see that you have hard-coded it, so to be generic I also think it should be settable as well.

@abcoathup
Copy link
Contributor Author

Hi @logeekal

I agree, I think that a private _baseTokenURI state variable should be set in the constructor via a parameter.

There should also be an internal setter function, so tokens inheriting from this contract can create a mechanism to change the _baseTokenURI as required.

In PeepethBadges.sol we had the private state variable _baseTokenURI that was set in the constructor, (though not via a parameter). We also had a mechanism for the owner to change the baseTokenURI, and I could see some projects wanting this ability to have some Role have the ability to change the baseTokenURI.

@pointtoken
Copy link

Once one embraces the idea of offchain metadata for an NFT, one is embracing the idea that the NFT has data which could change. As such, the URL itself of the metadata needs to be change-able. Classic example: the company that created the contract gets sold or folds or who knows what, and the the metadata server itself is no longer available. There could be a scenario where a new metadata server is deployed, thus the need to update the tokenURI, keeping the NFT's metadata URL to be accessed onchain. Otherwise, that contract become much less usable for people who hold the actual NFT.

@pointtoken
Copy link

Here's some code, although there may be better ways now:

` string public tokenURIPrefix = "https://api.nft.com/";

function updateTokenURIPrefix(string newPrefix) external onlyOwner {
    tokenURIPrefix = newPrefix;
}

function tokenURI(uint256 _tokenId) public view returns (string) {
return string(abi.encodePacked(tokenURIPrefix,uint2str(_tokenId)));
}

function uint2str(uint i) internal pure returns (string) {
    if (i == 0) return "0";
    uint j = i;
    uint length;
    while (j != 0){
        length++;
        j /= 10;
    }

    bytes memory bstr = new bytes(length);

    uint k = length - 1;
    while (i != 0){
        bstr[k--] = byte(48 + i % 10);
        i /= 10;
    }

    return string(bstr);

}

`

@abcoathup
Copy link
Contributor Author

Hi @pointtoken

There is already a Strings library in drafts with a fromUint256 function.
https://github.com/OpenZeppelin/openzeppelin-solidity/blob/c5c0e22c8963971381ba8e68198cf3ef0131ae3e/contracts/drafts/Strings.sol#L13

I suggest that the _tokenURIPrefix state variable be private (and hence have an underscore) and that the setter _updateTokenURIPrefix be internal.

That way, inheriting contracts have the option to create an updateTokenURIPrefix function if they desire, with whatever Role or Owner they prefer to use. Projects can also choose not to have an update.

I agree that having metadata off chain and based on a URI rather than being content addressable means that the metadata is centralized.

@pointtoken
Copy link

ok, as long that it is easy to make public and settable by an owner of the contract

@pointtoken
Copy link

Just curious the status of this feature @abcoathup @logeekal -- timeframe when it might get merged to master?

@abcoathup
Copy link
Contributor Author

@pointtoken I haven't done any more beyond the Strings library. I would love to see this implemented.

@logeekal are you still working on this?

@nventuro
Copy link
Contributor

@nachomazzara from Decentraland commented that it is currently not possible for users to easily implement this feature (which they need) due to tokenURI() being external, which makes it impossible to call the base implementation via super. The workaround they've found is to override _setTokenURI and use a separate mapping, which is far from ideal and will probably be disallowed once Solidity 0.6.0 comes out.

If @logeekal will not end up tackling this, someone else should do it: we already have a concrete design proposal, and many users would be benefited by this feature.

@logeekal
Copy link
Contributor

I apologize that I completely forgot about this and got involved in other task. I will take it up next week and get back with updates.

@nachomazzara
Copy link
Contributor

nachomazzara commented Oct 18, 2019

To put in here what we thought (omit non-changed code):

pragma solidity ^0.5.0;

import "../../GSN/Context.sol";
import "./ERC721.sol";
import "./IERC721Metadata.sol";
import "../../introspection/ERC165.sol";

contract ERC721Metadata is Context, ERC165, ERC721, IERC721Metadata {
    //...
    
    // Base Token URI
    string private _baseTokenURI;

    //...
    
    // **PROPOSED CHANGE**: 
    // - `external` to `public` (Allow 'super')
    // - Concat baseURI with tokenURI
    function tokenURI(uint256 tokenId) public view returns (string memory) { 
        return string(abi.encodePacked(_baseTokenURI, _tokenURI(tokenId)));
    }
    

    /**
     * @dev Internal returns an URI for a given token ID.
     * Throws if the token ID does not exist. May return an empty string.
     * @param tokenId uint256 ID of the token to query
     */
    function _tokenURI(uint256 tokenId) internal view returns (string memory) { 
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return _tokenURIs[tokenId];
    }
    
    /**
     * @dev Internal function to set the base token URI.
     * @param uri string base URI to assign
     */
    function _setBaseTokenURI(string memory uri) internal {
        _baseTokenURI = uri;
    }
}

It continues following the _INTERFACE_ID_ERC721_METADATA = 0x5b5e139f;.

The idea is not only to use the tokenId as the path for the URI as used here but just to have full control of what to save.

_setBaseTokenURI should be exposed to authorized users.

I'm wondering if we should emit an event once the baseTokenURI is changed. Indexers can be reactive to this, updating all the metadata references.

@nachomazzara
Copy link
Contributor

Added a PR

@abcoathup
Copy link
Contributor Author

Hi @nachomazzara,

Thank you 🙏

This solution gives flexibility, e.g. allows saving an IPFS hash, with the base URI being an IPFS gateway URI, where the base URI can be changed to support a change of IPFS gateway.

An example ERC721 token using this functionality as I envisaged could then be:
(perhaps we need to add a SimpleERC721Token.sol to examples?).

pragma solidity ^0.5.0;

import "../token/ERC721/ERC721Full.sol";
import "../token/ERC721/ERC721Mintable.sol";
import "../drafts/Strings.sol";

/**
 * @title MyERC721
 */
contract MyERC721 is ERC721Full, ERC721Mintable {
    constructor () public ERC721Mintable() ERC721Full("MyERC721", "MY721") {
        // solhint-disable-previous-line no-empty-blocks
        _setBaseTokenURI("https://baseUri/");
    }

    /**
     * @dev Function to mint tokens
     * @param to The address that will receive the minted tokens.
     * @return A boolean that indicates if the operation was successful.
     */
    function mint(address to) public onlyMinter returns (bool) {
        uint256 tokenId = totalSupply().add(1);
        _mint(to, tokenId);
        return true;
    }

    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "MyERC721: URI query for nonexistent token");
        return string(abi.encodePacked(_baseTokenURI(), Strings.fromUint256(tokenId)));
    }
}

@abcoathup
Copy link
Contributor Author

Reopening as #1970 doesn't implement an "automatic" token URI. (but is the basis for adding this functionality.

e.g.:

function tokenURI(uint256 tokenId) public view returns (string memory) {
     return string(abi.encodePacked(baseTokenURI(), Strings.fromUint256(tokenId)));
}

Should we be creating an ERC721AutomaticTokenURI contract or is there a better way to handle this? (e.g. creating an example of how to implement?)

@frangio
Copy link
Contributor

frangio commented Mar 30, 2020

As mentioned in #2154, we want to bundle this by default in ERC721.

@frangio frangio added the breaking change Changes that break backwards compatibility of the public API. label Mar 30, 2020
@frangio frangio added this to the v3.0 milestone Mar 30, 2020
@frangio frangio changed the title ERC721 extension with "automatic" token URI ERC721 "automatic" token URI Mar 30, 2020
@KaiRo-at
Copy link
Contributor

FWIW, I proposed a PR for this in #2174 based on the great work @frangio did in #1970.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Changes that break backwards compatibility of the public API. contracts Smart contract code. feature New contracts, functions, or helpers.
Projects
None yet
7 participants