Browse Source

Introduce ERC1155 totalSupply() and exists() functions (#2593)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Hadrien Croubois 4 years ago
parent
commit
406c83649b

+ 1 - 0
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
  

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

+ 2 - 0
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.

+ 67 - 0
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];
+        }
+    }
+}

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