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 all 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
9 changes: 5 additions & 4 deletions CHANGELOG.md
Expand Up @@ -5,18 +5,19 @@
* `AccessControl`: add a virtual `_checkRole(bytes32)` function that can be overridden to alter the `onlyRole` modifier behavior. ([#3137](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3137))
* `EnumerableMap`: add new `AddressToUintMap` map type. ([#3150](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3150))
* `EnumerableMap`: add new `Bytes32ToBytes32Map` map type. ([#3192](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3192))
* `ERC20FlashMint`: support infinite allowance when paying back a flash loan. ([#3226](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3226))
* `ERC20Wrapper`: the `decimals()` function now tries to fetch the value from the underlying token instance. If that calls revert, then the default value is used. ([#3259](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3259))
* `draft-ERC20Permit`: replace `immutable` with `constant` for `_PERMIT_TYPEHASH` since the `keccak256` of string literals is treated specially and the hash is evaluated at compile time. ([#3196](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3196))
* `ERC1155`: Add a `_afterTokenTransfer` hook for improved extensibility. ([#3166](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3166))
* `ERC1155URIStorage`: add a new extension that implements a `_setURI` behavior similar to ERC721's `_setTokenURI`. ([#3210](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3210))
* `DoubleEndedQueue`: a new data structure that supports efficient push and pop to both front and back, useful for FIFO and LIFO queues. ([#3153](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3153))
* `Governor`: improved security of `onlyGovernance` modifier when using an external executor contract (e.g. a timelock) that can operate without necessarily going through the governance protocol. ([#3147](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3147))
* `Governor`: Add a way to parameterize votes. This can be used to implement voting systems such as fractionalized voting, ERC721 based voting, or any number of other systems. The `params` argument added to `_countVote` method, and included in the newly added `_getVotes` method, can be used by counting and voting modules respectively for such purposes. ([#3043](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3043))
* `Governor`: rewording of revert reason for consistency. ([#3275](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3275))
* `Governor`: fix an inconsistency in data locations that could lead to invalid bytecode being produced. ([#3295](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3295))
* `ERC20FlashMint`: support infinite allowance when paying back a flash loan. ([#3226](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3226))
* `TimelockController`: Add a separate canceller role for the ability to cancel. ([#3165](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3165))
* `draft-ERC20Permit`: replace `immutable` with `constant` for `_PERMIT_TYPEHASH` since the `keccak256` of string literals is treated specially and the hash is evaluated at compile time. ([#3196](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3196))
* `ERC20Wrapper`: the `decimals()` function now tries to fetch the value from the underlying token instance. If that calls revert, then the default value is used. ([#3259](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3259))
* `Governor`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by governors. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230))
* `TimelockController`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by timelocks. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230))
* `TimelockController`: Add a separate canceller role for the ability to cancel. ([#3165](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3165))
* `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))

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);
}
}
2 changes: 2 additions & 0 deletions contracts/token/ERC1155/README.adoc
Expand Up @@ -36,6 +36,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

{{ERC1155Supply}}

{{ERC1155URIStorage}}

== Presets

These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code.
Expand Down
62 changes: 62 additions & 0 deletions contracts/token/ERC1155/extensions/ERC1155URIStorage.sol
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT

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
*
* _Available since v4.6._
*/
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 enables the following behaviors:
*
* - if `_tokenURIs[tokenId]` is set, then the result is the concatenation
* of `_baseURI` and `_tokenURIs[tokenId]` (keep in mind that `_baseURI`
* is empty per default);
*
* - if `_tokenURIs[tokenId]` is NOT set then we fallback to `super.uri()`
* which in most cases will contain `ERC1155._uri`;
*
* - if `_tokenURIs[tokenId]` is NOT set, and if the parents do not have a
* uri value set, then 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).
return bytes(tokenURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenURI)) : super.uri(tokenId);
}

/**
* @dev Sets `tokenURI` as the tokenURI of `tokenId`.
*/
function _setURI(uint256 tokenId, string memory tokenURI) internal virtual {
_tokenURIs[tokenId] = tokenURI;
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 });
});
});
});