From a0f6bd3926597196de9dc82c0f0598d831226b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 3 Apr 2020 16:21:58 -0300 Subject: [PATCH] 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 --- contracts/deploy-ready/ERC20MinterPauser.sol | 85 ++++++++++++++ contracts/deploy-ready/ERC721MinterPauser.sol | 85 ++++++++++++++ contracts/deploy-ready/README.adoc | 13 +++ test/deploy-ready/ERC20MinterPauser.test.js | 103 +++++++++++++++++ test/deploy-ready/ERC721MinterPauser.test.js | 106 ++++++++++++++++++ 5 files changed, 392 insertions(+) create mode 100644 contracts/deploy-ready/ERC20MinterPauser.sol create mode 100644 contracts/deploy-ready/ERC721MinterPauser.sol create mode 100644 contracts/deploy-ready/README.adoc create mode 100644 test/deploy-ready/ERC20MinterPauser.test.js create mode 100644 test/deploy-ready/ERC721MinterPauser.test.js diff --git a/contracts/deploy-ready/ERC20MinterPauser.sol b/contracts/deploy-ready/ERC20MinterPauser.sol new file mode 100644 index 000000000..2e50997cb --- /dev/null +++ b/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); + } +} diff --git a/contracts/deploy-ready/ERC721MinterPauser.sol b/contracts/deploy-ready/ERC721MinterPauser.sol new file mode 100644 index 000000000..68f46ed1c --- /dev/null +++ b/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); + } +} diff --git a/contracts/deploy-ready/README.adoc b/contracts/deploy-ready/README.adoc new file mode 100644 index 000000000..6ae8e63ab --- /dev/null +++ b/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}} diff --git a/test/deploy-ready/ERC20MinterPauser.test.js b/test/deploy-ready/ERC20MinterPauser.test.js new file mode 100644 index 000000000..ae1aa13c5 --- /dev/null +++ b/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'); + }); + }); +}); diff --git a/test/deploy-ready/ERC721MinterPauser.test.js b/test/deploy-ready/ERC721MinterPauser.test.js new file mode 100644 index 000000000..fff63928a --- /dev/null +++ b/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'); + }); + }); +});