Browse Source

Add 'deploy-ready' token contracts (#2167)

* Add ERC20DeployReady

* Add ERC721DeployReady

* Improve docs

* Fix linter errors

* Rename DeployReady contracts to MinterPauser, add docs

* Fix deploy ready docs

* Minor doc adjustment
Nicolás Venturo 5 years ago
parent
commit
a0f6bd3926

+ 85 - 0
contracts/deploy-ready/ERC20MinterPauser.sol

@@ -0,0 +1,85 @@
+pragma solidity ^0.6.0;
+
+import "../access/AccessControl.sol";
+import "../GSN/Context.sol";
+import "../token/ERC20/ERC20.sol";
+import "../token/ERC20/ERC20Burnable.sol";
+import "../token/ERC20/ERC20Pausable.sol";
+
+/**
+ * @dev {ERC20} token, including:
+ *
+ *  - ability for holders to burn (destroy) their tokens
+ *  - a minter role that allows for token minting (creation)
+ *  - a pauser role that allows to stop all token transfers
+ *
+ * This contract uses {AccessControl} to lock permissioned functions using the
+ * different roles - head to its documentation for details.
+ *
+ * The account that deploys the contract will be granted the minter role, the
+ * pauser role, and the default admin role, meaning it will be able to grant
+ * both the minter and pauser roles.
+ */
+contract ERC20MinterPauser is Context, AccessControl, ERC20Burnable, ERC20Pausable {
+    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
+    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
+
+    /**
+     * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the
+     * account that deploys the contract.
+     *
+     * See {ERC20-constructor}.
+     */
+    constructor(string memory name, string memory symbol) public ERC20(name, symbol) {
+        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
+
+        _setupRole(MINTER_ROLE, _msgSender());
+        _setupRole(PAUSER_ROLE, _msgSender());
+    }
+
+    /**
+     * @dev Creates `amount` new tokens for `to`.
+     *
+     * See {ERC20-_mint}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `MINTER_ROLE`.
+     */
+    function mint(address to, uint256 amount) public {
+        require(hasRole(MINTER_ROLE, _msgSender()), "ERC20MinterPauser: must have minter role to mint");
+        _mint(to, amount);
+    }
+
+    /**
+     * @dev Pauses all token transfers.
+     *
+     * See {ERC20Pausable} and {Pausable-_pause}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `PAUSER_ROLE`.
+     */
+    function pause() public {
+        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20MinterPauser: must have pauser role to pause");
+        _pause();
+    }
+
+    /**
+     * @dev Unpauses all token transfers.
+     *
+     * See {ERC20Pausable} and {Pausable-_unpause}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `PAUSER_ROLE`.
+     */
+    function unpause() public {
+        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20MinterPauser: must have pauser role to unpause");
+        _unpause();
+    }
+
+    function _beforeTokenTransfer(address from, address to, uint256 amount) internal override(ERC20, ERC20Pausable) {
+        super._beforeTokenTransfer(from, to, amount);
+    }
+}

+ 85 - 0
contracts/deploy-ready/ERC721MinterPauser.sol

@@ -0,0 +1,85 @@
+pragma solidity ^0.6.0;
+
+import "../access/AccessControl.sol";
+import "../GSN/Context.sol";
+import "../token/ERC721/ERC721.sol";
+import "../token/ERC721/ERC721Burnable.sol";
+import "../token/ERC721/ERC721Pausable.sol";
+
+/**
+ * @dev {ERC721} token, including:
+ *
+ *  - ability for holders to burn (destroy) their tokens
+ *  - a minter role that allows for token minting (creation)
+ *  - a pauser role that allows to stop all token transfers
+ *
+ * This contract uses {AccessControl} to lock permissioned functions using the
+ * different roles - head to its documentation for details.
+ *
+ * The account that deploys the contract will be granted the minter role, the
+ * pauser role, and the default admin role, meaning it will be able to grant
+ * both the minter and pauser roles.
+ */
+contract ERC721MinterPauser is Context, AccessControl, ERC721Burnable, ERC721Pausable {
+    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
+    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
+
+    /**
+     * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the
+     * account that deploys the contract.
+     *
+     * See {ERC721-constructor}.
+     */
+    constructor(string memory name, string memory symbol) public ERC721(name, symbol) {
+        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
+
+        _setupRole(MINTER_ROLE, _msgSender());
+        _setupRole(PAUSER_ROLE, _msgSender());
+    }
+
+    /**
+     * @dev Creates the `tokenId` tokens for `to`.
+     *
+     * See {ERC721-_mint}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `MINTER_ROLE`.
+     */
+    function mint(address to, uint256 tokenId) public {
+        require(hasRole(MINTER_ROLE, _msgSender()), "ERC721MinterPauser: must have minter role to mint");
+        _mint(to, tokenId);
+    }
+
+    /**
+     * @dev Pauses all token transfers.
+     *
+     * See {ERC721Pausable} and {Pausable-_pause}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `PAUSER_ROLE`.
+     */
+    function pause() public {
+        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC721MinterPauser: must have pauser role to pause");
+        _pause();
+    }
+
+    /**
+     * @dev Unpauses all token transfers.
+     *
+     * See {ERC20Pausable} and {Pausable-_unpause}.
+     *
+     * Requirements:
+     *
+     * - the caller must have the `PAUSER_ROLE`.
+     */
+    function unpause() public {
+        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC721MinterPauser: must have pauser role to unpause");
+        _unpause();
+    }
+
+    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Pausable) {
+        super._beforeTokenTransfer(from, to, tokenId);
+    }
+}

+ 13 - 0
contracts/deploy-ready/README.adoc

@@ -0,0 +1,13 @@
+= Deploy Ready
+
+These contracts integrate different Ethereum standards (ERCs) with custom extensions and modules, showcasing common configurations that are ready to deploy **without having to write any Solidity code**.
+
+They can be used as-is for quick prototyping and testing, but are **also suitable for production environments**.
+
+TIP: Intermediate and advanced users can use these as starting points when writing their own contracts, extending them with custom functionality as they see fit.
+
+== Tokens
+
+{{ERC20MinterPauser}}
+
+{{ERC721MinterPauser}}

+ 103 - 0
test/deploy-ready/ERC20MinterPauser.test.js

@@ -0,0 +1,103 @@
+const { accounts, contract, web3 } = require('@openzeppelin/test-environment');
+
+const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { ZERO_ADDRESS } = constants;
+
+const { expect } = require('chai');
+
+const ERC20MinterPauser = contract.fromArtifact('ERC20MinterPauser');
+
+describe('ERC20MinterPauser', function () {
+  const [ deployer, other ] = accounts;
+
+  const name = 'MinterPauserToken';
+  const symbol = 'DRT';
+
+  const amount = new BN('5000');
+
+  const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
+  const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
+  const PAUSER_ROLE = web3.utils.soliditySha3('PAUSER_ROLE');
+
+  beforeEach(async function () {
+    this.token = await ERC20MinterPauser.new(name, symbol, { from: deployer });
+  });
+
+  it('deployer has the default admin role', async function () {
+    expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('deployer has the minter role', async function () {
+    expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('deployer has the pauser role', async function () {
+    expect(await this.token.getRoleMemberCount(PAUSER_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(PAUSER_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('minter and pauser role admin is the default admin', async function () {
+    expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
+    expect(await this.token.getRoleAdmin(PAUSER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
+  });
+
+  describe('minting', function () {
+    it('deployer can mint tokens', async function () {
+      const receipt = await this.token.mint(other, amount, { from: deployer });
+      expectEvent(receipt, 'Transfer', { from: ZERO_ADDRESS, to: other, value: amount });
+
+      expect(await this.token.balanceOf(other)).to.be.bignumber.equal(amount);
+    });
+
+    it('other accounts cannot mint tokens', async function () {
+      await expectRevert(
+        this.token.mint(other, amount, { from: other }),
+        'ERC20MinterPauser: must have minter role to mint'
+      );
+    });
+  });
+
+  describe('pausing', function () {
+    it('deployer can pause', async function () {
+      const receipt = await this.token.pause({ from: deployer });
+      expectEvent(receipt, 'Paused', { account: deployer });
+
+      expect(await this.token.paused()).to.equal(true);
+    });
+
+    it('deployer can unpause', async function () {
+      await this.token.pause({ from: deployer });
+
+      const receipt = await this.token.unpause({ from: deployer });
+      expectEvent(receipt, 'Unpaused', { account: deployer });
+
+      expect(await this.token.paused()).to.equal(false);
+    });
+
+    it('cannot mint while paused', async function () {
+      await this.token.pause({ from: deployer });
+
+      await expectRevert(
+        this.token.mint(other, amount, { from: deployer }),
+        'ERC20Pausable: token transfer while paused'
+      );
+    });
+
+    it('other accounts cannot pause', async function () {
+      await expectRevert(this.token.pause({ from: other }), 'ERC20MinterPauser: must have pauser role to pause');
+    });
+  });
+
+  describe('burning', function () {
+    it('holders can burn their tokens', async function () {
+      await this.token.mint(other, amount, { from: deployer });
+
+      const receipt = await this.token.burn(amount.subn(1), { from: other });
+      expectEvent(receipt, 'Transfer', { from: other, to: ZERO_ADDRESS, value: amount.subn(1) });
+
+      expect(await this.token.balanceOf(other)).to.be.bignumber.equal('1');
+    });
+  });
+});

+ 106 - 0
test/deploy-ready/ERC721MinterPauser.test.js

@@ -0,0 +1,106 @@
+const { accounts, contract, web3 } = require('@openzeppelin/test-environment');
+
+const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { ZERO_ADDRESS } = constants;
+
+const { expect } = require('chai');
+
+const ERC721MinterPauser = contract.fromArtifact('ERC721MinterPauser');
+
+describe('ERC721MinterPauser', function () {
+  const [ deployer, other ] = accounts;
+
+  const name = 'MinterPauserToken';
+  const symbol = 'DRT';
+
+  const tokenId = new BN('1337');
+
+  const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
+  const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
+  const PAUSER_ROLE = web3.utils.soliditySha3('PAUSER_ROLE');
+
+  beforeEach(async function () {
+    this.token = await ERC721MinterPauser.new(name, symbol, { from: deployer });
+  });
+
+  it('deployer has the default admin role', async function () {
+    expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('deployer has the minter role', async function () {
+    expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('deployer has the pauser role', async function () {
+    expect(await this.token.getRoleMemberCount(PAUSER_ROLE)).to.be.bignumber.equal('1');
+    expect(await this.token.getRoleMember(PAUSER_ROLE, 0)).to.equal(deployer);
+  });
+
+  it('minter and pauser role admin is the default admin', async function () {
+    expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
+    expect(await this.token.getRoleAdmin(PAUSER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
+  });
+
+  describe('minting', function () {
+    it('deployer can mint tokens', async function () {
+      const receipt = await this.token.mint(other, tokenId, { from: deployer });
+      expectEvent(receipt, 'Transfer', { from: ZERO_ADDRESS, to: other, tokenId });
+
+      expect(await this.token.balanceOf(other)).to.be.bignumber.equal('1');
+      expect(await this.token.ownerOf(tokenId)).to.equal(other);
+    });
+
+    it('other accounts cannot mint tokens', async function () {
+      await expectRevert(
+        this.token.mint(other, tokenId, { from: other }),
+        'ERC721MinterPauser: must have minter role to mint'
+      );
+    });
+  });
+
+  describe('pausing', function () {
+    it('deployer can pause', async function () {
+      const receipt = await this.token.pause({ from: deployer });
+      expectEvent(receipt, 'Paused', { account: deployer });
+
+      expect(await this.token.paused()).to.equal(true);
+    });
+
+    it('deployer can unpause', async function () {
+      await this.token.pause({ from: deployer });
+
+      const receipt = await this.token.unpause({ from: deployer });
+      expectEvent(receipt, 'Unpaused', { account: deployer });
+
+      expect(await this.token.paused()).to.equal(false);
+    });
+
+    it('cannot mint while paused', async function () {
+      await this.token.pause({ from: deployer });
+
+      await expectRevert(
+        this.token.mint(other, tokenId, { from: deployer }),
+        'ERC721Pausable: token transfer while paused'
+      );
+    });
+
+    it('other accounts cannot pause', async function () {
+      await expectRevert(this.token.pause({ from: other }), 'ERC721MinterPauser: must have pauser role to pause');
+    });
+  });
+
+  describe('burning', function () {
+    it('holders can burn their tokens', async function () {
+      await this.token.mint(other, tokenId, { from: deployer });
+
+      const receipt = await this.token.burn(tokenId, { from: other });
+
+      expectEvent(receipt, 'Transfer', { from: other, to: ZERO_ADDRESS, tokenId });
+
+      expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0');
+      expect(await this.token.totalSupply()).to.be.bignumber.equal('0');
+    });
+  });
+});