Unidentified contract: OpenSea is unable to "understand" ERC-1155

Hey guys. I have deployed a ERC-1155 based contract and minted some NFTs on this contract successfully. But when I want to use these NFTs in OpenSea, it always says "Unidentified contract".

Example: https://testnets.opensea.io/assets/0xc7d3e4a5A0c3e14ba8C68ea1b8a99a9dBf3ca76F/2

API-Example: https://testnets-api.opensea.io/api/v1/asset/0xc7d3e4a5A0c3e14ba8C68ea1b8a99a9dBf3ca76F/2/?force_update=true

Following their official Tutorial repository (which does not compile any more because of outdated dependencies and other issues) I have added some (maybe) opensea-specific functions and data that might required for OpenSea in order to work properly. However, OpenSea is able to grab all required data to display an NFT, but as long as they say "Unidentified contract", this all makes no sense so far.

My question has:

has someone already managed to deploy a ERC-1155 and used it with OpenSea properly without this issue? Is there anything we have to "register" somehow contracts that are not based on ERC-721?

:1234: Code to reproduce

I have commented out some specific parts that was taken from their own tutorial which does not work without their deprecated dependencies (a very old implementation "multi-token-standard") to just keep the ABI constant in case it would matter (it does not).

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


contract OwnableDelegateProxy { }

contract ProxyRegistry {
  mapping(address => OwnableDelegateProxy) public proxies;
}


contract MetaCoin is ERC1155, AccessControl, Pausable, ERC1155Burnable {
    bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    address proxyRegistryAddress;

    uint256 private _currentTokenID = 0;
    mapping (uint256 => address) public creators;
    mapping (uint256 => uint256) public tokenSupply;

    /**
    * @dev Require msg.sender to be the creator of the token id
    */
    modifier creatorOnly(uint256 _id) {
        require(creators[_id] == msg.sender, "ERC1155Tradable#creatorOnly: ONLY_CREATOR_ALLOWED");
        _;
    }

    /**
    * @dev Require msg.sender to own more than 0 of the token id
    */
    modifier ownersOnly(uint256 _id) {
        //require(balances[msg.sender][_id] > 0, "ERC1155Tradable#ownersOnly: ONLY_OWNERS_ALLOWED");
        _;
    }

    constructor(address _proxyRegistryAddress) ERC1155("https://abcoathup.github.io/SampleERC1155/api/token/{id}.json") {       
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(URI_SETTER_ROLE, msg.sender);
        _setupRole(PAUSER_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);

        proxyRegistryAddress = /*0xF57B2c51dED3A29e6891aba85459d600256Cf317; //*/ _proxyRegistryAddress;
    }

    function setURI(string memory newuri) public onlyRole(URI_SETTER_ROLE) {
        _setURI(newuri);
    }

  /**
    * @dev Returns the total quantity for a token ID
    * @param _id uint256 ID of the token to query
    * @return amount of token in existence
    */
  function totalSupply(
    uint256 _id
  ) public view returns (uint256) {
    return tokenSupply[_id];
  }

  /**
   * @dev Will update the base URL of token's URI
   * @param _newBaseMetadataURI New base URL of token's URI
   */
  function setBaseMetadataURI(
    string memory _newBaseMetadataURI
  ) public /*onlyOwner*/ {
    
  }



    function pause() public onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() public onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC1155, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function mint(address account, uint256 id, uint256 amount, bytes memory data)
        public
        onlyRole(MINTER_ROLE)
    {
        _mint(account, id, amount, data);
    }

    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
        public
        onlyRole(MINTER_ROLE)
    {
        _mintBatch(to, ids, amounts, data);
    }

    function _beforeTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
        internal
        whenNotPaused
        override
    {
        super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
    }

  /**
   * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-free listings.
   */
  function isApprovedForAll(
    address _owner,
    address _operator
  ) public override view returns (bool isOperator) {
    // Whitelist OpenSea proxy contract for easy trading.
    ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
    if (address(proxyRegistry.proxies(_owner)) == _operator) {
      return true;
    }

    return ERC1155.isApprovedForAll(_owner, _operator);
  }

  /**
    * @dev Change the creator address for given token
    * @param _to   Address of the new creator
    * @param _id  Token IDs to change creator of
    */
  function _setCreator(address _to, uint256 _id) internal creatorOnly(_id)
  {
      creators[_id] = _to;
  }

  /**
    * @dev Returns whether the specified token exists by checking to see if it has a creator
    * @param _id uint256 ID of the token to query the existence of
    * @return bool whether the token exists
    */
  function _exists(
    uint256 _id
  ) internal view returns (bool) {
    return creators[_id] != address(0);
  }

  /**
    * @dev calculates the next token ID based on value of _currentTokenID
    * @return uint256 for the next token ID
    */
  function _getNextTokenID() private returns (uint256) {
      _currentTokenID = _currentTokenID + 1;
    return _currentTokenID;
  }

  /**
    * @dev increments the value of _currentTokenID
    */
  function _incrementTokenTypeId() private  {
    _currentTokenID++;
  }

}

:computer: Environment

node: v16.7.0

deps:

"@openzeppelin/contracts": "^4.3.0",
"@nomiclabs/buidler": "^1.4.8",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^2.1.1",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/hardhat-upgrades": "^1.9.0",
"@typechain/ethers-v5": "^6.0.5",
"@typechain/hardhat": "^1.0.1",
"@types/chai": "^4.2.15",
"@types/chai-as-promised": "^7.1.3",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.37",
"chai": "^4.3.3",
"chai-as-promised": "^7.1.1",
"chai-datetime": "^1.8.0",
"ethereum-waffle": "^3.3.0",
"ethers": "^5.4.5",
"hardhat": "^2.6.1",
"hardhat-typechain": "^0.3.5",
"ts-generator": "^0.1.1",
"ts-node": "^9.1.1",
"typechain": "^4.0.3",
"typescript": "^4.2.4"

Finally found the root cause! OpenSea expects a public property called name in order to display the proper Name on the Collection instead of "Unidentified contract".

I came across this while looking at their reference code (which depends on a now 3-year-old MultiToken-Contract implementation and needs all in all some downgrades of Node and other tools in order to get it build [a downgrade to Node 10 worked best for me today] ).

6 Likes

However, this solution also has a big disadvantage: all NFTs minted by this contract are listed by OpenSea under the same collection, which name was identified by the name-variable of the contract.

Has anyone found out how to provide multiple collections with different names for each?

@itinance
Yeah the standard convention is to have a separate smart contract for each collection. That's what I've been noticing across all marketplaces.

Thank you for your answer I'm going to implement it as soon as I get home and see if this helps me as well.

Update: the latest standard is here.

You have to implement the contractURI method that returns a URI that OpenSea can use to fetch contract metadata.

The expected JSON looks like this:

{
"name": "OpenSea Creatures",
"description": "OpenSea Creatures are adorable aquatic beings primarily for demonstrating what can be done using the OpenSea platform. Adopt one today to try out all the OpenSea buying, selling, and bidding feature set.",
"image": "/image.png",
"external_link": "",
"seller_fee_basis_points": 100, # Indicates a 1% seller fee.
"fee_recipient": "0xA97F337c39cccE66adfeCB2BF99C1DdC54C2D721" # Where seller fees will be paid to.
}

4 Likes

Your update helped me a lot @Stevers, thank you very much!

That contract URI doesn't solve the issue you presented because you can only have one contract URI hence only one name.
It's not possible to have one ERC1155 to represent multiple collections.

Cool! thanks for solving the issue!

Check my collection: https://opensea.io/Ali3nSheep

you can have proxy contracts provide addresses to opensea then access a token id within the 1155.

the problem is that the tokens within an 1155 are not addressable like other tokens. that is they do not have an address that correlates 1 to 1 with a token within and 1155. So when you go to opensea and give it an address, instead of giving it the address of your 1155, give it the address of a proxy, that maps to a single token in the 1155.

The proxy can expose a local contractUri as well..it should be able to provide a full interface to the opensea API within a proxy then a inheriting from an abstract contract to model the opensea API.

That is a valid point. It is torture trying to reign in the variation of metadata. I tried this with the top 20 NFTs. Complete minefield token_id is suddenly fluff_id, other creators have added further categories and others have cut down on what would be a considered standard. The name, just as you mentioned some people even attempt to put a name and a symbol in the ERC1155 too.

I did realize, standardization really has no place here and I need to code around it.