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

Add ERC1155URIStorage #3210

Merged
merged 21 commits into from Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -19,6 +19,7 @@
* `TimelockController`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by timelocks. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230))
* `Initializable`: add a reinitializer modifier that enables the initialization of new modules, added to already initialized contracts through upgradeability. ([#3232](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3232))
* `Initializable`: add an Initialized event that tracks initialized version numbers. ([#3294](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3294))
* `ERC1155URIStorage`: add a new extension for `ERC1155` that implements a `ERC721`-style `_setTokenURI` and `tokenURI` behavior called `ERC1155URIStorage.setURI` and `ERC1155URIStorage.uri`, respectively.

### Breaking changes

Expand Down
22 changes: 22 additions & 0 deletions contracts/mocks/ERC1155URIStorageMock.sol
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./ERC1155Mock.sol";
import "../token/ERC1155/extensions/ERC1155URIStorage.sol";

contract ERC1155URIStorageMock is ERC1155Mock, ERC1155URIStorage {
constructor(string memory _uri) ERC1155Mock(_uri) {}

function uri(uint256 tokenId) public view virtual override(ERC1155, ERC1155URIStorage) returns (string memory) {
return ERC1155URIStorage.uri(tokenId);
}

function setURI(uint256 tokenId, string memory _tokenURI) public {
_setURI(tokenId, _tokenURI);
}

function setBaseURI(string memory baseURI) public {
_setBaseURI(baseURI);
}
}
70 changes: 70 additions & 0 deletions contracts/token/ERC1155/extensions/ERC1155URIStorage.sol
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/ERC1155URIStorage.sol)

pragma solidity ^0.8.0;

import "../../../utils/Strings.sol";
import "../ERC1155.sol";

/**
* @dev ERC1155 token with storage based token URI management.
* Inspired by the ERC721URIStorage extension
*/
abstract contract ERC1155URIStorage is ERC1155 {
using Strings for uint256;

// optional base URI
string private _baseURI = "";

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

/**
* @dev See {IERC1155MetadataURI-uri}.
*
* This implementation returns the concatenation of the `_baseURI`
* and the token-specific uri if the latter is set
*
* This allowes for the following behavior:
*
* the `ERC1155URIStorage._tokenURIs[tokenId]` is set
* → the result is the concatenation of `_baseURI` and `ERC1155URIStorage._tokenURIs[tokenId]`
* (keep in mind that `_baseURI` is empty per default)
*
* the `ERC1155URIStorage._tokenURIs[tokenId]` is NOT set
* → the result is `ERC1155._uri`
*
* neither `ERC1155URIStorage._tokenURIs[tokenId]` nor `ERC1155._uri` are set
* → the result is empty
*/
function uri(uint256 tokenId) public view virtual override returns (string memory) {
string memory _tokenURI = _tokenURIs[tokenId];

// If token URI is set, concatenate base URI and tokenURI (via abi.encodePacked).
if (bytes(_tokenURI).length > 0) {
return string(abi.encodePacked(_baseURI, _tokenURI));
}

// If there is no tokenURI, return the ERC1155.uri
return super.uri(tokenId);
}

/**
* @dev Sets `_tokenURI` as the tokenURI of `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function _setURI(uint256 tokenId, string memory _tokenURI) internal virtual {
_tokenURIs[tokenId] = _tokenURI;
Amxx marked this conversation as resolved.
Show resolved Hide resolved
emit URI(uri(tokenId), tokenId);
}

/**
* @dev Sets `baseURI` as the `_baseURI` for all tokens
*/
function _setBaseURI(string memory baseURI) internal virtual {
_baseURI = baseURI;
}
}
66 changes: 66 additions & 0 deletions test/token/ERC1155/extensions/ERC1155URIStorage.test.js
@@ -0,0 +1,66 @@
const { BN, expectEvent } = require('@openzeppelin/test-helpers');

const { expect } = require('chai');
const { artifacts } = require('hardhat');

const ERC1155URIStorageMock = artifacts.require('ERC1155URIStorageMock');

contract(['ERC1155URIStorage'], function (accounts) {
const [ holder ] = accounts;

const erc1155Uri = 'https://token.com/nfts/';
const baseUri = 'https://token.com/';

const tokenId = new BN('1');
const amount = new BN('3000');

describe('with base uri set', function () {
beforeEach(async function () {
this.token = await ERC1155URIStorageMock.new(erc1155Uri);
this.token.setBaseURI(baseUri);

await this.token.mint(holder, tokenId, amount, '0x');
});

it('can request the token uri, returning the erc1155 uri if no token uri was set', async function () {
const receivedTokenUri = await this.token.uri(tokenId);

expect(receivedTokenUri).to.be.equal(erc1155Uri);
});

it('can request the token uri, returning the concatenated uri if a token uri was set', async function () {
const tokenUri = '1234/';
const receipt = await this.token.setURI(tokenId, tokenUri);

const receivedTokenUri = await this.token.uri(tokenId);

const expectedUri = `${baseUri}${tokenUri}`;
expect(receivedTokenUri).to.be.equal(expectedUri);
expectEvent(receipt, 'URI', { value: expectedUri, id: tokenId });
});
});

describe('with base uri set to the empty string', function () {
beforeEach(async function () {
this.token = await ERC1155URIStorageMock.new('');

await this.token.mint(holder, tokenId, amount, '0x');
});

it('can request the token uri, returning an empty string if no token uri was set', async function () {
const receivedTokenUri = await this.token.uri(tokenId);

expect(receivedTokenUri).to.be.equal('');
});

it('can request the token uri, returning the token uri if a token uri was set', async function () {
const tokenUri = 'ipfs://1234/';
const receipt = await this.token.setURI(tokenId, tokenUri);

const receivedTokenUri = await this.token.uri(tokenId);

expect(receivedTokenUri).to.be.equal(tokenUri);
expectEvent(receipt, 'URI', { value: tokenUri, id: tokenId });
});
});
});