From ff575435b1bfd4bf96a05a3ed0ccd6ca9939ed90 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 27 Jan 2021 15:20:09 +0100 Subject: [PATCH 1/7] adding an _exists internal function on ERC1155 --- contracts/token/ERC1155/ERC1155Supply.sol | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 contracts/token/ERC1155/ERC1155Supply.sol diff --git a/contracts/token/ERC1155/ERC1155Supply.sol b/contracts/token/ERC1155/ERC1155Supply.sol new file mode 100644 index 00000000000..5b75f5f9f0f --- /dev/null +++ b/contracts/token/ERC1155/ERC1155Supply.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.9.0; + +import "./ERC1155.sol"; + +/** + * @dev ERC1155 token with totalSupply (per id). + * + * Useful for scenarios where Fungible and Non-fungible tokens have to be + * clearly identified. Note: While a totalSupply of 1 might mean the + * corresponding is an NFT, there is no guaranties that no other token with the + * same id are not going to be minted + */ +abstract contract ERC1155Supply is ERC1155 { + using SafeMath for uint256; + + mapping (uint256 => uint256) private _totalSupply; + + /** + * @dev Total amount of tokens in with a given id. + */ + function totalSupply(uint256 id) public view returns (uint256) { + return _totalSupply[id]; + } + + /** + * @dev Indicates weither any token exist with a given id, or not. + */ + function _exists(uint256 id) internal view returns(bool) { + return _totalSupply[id] > 0; + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) + internal + virtual + override + { + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + if (from == address(0)) { + _totalSupply[id] = _totalSupply[id].add(amount); + } + if (to == address(0)) { + _totalSupply[id] = _totalSupply[id].sub(amount); + } + } + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + } +} From e8a661c3b51eada0ac8410239a28b68812bd10b8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Feb 2021 00:27:44 +0100 Subject: [PATCH 2/7] hook on _mint and _burn to avoid gas cost when doing transfers --- contracts/token/ERC1155/ERC1155Supply.sol | 61 ------------------- .../ERC1155/extensions/ERC1155Supply.sol | 58 ++++++++++++++++++ 2 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 contracts/token/ERC1155/ERC1155Supply.sol create mode 100644 contracts/token/ERC1155/extensions/ERC1155Supply.sol diff --git a/contracts/token/ERC1155/ERC1155Supply.sol b/contracts/token/ERC1155/ERC1155Supply.sol deleted file mode 100644 index 5b75f5f9f0f..00000000000 --- a/contracts/token/ERC1155/ERC1155Supply.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.9.0; - -import "./ERC1155.sol"; - -/** - * @dev ERC1155 token with totalSupply (per id). - * - * Useful for scenarios where Fungible and Non-fungible tokens have to be - * clearly identified. Note: While a totalSupply of 1 might mean the - * corresponding is an NFT, there is no guaranties that no other token with the - * same id are not going to be minted - */ -abstract contract ERC1155Supply is ERC1155 { - using SafeMath for uint256; - - mapping (uint256 => uint256) private _totalSupply; - - /** - * @dev Total amount of tokens in with a given id. - */ - function totalSupply(uint256 id) public view returns (uint256) { - return _totalSupply[id]; - } - - /** - * @dev Indicates weither any token exist with a given id, or not. - */ - function _exists(uint256 id) internal view returns(bool) { - return _totalSupply[id] > 0; - } - - /** - * @dev See {ERC1155-_beforeTokenTransfer}. - */ - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) - internal - virtual - override - { - for (uint256 i = 0; i < ids.length; ++i) { - uint256 id = ids[i]; - uint256 amount = amounts[i]; - if (from == address(0)) { - _totalSupply[id] = _totalSupply[id].add(amount); - } - if (to == address(0)) { - _totalSupply[id] = _totalSupply[id].sub(amount); - } - } - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - } -} diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol new file mode 100644 index 00000000000..3a0fe555ede --- /dev/null +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1155.sol"; +import "../../../utils/math/SafeMath.sol"; + +/** + * @dev ERC1155 token with totalSupply (per id). + * + * Useful for scenarios where Fungible and Non-fungible tokens have to be + * clearly identified. Note: While a totalSupply of 1 might mean the + * corresponding is an NFT, there is no guaranties that no other token with the + * same id are not going to be minted + */ +abstract contract ERC1155Supply is ERC1155 { + using SafeMath for uint256; + + mapping (uint256 => uint256) private _totalSupply; + + /** + * @dev Total amount of tokens in with a given id. + */ + function totalSupply(uint256 id) public view virtual returns (uint256) { + return _totalSupply[id]; + } + + /** + * @dev Indicates weither any token exist with a given id, or not. + */ + function exists(uint256 id) public view virtual returns(bool) { + return ERC1155Supply.totalSupply(id) > 0; + } + + function _mint(address account, uint256 id, uint256 amount, bytes memory data) internal virtual override { + super._mint(account, id, amount, data); + _totalSupply[id] = _totalSupply[id].add(amount); + } + + function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual override { + super._mintBatch(to, ids, amounts, data); + for (uint256 i = 0; i < ids.length; ++i) { + _totalSupply[ids[i]] = _totalSupply[ids[i]].add(amounts[i]); + } + } + + function _burn(address account, uint256 id, uint256 amount) internal virtual override { + super._burn(account, id, amount); + _totalSupply[id] = _totalSupply[id].sub(amount); + } + + function _burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) internal virtual override { + super._burnBatch(account, ids, amounts); + for (uint256 i = 0; i < ids.length; ++i) { + _totalSupply[ids[i]] = _totalSupply[ids[i]].sub(amounts[i]); + } + } +} From 69324bb8b461e58c3db01d58bf4c764b62fb2212 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 15 Mar 2021 11:22:59 +0100 Subject: [PATCH 3/7] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49eb3ebbcfe..e3d55a2f0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * `IERC20Metadata`: add a new extended interface that includes the optional `name()`, `symbol()` and `decimals()` functions. ([#2561](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2561)) * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) + * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593)) ## 4.0.0 (2021-03-23) From a58693886d2b3a103edf60184f03a1c0718aea10 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 15 Mar 2021 11:45:38 +0100 Subject: [PATCH 4/7] add mock + tests for ERC1155Supply --- contracts/mocks/ERC1155SupplyMock.sol | 26 ++++ .../ERC1155/extensions/ERC1155Supply.test.js | 111 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 contracts/mocks/ERC1155SupplyMock.sol create mode 100644 test/token/ERC1155/extensions/ERC1155Supply.test.js diff --git a/contracts/mocks/ERC1155SupplyMock.sol b/contracts/mocks/ERC1155SupplyMock.sol new file mode 100644 index 00000000000..995b88a68be --- /dev/null +++ b/contracts/mocks/ERC1155SupplyMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ERC1155Mock.sol"; +import "../token/ERC1155/extensions/ERC1155Supply.sol"; + +contract ERC1155SupplyMock is ERC1155Mock, ERC1155Supply { + constructor(string memory uri) ERC1155Mock(uri) { } + + function _mint(address account, uint256 id, uint256 amount, bytes memory data) internal virtual override(ERC1155, ERC1155Supply) { + super._mint(account, id, amount, data); + } + + function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual override(ERC1155, ERC1155Supply) { + super._mintBatch(to, ids, amounts, data); + } + + function _burn(address account, uint256 id, uint256 amount) internal virtual override(ERC1155, ERC1155Supply) { + super._burn(account, id, amount); + } + + function _burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) internal virtual override(ERC1155, ERC1155Supply) { + super._burnBatch(account, ids, amounts); + } +} diff --git a/test/token/ERC1155/extensions/ERC1155Supply.test.js b/test/token/ERC1155/extensions/ERC1155Supply.test.js new file mode 100644 index 00000000000..b74ede8ee93 --- /dev/null +++ b/test/token/ERC1155/extensions/ERC1155Supply.test.js @@ -0,0 +1,111 @@ +const { BN, expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ERC1155SupplyMock = artifacts.require('ERC1155SupplyMock'); + +contract('ERC1155Supply', function (accounts) { + const [ holder, operator, receiver, other ] = accounts; + + const uri = 'https://token.com'; + + const firstTokenId = new BN('37'); + const firstTokenAmount = new BN('42'); + + const secondTokenId = new BN('19842'); + const secondTokenAmount = new BN('23'); + + beforeEach(async function () { + this.token = await ERC1155SupplyMock.new(uri); + }); + + context('before mint', function () { + it('exist', async function () { + expect(await this.token.exists(firstTokenId)).to.be.equal(false); + }); + + it('totalSupply', async function () { + expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0'); + }); + }); + + context('after mint', function () { + context('single', function () { + beforeEach(async function () { + await this.token.mint(holder, firstTokenId, firstTokenAmount, '0x'); + }); + + it('exist', async function () { + expect(await this.token.exists(firstTokenId)).to.be.equal(true); + }); + + it('totalSupply', async function () { + expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal(firstTokenAmount); + }); + }); + + context('batch', function () { + beforeEach(async function () { + await this.token.mintBatch( + holder, + [ firstTokenId, secondTokenId ], + [ firstTokenAmount, secondTokenAmount ], + '0x', + ); + }); + + it('exist', async function () { + expect(await this.token.exists(firstTokenId)).to.be.equal(true); + expect(await this.token.exists(secondTokenId)).to.be.equal(true); + }); + + it('totalSupply', async function () { + expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal(firstTokenAmount); + expect(await this.token.totalSupply(secondTokenId)).to.be.bignumber.equal(secondTokenAmount); + }); + }); + }); + + context('after burn', function () { + context('single', function () { + beforeEach(async function () { + await this.token.mint(holder, firstTokenId, firstTokenAmount, '0x'); + await this.token.burn(holder, firstTokenId, firstTokenAmount); + }); + + it('exist', async function () { + expect(await this.token.exists(firstTokenId)).to.be.equal(false); + }); + + it('totalSupply', async function () { + expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0'); + }); + }); + + context('batch', function () { + beforeEach(async function () { + await this.token.mintBatch( + holder, + [ firstTokenId, secondTokenId ], + [ firstTokenAmount, secondTokenAmount ], + '0x', + ); + await this.token.burnBatch( + holder, + [ firstTokenId, secondTokenId ], + [ firstTokenAmount, secondTokenAmount ], + ); + }); + + it('exist', async function () { + expect(await this.token.exists(firstTokenId)).to.be.equal(false); + expect(await this.token.exists(secondTokenId)).to.be.equal(false); + }); + + it('totalSupply', async function () { + expect(await this.token.totalSupply(firstTokenId)).to.be.bignumber.equal('0'); + expect(await this.token.totalSupply(secondTokenId)).to.be.bignumber.equal('0'); + }); + }); + }); +}); From a4fde30507df09e116c647603bc0762597c8924a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 15 Mar 2021 14:53:02 +0100 Subject: [PATCH 5/7] fix lint in test --- test/token/ERC1155/extensions/ERC1155Supply.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC1155/extensions/ERC1155Supply.test.js b/test/token/ERC1155/extensions/ERC1155Supply.test.js index b74ede8ee93..1a632604d97 100644 --- a/test/token/ERC1155/extensions/ERC1155Supply.test.js +++ b/test/token/ERC1155/extensions/ERC1155Supply.test.js @@ -1,11 +1,11 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ERC1155SupplyMock = artifacts.require('ERC1155SupplyMock'); contract('ERC1155Supply', function (accounts) { - const [ holder, operator, receiver, other ] = accounts; + const [ holder ] = accounts; const uri = 'https://token.com'; From 628a6976a54e07f72afac2e0000c23e33cd5d502 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 16 Mar 2021 21:48:44 +0100 Subject: [PATCH 6/7] remove safemath in ERC1155Supply --- contracts/token/ERC1155/extensions/ERC1155Supply.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 3a0fe555ede..20ff3f327ae 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "../ERC1155.sol"; -import "../../../utils/math/SafeMath.sol"; /** * @dev ERC1155 token with totalSupply (per id). @@ -14,8 +13,6 @@ import "../../../utils/math/SafeMath.sol"; * same id are not going to be minted */ abstract contract ERC1155Supply is ERC1155 { - using SafeMath for uint256; - mapping (uint256 => uint256) private _totalSupply; /** @@ -34,25 +31,25 @@ abstract contract ERC1155Supply is ERC1155 { function _mint(address account, uint256 id, uint256 amount, bytes memory data) internal virtual override { super._mint(account, id, amount, data); - _totalSupply[id] = _totalSupply[id].add(amount); + _totalSupply[id] += amount; } function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual override { super._mintBatch(to, ids, amounts, data); for (uint256 i = 0; i < ids.length; ++i) { - _totalSupply[ids[i]] = _totalSupply[ids[i]].add(amounts[i]); + _totalSupply[ids[i]] += amounts[i]; } } function _burn(address account, uint256 id, uint256 amount) internal virtual override { super._burn(account, id, amount); - _totalSupply[id] = _totalSupply[id].sub(amount); + _totalSupply[id] -= amount; } function _burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) internal virtual override { super._burnBatch(account, ids, amounts); for (uint256 i = 0; i < ids.length; ++i) { - _totalSupply[ids[i]] = _totalSupply[ids[i]].sub(amounts[i]); + _totalSupply[ids[i]] -= amounts[i]; } } } From 9be203156ba8e7d67cd45e2763d39812dc04e87f Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 20 May 2021 18:39:23 -0300 Subject: [PATCH 7/7] docs --- contracts/token/ERC1155/README.adoc | 2 ++ .../token/ERC1155/extensions/ERC1155Supply.sol | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC1155/README.adoc b/contracts/token/ERC1155/README.adoc index c1dc83e155e..c4179320599 100644 --- a/contracts/token/ERC1155/README.adoc +++ b/contracts/token/ERC1155/README.adoc @@ -32,6 +32,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC1155Burnable}} +{{ERC1155Supply}} + == 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. diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 20ff3f327ae..6ec3b9b0577 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -5,12 +5,12 @@ pragma solidity ^0.8.0; import "../ERC1155.sol"; /** - * @dev ERC1155 token with totalSupply (per id). + * @dev Extension of ERC1155 that adds tracking of total supply per id. * * Useful for scenarios where Fungible and Non-fungible tokens have to be * clearly identified. Note: While a totalSupply of 1 might mean the - * corresponding is an NFT, there is no guaranties that no other token with the - * same id are not going to be minted + * corresponding is an NFT, there is no guarantees that no other token with the + * same id are not going to be minted. */ abstract contract ERC1155Supply is ERC1155 { mapping (uint256 => uint256) private _totalSupply; @@ -29,11 +29,17 @@ abstract contract ERC1155Supply is ERC1155 { return ERC1155Supply.totalSupply(id) > 0; } + /** + * @dev See {ERC1155-_mint}. + */ function _mint(address account, uint256 id, uint256 amount, bytes memory data) internal virtual override { super._mint(account, id, amount, data); _totalSupply[id] += amount; } + /** + * @dev See {ERC1155-_mintBatch}. + */ function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual override { super._mintBatch(to, ids, amounts, data); for (uint256 i = 0; i < ids.length; ++i) { @@ -41,11 +47,17 @@ abstract contract ERC1155Supply is ERC1155 { } } + /** + * @dev See {ERC1155-_burn}. + */ function _burn(address account, uint256 id, uint256 amount) internal virtual override { super._burn(account, id, amount); _totalSupply[id] -= amount; } + /** + * @dev See {ERC1155-_burnBatch}. + */ function _burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) internal virtual override { super._burnBatch(account, ids, amounts); for (uint256 i = 0; i < ids.length; ++i) {