Browse Source

Add ERC1155URIStorage (#3210)

* Add ERC721URIStorage-like extension for ERC1155

* Add tests for ERC1155URIStorage extension

* add changelog entry for ERC721URIStorage

* Fix linting errors

* Emit URI event in ERC1155URIStorage

* Remove exists check and ERC1155Supply dependency

* Fix lint error

* Overwrite ERC1155 uri method

* Update ERC1155URIStorage specs

* Fix ERC1155URIStorageMock

* Rename _setTokenURI => _setURI in ERC1155URIStorage

* Add baseURI to ERC1155URIStorage

* Move super.uri call in ERC1155URIStorage

* Clearify ERC1155URIStorage description in change log

* reorder changelog & add documentation

* improve documentation

* fix typo

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
S E R A Y A 3 years ago
parent
commit
02fcc75bb7

+ 5 - 4
CHANGELOG.md

@@ -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))
  * `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 `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))
  * `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))
  * `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))
  * `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`: 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`: 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`: 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))
  * `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))
  * `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`: 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 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))
  * `Initializable`: add an Initialized event that tracks initialized version numbers. ([#3294](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3294))
 
 

+ 22 - 0
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 - 0
contracts/token/ERC1155/README.adoc

@@ -36,6 +36,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 
 
 {{ERC1155Supply}}
 {{ERC1155Supply}}
 
 
+{{ERC1155URIStorage}}
+
 == Presets
 == 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.
 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.

+ 62 - 0
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 - 0
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 });
+    });
+  });
+});