diff --git a/CHANGELOG.md b/CHANGELOG.md index c09c10bc25d..1b596d80fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * `Counter`: add a reset method. ([#2678](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2678)) * Tokens: Wrap definitely safe subtractions in `unchecked` blocks. * `Math`: Add a `ceilDiv` method for performing ceiling division. + * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593)) ### Breaking Changes 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/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 new file mode 100644 index 00000000000..6ec3b9b0577 --- /dev/null +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1155.sol"; + +/** + * @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 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; + + /** + * @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; + } + + /** + * @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) { + _totalSupply[ids[i]] += amounts[i]; + } + } + + /** + * @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) { + _totalSupply[ids[i]] -= amounts[i]; + } + } +} diff --git a/test/token/ERC1155/extensions/ERC1155Supply.test.js b/test/token/ERC1155/extensions/ERC1155Supply.test.js new file mode 100644 index 00000000000..1a632604d97 --- /dev/null +++ b/test/token/ERC1155/extensions/ERC1155Supply.test.js @@ -0,0 +1,111 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ERC1155SupplyMock = artifacts.require('ERC1155SupplyMock'); + +contract('ERC1155Supply', function (accounts) { + const [ holder ] = 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'); + }); + }); + }); +});