Bladeren bron

Merge branch 'master' into release-v3.0.0

Nicolás Venturo 5 jaren geleden
bovenliggende
commit
92a60b2587
94 gewijzigde bestanden met toevoegingen van 2889 en 2146 verwijderingen
  1. 4 0
      .github/ISSUE_TEMPLATE/config.yml
  2. 2 1
      .solhint.json
  3. 12 1
      CHANGELOG.md
  4. 7 7
      contracts/GSN/GSNRecipient.sol
  5. 11 12
      contracts/GSN/GSNRecipientERC20Fee.sol
  6. 3 3
      contracts/GSN/GSNRecipientSignature.sol
  7. 36 30
      contracts/access/AccessControl.sol
  8. 0 7
      contracts/access/Ownable.sol
  9. 85 0
      contracts/deploy-ready/ERC20MinterPauser.sol
  10. 85 0
      contracts/deploy-ready/ERC721MinterPauser.sol
  11. 13 0
      contracts/deploy-ready/README.adoc
  12. 1 1
      contracts/introspection/ERC165.sol
  13. 6 6
      contracts/introspection/ERC165Checker.sol
  14. 3 3
      contracts/introspection/ERC1820Implementer.sol
  15. 3 3
      contracts/math/SignedSafeMath.sol
  16. 5 1
      contracts/mocks/AccessControlMock.sol
  17. 5 5
      contracts/mocks/ArraysImpl.sol
  18. 7 5
      contracts/mocks/Create2Impl.sol
  19. 1 1
      contracts/mocks/ERC165/ERC165InterfacesSupported.sol
  20. 3 3
      contracts/mocks/ERC165CheckerMock.sol
  21. 6 1
      contracts/mocks/ERC20BurnableMock.sol
  22. 3 1
      contracts/mocks/ERC20CappedMock.sol
  23. 13 0
      contracts/mocks/ERC20DecimalsMock.sol
  24. 0 13
      contracts/mocks/ERC20DetailedMock.sol
  25. 6 1
      contracts/mocks/ERC20Mock.sol
  26. 6 1
      contracts/mocks/ERC20PausableMock.sol
  27. 6 1
      contracts/mocks/ERC20SnapshotMock.sol
  28. 2 0
      contracts/mocks/ERC721BurnableMock.sol
  29. 0 37
      contracts/mocks/ERC721FullMock.sol
  30. 5 1
      contracts/mocks/ERC721GSNRecipientMock.sol
  31. 18 4
      contracts/mocks/ERC721Mock.sol
  32. 2 0
      contracts/mocks/ERC721PausableMock.sol
  33. 6 6
      contracts/mocks/ERC777SenderRecipientMock.sol
  34. 38 0
      contracts/mocks/EnumerableMapMock.sol
  35. 11 15
      contracts/mocks/EnumerableSetMock.sol
  36. 5 5
      contracts/mocks/ReentrancyMock.sol
  37. 9 0
      contracts/mocks/SafeCastMock.sol
  38. 65 0
      contracts/token/ERC20/ERC20.sol
  39. 1 1
      contracts/token/ERC20/ERC20Burnable.sol
  40. 1 1
      contracts/token/ERC20/ERC20Capped.sol
  41. 0 54
      contracts/token/ERC20/ERC20Detailed.sol
  42. 1 1
      contracts/token/ERC20/ERC20Pausable.sol
  43. 1 1
      contracts/token/ERC20/ERC20Snapshot.sol
  44. 10 18
      contracts/token/ERC20/README.adoc
  45. 6 6
      contracts/token/ERC20/SafeERC20.sol
  46. 170 29
      contracts/token/ERC721/ERC721.sol
  47. 1 1
      contracts/token/ERC721/ERC721Burnable.sol
  48. 0 175
      contracts/token/ERC721/ERC721Enumerable.sol
  49. 0 23
      contracts/token/ERC721/ERC721Full.sol
  50. 0 121
      contracts/token/ERC721/ERC721Metadata.sol
  51. 1 1
      contracts/token/ERC721/ERC721Pausable.sol
  52. 0 11
      contracts/token/ERC721/IERC721Full.sol
  53. 7 24
      contracts/token/ERC721/README.adoc
  54. 7 7
      contracts/token/ERC777/ERC777.sol
  55. 1 1
      contracts/token/ERC777/README.adoc
  56. 17 6
      contracts/utils/Create2.sol
  57. 211 0
      contracts/utils/EnumerableMap.sol
  58. 166 71
      contracts/utils/EnumerableSet.sol
  59. 2 0
      contracts/utils/README.adoc
  60. 24 0
      contracts/utils/SafeCast.sol
  61. 4 4
      docs/contract.hbs
  62. 8 8
      docs/modules/ROOT/pages/access-control.adoc
  63. 6 4
      docs/modules/ROOT/pages/erc20-supply.adoc
  64. 7 6
      docs/modules/ROOT/pages/erc20.adoc
  65. 5 5
      docs/modules/ROOT/pages/erc721.adoc
  66. 8 3
      docs/modules/ROOT/pages/gsn-strategies.adoc
  67. 4 5
      docs/modules/ROOT/pages/index.adoc
  68. 2 2
      docs/modules/ROOT/pages/releases-stability.adoc
  69. 2 2
      docs/modules/ROOT/pages/utilities.adoc
  70. 324 235
      package-lock.json
  71. 6 5
      package.json
  72. 3 1
      test/GSN/ERC721GSNRecipientMock.test.js
  73. 2 2
      test/GSN/GSNRecipientERC20Fee.test.js
  74. 9 0
      test/access/AccessControl.test.js
  75. 0 149
      test/behaviors/access/roles/PublicRole.behavior.js
  76. 103 0
      test/deploy-ready/ERC20MinterPauser.test.js
  77. 106 0
      test/deploy-ready/ERC721MinterPauser.test.js
  78. 32 1
      test/token/ERC20/ERC20.test.js
  79. 4 1
      test/token/ERC20/ERC20Burnable.test.js
  80. 5 2
      test/token/ERC20/ERC20Capped.test.js
  81. 0 28
      test/token/ERC20/ERC20Detailed.test.js
  82. 4 1
      test/token/ERC20/ERC20Pausable.test.js
  83. 4 1
      test/token/ERC20/ERC20Snapshot.test.js
  84. 4 1
      test/token/ERC20/TokenTimelock.test.js
  85. 0 628
      test/token/ERC721/ERC721.behavior.js
  86. 833 36
      test/token/ERC721/ERC721.test.js
  87. 4 1
      test/token/ERC721/ERC721Burnable.test.js
  88. 0 249
      test/token/ERC721/ERC721Full.test.js
  89. 4 1
      test/token/ERC721/ERC721Holder.test.js
  90. 4 7
      test/token/ERC721/ERC721Pausable.test.js
  91. 46 16
      test/utils/Create2.test.js
  92. 139 0
      test/utils/EnumerableMap.test.js
  93. 17 15
      test/utils/EnumerableSet.test.js
  94. 70 0
      test/utils/SafeCast.test.js

+ 4 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,4 @@
+contact_links:
+  - name: Support request
+    url: https://forum.openzeppelin.com/c/support/contracts/18
+    about: Ask the community in the Community Forum

+ 2 - 1
.solhint.json

@@ -4,6 +4,7 @@
     "func-order": "off",
     "mark-callable-contracts": "off",
     "no-empty-blocks": "off",
-    "compiler-version": ["error", "^0.6.0"]
+    "compiler-version": ["error", "^0.6.0"],
+    "private-vars-leading-underscore": "error"
   }
 }

+ 12 - 1
CHANGELOG.md

@@ -4,13 +4,18 @@
 
 ### New features
  * `AccessControl`: new contract for managing permissions in a system, replacement for `Ownable` and `Roles`. ([#2112](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2112))
+ * `SafeCast`: new functions to convert to and from signed and unsigned values: `toUint256` and `toInt256`. ([#2123](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2123))
+ * `EnumerableMap`: a new data structure for key-value pairs (like `mapping`) that can be iterated over. ([#2160](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2160))
 
 ### Breaking changes
- * `ERC721`: `burn(owner, tokenId)` was removed, use `burn(owner)` instead. ([#2125](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2125))
+ * `ERC721`: `burn(owner, tokenId)` was removed, use `burn(tokenId)` instead. ([#2125](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2125))
  * `ERC721`: `_checkOnERC721Received` was removed. ([#2125](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2125))
+ * `ERC721`: `_transferFrom` and `_safeTransferFrom` were renamed to `_transfer` and `_safeTransfer`. ([#2162](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2162))
+ * `Ownable`: removed `_transferOwnership`. ([#2162](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2162))
  * `PullPayment`, `Escrow`: `withdrawWithGas` was removed. The old `withdraw` function now forwards all gas. ([#2125](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2125))
  * `Roles` was removed, use `AccessControl` as a replacement. ([#2112](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2112))
  * `ECDSA`: when receiving an invalid signature, `recover` now reverts instead of returning the zero address. ([#2114](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2114))
+ * `Create2`: added an `amount` argument to `deploy` for contracts with `payable` constructors. ([#2117](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2117))
  * `Pausable`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
  * `Strings`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
  * `Counters`: moved to the `utils` directory. ([#2122](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2122))
@@ -24,6 +29,12 @@
  * `Address`: removed `toPayable`, use `payable(address)` instead. ([#2133](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2133))
  * `ERC777`: `_send`, `_mint` and `_burn` now use the caller as the operator. ([#2134](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2134))
  * `ERC777`: removed `_callsTokensToSend` and `_callTokensReceived`. ([#2134](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2134))
+ * `EnumerableSet`: renamed `get` to `at`. ([#2151](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2151))
+ * `ERC165Checker`: functions no longer have a leading underscore. ([#2150](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2150))
+ * `ERC721Metadata`, `ERC721Enumerable`: these contracts were removed, and their functionality merged into `ERC721`. ([#2160](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2160))
+ * `ERC721`: added a constructor for `name` and `symbol`. ([#2160](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2160))
+ * `ERC20Detailed`: this contract was removed and its functionality merged into `ERC20`. ([#2161](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2161))
+ * `ERC20`: added a constructor for `name` and `symbol`. `decimals` now defaults to 18. ([#2161](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2161))
 
 ## 2.5.0 (2020-02-04)
 

+ 7 - 7
contracts/GSN/GSNRecipient.sol

@@ -19,11 +19,11 @@ abstract contract GSNRecipient is IRelayRecipient, Context {
     // Default RelayHub address, deployed on mainnet and all testnets at the same address
     address private _relayHub = 0xD216153c06E857cD7f72665E0aF1d7D82172F494;
 
-    uint256 constant private RELAYED_CALL_ACCEPTED = 0;
-    uint256 constant private RELAYED_CALL_REJECTED = 11;
+    uint256 constant private _RELAYED_CALL_ACCEPTED = 0;
+    uint256 constant private _RELAYED_CALL_REJECTED = 11;
 
     // How much gas is forwarded to postRelayedCall
-    uint256 constant internal POST_RELAYED_CALL_MAX_GAS = 100000;
+    uint256 constant internal _POST_RELAYED_CALL_MAX_GAS = 100000;
 
     /**
      * @dev Emitted when a contract changes its {IRelayHub} contract to a new one.
@@ -119,7 +119,7 @@ abstract contract GSNRecipient is IRelayRecipient, Context {
      *
      * - the caller must be the `RelayHub` contract.
      */
-    function preRelayedCall(bytes calldata context) external virtual override returns (bytes32) {
+    function preRelayedCall(bytes memory context) public virtual override returns (bytes32) {
         require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
         return _preRelayedCall(context);
     }
@@ -142,7 +142,7 @@ abstract contract GSNRecipient is IRelayRecipient, Context {
      *
      * - the caller must be the `RelayHub` contract.
      */
-    function postRelayedCall(bytes calldata context, bool success, uint256 actualCharge, bytes32 preRetVal) external virtual override {
+    function postRelayedCall(bytes memory context, bool success, uint256 actualCharge, bytes32 preRetVal) public virtual override {
         require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
         _postRelayedCall(context, success, actualCharge, preRetVal);
     }
@@ -170,14 +170,14 @@ abstract contract GSNRecipient is IRelayRecipient, Context {
      * This overload forwards `context` to _preRelayedCall and _postRelayedCall.
      */
     function _approveRelayedCall(bytes memory context) internal pure returns (uint256, bytes memory) {
-        return (RELAYED_CALL_ACCEPTED, context);
+        return (_RELAYED_CALL_ACCEPTED, context);
     }
 
     /**
      * @dev Return this in acceptRelayedCall to impede execution of a relayed call. No fees will be charged.
      */
     function _rejectRelayedCall(uint256 errorCode) internal pure returns (uint256, bytes memory) {
-        return (RELAYED_CALL_REJECTED + errorCode, "");
+        return (_RELAYED_CALL_REJECTED + errorCode, "");
     }
 
     /*

+ 11 - 12
contracts/GSN/GSNRecipientERC20Fee.sol

@@ -5,7 +5,6 @@ import "../math/SafeMath.sol";
 import "../access/Ownable.sol";
 import "../token/ERC20/SafeERC20.sol";
 import "../token/ERC20/ERC20.sol";
-import "../token/ERC20/ERC20Detailed.sol";
 
 /**
  * @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that charges transaction fees in a special purpose ERC20
@@ -30,7 +29,7 @@ contract GSNRecipientERC20Fee is GSNRecipient {
      * @dev The arguments to the constructor are the details that the gas payment token will have: `name` and `symbol`. `decimals` is hard-coded to 18.
      */
     constructor(string memory name, string memory symbol) public {
-        _token = new __unstable__ERC20Owned(name, symbol, 18);
+        _token = new __unstable__ERC20Owned(name, symbol);
     }
 
     /**
@@ -53,15 +52,15 @@ contract GSNRecipientERC20Fee is GSNRecipient {
     function acceptRelayedCall(
         address,
         address from,
-        bytes calldata,
+        bytes memory,
         uint256 transactionFee,
         uint256 gasPrice,
         uint256,
         uint256,
-        bytes calldata,
+        bytes memory,
         uint256 maxPossibleCharge
     )
-        external
+        public
         view
         virtual
         override
@@ -97,7 +96,7 @@ contract GSNRecipientERC20Fee is GSNRecipient {
         // actualCharge is an _estimated_ charge, which assumes postRelayedCall will use all available gas.
         // This implementation's gas cost can be roughly estimated as 10k gas, for the two SSTORE operations in an
         // ERC20 transfer.
-        uint256 overestimation = _computeCharge(POST_RELAYED_CALL_MAX_GAS.sub(10000), gasPrice, transactionFee);
+        uint256 overestimation = _computeCharge(_POST_RELAYED_CALL_MAX_GAS.sub(10000), gasPrice, transactionFee);
         actualCharge = actualCharge.sub(overestimation);
 
         // After the relayed call has been executed and the actual charge estimated, the excess pre-charge is returned
@@ -112,10 +111,10 @@ contract GSNRecipientERC20Fee is GSNRecipient {
  * outside of this context.
  */
 // solhint-disable-next-line contract-name-camelcase
-contract __unstable__ERC20Owned is ERC20, ERC20Detailed, Ownable {
-    uint256 private constant UINT256_MAX = 2**256 - 1;
+contract __unstable__ERC20Owned is ERC20, Ownable {
+    uint256 private constant _UINT256_MAX = 2**256 - 1;
 
-    constructor(string memory name, string memory symbol, uint8 decimals) public ERC20Detailed(name, symbol, decimals) { }
+    constructor(string memory name, string memory symbol) public ERC20(name, symbol) { }
 
     // The owner (GSNRecipientERC20Fee) can mint tokens
     function mint(address account, uint256 amount) public onlyOwner {
@@ -123,9 +122,9 @@ contract __unstable__ERC20Owned is ERC20, ERC20Detailed, Ownable {
     }
 
     // The owner has 'infinite' allowance for all token holders
-    function allowance(address tokenOwner, address spender) public view override(ERC20, IERC20) returns (uint256) {
+    function allowance(address tokenOwner, address spender) public view override returns (uint256) {
         if (spender == owner()) {
-            return UINT256_MAX;
+            return _UINT256_MAX;
         } else {
             return super.allowance(tokenOwner, spender);
         }
@@ -140,7 +139,7 @@ contract __unstable__ERC20Owned is ERC20, ERC20Detailed, Ownable {
         }
     }
 
-    function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
+    function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
         if (recipient == owner()) {
             _transfer(sender, recipient, amount);
             return true;

+ 3 - 3
contracts/GSN/GSNRecipientSignature.sol

@@ -32,15 +32,15 @@ contract GSNRecipientSignature is GSNRecipient {
     function acceptRelayedCall(
         address relay,
         address from,
-        bytes calldata encodedFunction,
+        bytes memory encodedFunction,
         uint256 transactionFee,
         uint256 gasPrice,
         uint256 gasLimit,
         uint256 nonce,
-        bytes calldata approvalData,
+        bytes memory approvalData,
         uint256
     )
-        external
+        public
         view
         virtual
         override

+ 36 - 30
contracts/access/AccessControl.sol

@@ -1,6 +1,7 @@
 pragma solidity ^0.6.0;
 
 import "../utils/EnumerableSet.sol";
+import "../utils/Address.sol";
 import "../GSN/Context.sol";
 
 /**
@@ -25,10 +26,7 @@ import "../GSN/Context.sol";
  * }
  * ```
  *
- * Roles can be granted and revoked programatically by calling the `internal`
- * {_grantRole} and {_revokeRole} functions.
- *
- * This can also be achieved dynamically via the `external` {grantRole} and
+ * Roles can be granted and revoked dynamically via the {grantRole} and
  * {revokeRole} functions. Each role has an associated admin role, and only
  * accounts that have a role's admin role can call {grantRole} and {revokeRoke}.
  *
@@ -39,6 +37,7 @@ import "../GSN/Context.sol";
  */
 abstract contract AccessControl is Context {
     using EnumerableSet for EnumerableSet.AddressSet;
+    using Address for address;
 
     struct RoleData {
         EnumerableSet.AddressSet members;
@@ -52,9 +51,8 @@ abstract contract AccessControl is Context {
     /**
      * @dev Emitted when `account` is granted `role`.
      *
-     * `sender` is the account that originated the contract call:
-     *   - if using `grantRole`, it is the admin role bearer
-     *   - if using `_grantRole`, its meaning is system-dependent
+     * `sender` is the account that originated the contract call, an admin role
+     * bearer except when using {_setupRole}.
      */
     event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
 
@@ -64,7 +62,6 @@ abstract contract AccessControl is Context {
      * `sender` is the account that originated the contract call:
      *   - if using `revokeRole`, it is the admin role bearer
      *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
-     *   - if using `_renounceRole`, its meaning is system-dependent
      */
     event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
 
@@ -96,7 +93,7 @@ abstract contract AccessControl is Context {
      * for more information.
      */
     function getRoleMember(bytes32 role, uint256 index) public view returns (address) {
-        return _roles[role].members.get(index);
+        return _roles[role].members.at(index);
     }
 
     /**
@@ -105,20 +102,21 @@ abstract contract AccessControl is Context {
      *
      * To change a role's admin, use {_setRoleAdmin}.
      */
-    function getRoleAdmin(bytes32 role) external view returns (bytes32) {
+    function getRoleAdmin(bytes32 role) public view returns (bytes32) {
         return _roles[role].adminRole;
     }
 
     /**
      * @dev Grants `role` to `account`.
      *
-     * Calls {_grantRole} internally.
+     * If `account` had not been already granted `role`, emits a {RoleGranted}
+     * event.
      *
      * Requirements:
      *
      * - the caller must have `role`'s admin role.
      */
-    function grantRole(bytes32 role, address account) external virtual {
+    function grantRole(bytes32 role, address account) public virtual {
         require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant");
 
         _grantRole(role, account);
@@ -127,13 +125,13 @@ abstract contract AccessControl is Context {
     /**
      * @dev Revokes `role` from `account`.
      *
-     * Calls {_revokeRole} internally.
+     * If `account` had been granted `role`, emits a {RoleRevoked} event.
      *
      * Requirements:
      *
      * - the caller must have `role`'s admin role.
      */
-    function revokeRole(bytes32 role, address account) external virtual {
+    function revokeRole(bytes32 role, address account) public virtual {
         require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke");
 
         _revokeRole(role, account);
@@ -146,11 +144,14 @@ abstract contract AccessControl is Context {
      * purpose is to provide a mechanism for accounts to lose their privileges
      * if they are compromised (such as when a trusted device is misplaced).
      *
+     * If the calling account had been granted `role`, emits a {RoleRevoked}
+     * event.
+     *
      * Requirements:
      *
      * - the caller must be `account`.
      */
-    function renounceRole(bytes32 role, address account) external virtual {
+    function renounceRole(bytes32 role, address account) public virtual {
         require(account == _msgSender(), "AccessControl: can only renounce roles for self");
 
         _revokeRole(role, account);
@@ -160,23 +161,16 @@ abstract contract AccessControl is Context {
      * @dev Grants `role` to `account`.
      *
      * If `account` had not been already granted `role`, emits a {RoleGranted}
-     * event.
-     */
-    function _grantRole(bytes32 role, address account) internal virtual {
-        if (_roles[role].members.add(account)) {
-            emit RoleGranted(role, account, _msgSender());
-        }
-    }
-
-    /**
-     * @dev Revokes `role` from `account`.
+     * event. Note that unlike {grantRole}, this function doesn't perform any
+     * checks on the calling account.
      *
-     * If `account` had been granted `role`, emits a {RoleRevoked} event.
+     * Requirements:
+     *
+     * - this function can only be called from a constructor.
      */
-    function _revokeRole(bytes32 role, address account) internal virtual {
-        if (_roles[role].members.remove(account)) {
-            emit RoleRevoked(role, account, _msgSender());
-        }
+    function _setupRole(bytes32 role, address account) internal virtual {
+        require(!address(this).isContract(), "AccessControl: roles cannot be setup after construction");
+        _grantRole(role, account);
     }
 
     /**
@@ -185,4 +179,16 @@ abstract contract AccessControl is Context {
     function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
         _roles[role].adminRole = adminRole;
     }
+
+    function _grantRole(bytes32 role, address account) private {
+        if (_roles[role].members.add(account)) {
+            emit RoleGranted(role, account, _msgSender());
+        }
+    }
+
+    function _revokeRole(bytes32 role, address account) private {
+        if (_roles[role].members.remove(account)) {
+            emit RoleRevoked(role, account, _msgSender());
+        }
+    }
 }

+ 0 - 7
contracts/access/Ownable.sol

@@ -59,13 +59,6 @@ contract Ownable is Context {
      * Can only be called by the current owner.
      */
     function transferOwnership(address newOwner) public virtual onlyOwner {
-        _transferOwnership(newOwner);
-    }
-
-    /**
-     * @dev Transfers ownership of the contract to a new account (`newOwner`).
-     */
-    function _transferOwnership(address newOwner) internal virtual {
         require(newOwner != address(0), "Ownable: new owner is the zero address");
         emit OwnershipTransferred(_owner, newOwner);
         _owner = newOwner;

+ 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}}

+ 1 - 1
contracts/introspection/ERC165.sol

@@ -30,7 +30,7 @@ contract ERC165 is IERC165 {
      *
      * Time complexity O(1), guaranteed to always use less than 30 000 gas.
      */
-    function supportsInterface(bytes4 interfaceId) external view override returns (bool) {
+    function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
         return _supportedInterfaces[interfaceId];
     }
 

+ 6 - 6
contracts/introspection/ERC165Checker.sol

@@ -19,7 +19,7 @@ library ERC165Checker {
     /**
      * @dev Returns true if `account` supports the {IERC165} interface,
      */
-    function _supportsERC165(address account) internal view returns (bool) {
+    function supportsERC165(address account) internal view returns (bool) {
         // Any contract that implements ERC165 must explicitly indicate support of
         // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid
         return _supportsERC165Interface(account, _INTERFACE_ID_ERC165) &&
@@ -32,9 +32,9 @@ library ERC165Checker {
      *
      * See {IERC165-supportsInterface}.
      */
-    function _supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) {
+    function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) {
         // query support of both ERC165 as per the spec and support of _interfaceId
-        return _supportsERC165(account) &&
+        return supportsERC165(account) &&
             _supportsERC165Interface(account, interfaceId);
     }
 
@@ -47,9 +47,9 @@ library ERC165Checker {
      *
      * See {IERC165-supportsInterface}.
      */
-    function _supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) {
+    function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) {
         // query support of ERC165 itself
-        if (!_supportsERC165(account)) {
+        if (!supportsERC165(account)) {
             return false;
         }
 
@@ -72,7 +72,7 @@ library ERC165Checker {
      * identifier interfaceId, false otherwise
      * @dev Assumes that account contains a contract that supports ERC165, otherwise
      * the behavior of this method is undefined. This precondition can be checked
-     * with the `supportsERC165` method in this library.
+     * with {supportsERC165}.
      * Interface identification is specified in ERC-165.
      */
     function _supportsERC165Interface(address account, bytes4 interfaceId) private view returns (bool) {

+ 3 - 3
contracts/introspection/ERC1820Implementer.sol

@@ -11,15 +11,15 @@ import "./IERC1820Implementer.sol";
  * registration to be complete.
  */
 contract ERC1820Implementer is IERC1820Implementer {
-    bytes32 constant private ERC1820_ACCEPT_MAGIC = keccak256(abi.encodePacked("ERC1820_ACCEPT_MAGIC"));
+    bytes32 constant private _ERC1820_ACCEPT_MAGIC = keccak256(abi.encodePacked("ERC1820_ACCEPT_MAGIC"));
 
     mapping(bytes32 => mapping(address => bool)) private _supportedInterfaces;
 
     /**
      * See {IERC1820Implementer-canImplementInterfaceForAddress}.
      */
-    function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view override returns (bytes32) {
-        return _supportedInterfaces[interfaceHash][account] ? ERC1820_ACCEPT_MAGIC : bytes32(0x00);
+    function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) public view override returns (bytes32) {
+        return _supportedInterfaces[interfaceHash][account] ? _ERC1820_ACCEPT_MAGIC : bytes32(0x00);
     }
 
     /**

+ 3 - 3
contracts/math/SignedSafeMath.sol

@@ -5,7 +5,7 @@ pragma solidity ^0.6.0;
  * @dev Signed math operations with safety checks that revert on error.
  */
 library SignedSafeMath {
-    int256 constant private INT256_MIN = -2**255;
+    int256 constant private _INT256_MIN = -2**255;
 
     /**
      * @dev Multiplies two signed integers, reverts on overflow.
@@ -18,7 +18,7 @@ library SignedSafeMath {
             return 0;
         }
 
-        require(!(a == -1 && b == INT256_MIN), "SignedSafeMath: multiplication overflow");
+        require(!(a == -1 && b == _INT256_MIN), "SignedSafeMath: multiplication overflow");
 
         int256 c = a * b;
         require(c / a == b, "SignedSafeMath: multiplication overflow");
@@ -31,7 +31,7 @@ library SignedSafeMath {
      */
     function div(int256 a, int256 b) internal pure returns (int256) {
         require(b != 0, "SignedSafeMath: division by zero");
-        require(!(b == -1 && a == INT256_MIN), "SignedSafeMath: division overflow");
+        require(!(b == -1 && a == _INT256_MIN), "SignedSafeMath: division overflow");
 
         int256 c = a / b;
 

+ 5 - 1
contracts/mocks/AccessControlMock.sol

@@ -4,10 +4,14 @@ import "../access/AccessControl.sol";
 
 contract AccessControlMock is AccessControl {
     constructor() public {
-        _grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
+        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
     }
 
     function setRoleAdmin(bytes32 roleId, bytes32 adminRoleId) public {
         _setRoleAdmin(roleId, adminRoleId);
     }
+
+    function setupRole(bytes32 roleId, address account) public {
+        _setupRole(roleId, account);
+    }
 }

+ 5 - 5
contracts/mocks/ArraysImpl.sol

@@ -5,13 +5,13 @@ import "../utils/Arrays.sol";
 contract ArraysImpl {
     using Arrays for uint256[];
 
-    uint256[] private array;
+    uint256[] private _array;
 
-    constructor (uint256[] memory _array) public {
-        array = _array;
+    constructor (uint256[] memory array) public {
+        _array = array;
     }
 
-    function findUpperBound(uint256 _element) external view returns (uint256) {
-        return array.findUpperBound(_element);
+    function findUpperBound(uint256 element) external view returns (uint256) {
+        return _array.findUpperBound(element);
     }
 }

+ 7 - 5
contracts/mocks/Create2Impl.sol

@@ -1,16 +1,16 @@
 pragma solidity ^0.6.0;
 
 import "../utils/Create2.sol";
-import "../token/ERC20/ERC20.sol";
+import "../introspection/ERC1820Implementer.sol";
 
 contract Create2Impl {
-    function deploy(bytes32 salt, bytes memory code) public {
-        Create2.deploy(salt, code);
+    function deploy(uint256 value, bytes32 salt, bytes memory code) public {
+        Create2.deploy(value, salt, code);
     }
 
-    function deployERC20(bytes32 salt) public {
+    function deployERC1820Implementer(uint256 value, bytes32 salt) public {
         // solhint-disable-next-line indent
-        Create2.deploy(salt, type(ERC20).creationCode);
+        Create2.deploy(value, salt, type(ERC1820Implementer).creationCode);
     }
 
     function computeAddress(bytes32 salt, bytes32 codeHash) public view returns (address) {
@@ -20,4 +20,6 @@ contract Create2Impl {
     function computeAddressWithDeployer(bytes32 salt, bytes32 codeHash, address deployer) public pure returns (address) {
         return Create2.computeAddress(salt, codeHash, deployer);
     }
+
+    receive() payable external {}
 }

+ 1 - 1
contracts/mocks/ERC165/ERC165InterfacesSupported.sol

@@ -34,7 +34,7 @@ contract SupportsInterfaceWithLookupMock is IERC165 {
     /**
      * @dev Implement supportsInterface(bytes4) using a lookup table.
      */
-    function supportsInterface(bytes4 interfaceId) external view override returns (bool) {
+    function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
         return _supportedInterfaces[interfaceId];
     }
 

+ 3 - 3
contracts/mocks/ERC165CheckerMock.sol

@@ -6,14 +6,14 @@ contract ERC165CheckerMock {
     using ERC165Checker for address;
 
     function supportsERC165(address account) public view returns (bool) {
-        return account._supportsERC165();
+        return account.supportsERC165();
     }
 
     function supportsInterface(address account, bytes4 interfaceId) public view returns (bool) {
-        return account._supportsInterface(interfaceId);
+        return account.supportsInterface(interfaceId);
     }
 
     function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) public view returns (bool) {
-        return account._supportsAllInterfaces(interfaceIds);
+        return account.supportsAllInterfaces(interfaceIds);
     }
 }

+ 6 - 1
contracts/mocks/ERC20BurnableMock.sol

@@ -3,7 +3,12 @@ pragma solidity ^0.6.0;
 import "../token/ERC20/ERC20Burnable.sol";
 
 contract ERC20BurnableMock is ERC20Burnable {
-    constructor (address initialAccount, uint256 initialBalance) public {
+    constructor (
+        string memory name,
+        string memory symbol,
+        address initialAccount,
+        uint256 initialBalance
+    ) public ERC20(name, symbol) {
         _mint(initialAccount, initialBalance);
     }
 }

+ 3 - 1
contracts/mocks/ERC20CappedMock.sol

@@ -3,7 +3,9 @@ pragma solidity ^0.6.0;
 import "../token/ERC20/ERC20Capped.sol";
 
 contract ERC20CappedMock is ERC20Capped {
-    constructor (uint256 cap) public ERC20Capped(cap) { }
+    constructor (string memory name, string memory symbol, uint256 cap)
+        public ERC20(name, symbol) ERC20Capped(cap)
+    { }
 
     function mint(address to, uint256 tokenId) public {
         _mint(to, tokenId);

+ 13 - 0
contracts/mocks/ERC20DecimalsMock.sol

@@ -0,0 +1,13 @@
+pragma solidity ^0.6.0;
+
+import "../token/ERC20/ERC20.sol";
+
+contract ERC20DecimalsMock is ERC20 {
+    constructor (string memory name, string memory symbol, uint8 decimals) public ERC20(name, symbol) {
+        _setupDecimals(decimals);
+    }
+
+    function setupDecimals(uint8 decimals) public {
+        _setupDecimals(decimals);
+    }
+}

+ 0 - 13
contracts/mocks/ERC20DetailedMock.sol

@@ -1,13 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "../token/ERC20/ERC20.sol";
-import "../token/ERC20/ERC20Detailed.sol";
-
-contract ERC20DetailedMock is ERC20, ERC20Detailed {
-    constructor (string memory name, string memory symbol, uint8 decimals)
-        public
-        ERC20Detailed(name, symbol, decimals)
-    {
-
-    }
-}

+ 6 - 1
contracts/mocks/ERC20Mock.sol

@@ -4,7 +4,12 @@ import "../token/ERC20/ERC20.sol";
 
 // mock class using ERC20
 contract ERC20Mock is ERC20 {
-    constructor (address initialAccount, uint256 initialBalance) public {
+    constructor (
+        string memory name,
+        string memory symbol,
+        address initialAccount,
+        uint256 initialBalance
+    ) public payable ERC20(name, symbol) {
         _mint(initialAccount, initialBalance);
     }
 

+ 6 - 1
contracts/mocks/ERC20PausableMock.sol

@@ -4,7 +4,12 @@ import "../token/ERC20/ERC20Pausable.sol";
 
 // mock class using ERC20Pausable
 contract ERC20PausableMock is ERC20Pausable {
-    constructor (address initialAccount, uint256 initialBalance) public {
+    constructor (
+        string memory name,
+        string memory symbol,
+        address initialAccount,
+        uint256 initialBalance
+    ) public ERC20(name, symbol) {
         _mint(initialAccount, initialBalance);
     }
 

+ 6 - 1
contracts/mocks/ERC20SnapshotMock.sol

@@ -4,7 +4,12 @@ import "../token/ERC20/ERC20Snapshot.sol";
 
 
 contract ERC20SnapshotMock is ERC20Snapshot {
-    constructor(address initialAccount, uint256 initialBalance) public {
+    constructor(
+        string memory name,
+        string memory symbol,
+        address initialAccount,
+        uint256 initialBalance
+    ) public ERC20(name, symbol) {
         _mint(initialAccount, initialBalance);
     }
 

+ 2 - 0
contracts/mocks/ERC721BurnableMock.sol

@@ -3,6 +3,8 @@ pragma solidity ^0.6.0;
 import "../token/ERC721/ERC721Burnable.sol";
 
 contract ERC721BurnableMock is ERC721Burnable {
+    constructor(string memory name, string memory symbol) public ERC721(name, symbol) { }
+
     function mint(address to, uint256 tokenId) public {
         _mint(to, tokenId);
     }

+ 0 - 37
contracts/mocks/ERC721FullMock.sol

@@ -1,37 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "../token/ERC721/ERC721Full.sol";
-import "../token/ERC721/ERC721Burnable.sol";
-
-/**
- * @title ERC721FullMock
- * This mock just provides public functions for setting metadata URI, getting all tokens of an owner,
- * checking token existence, removal of a token from an address
- */
-contract ERC721FullMock is ERC721Full, ERC721Burnable {
-    constructor (string memory name, string memory symbol) public ERC721Full(name, symbol) { }
-
-    function exists(uint256 tokenId) public view returns (bool) {
-        return _exists(tokenId);
-    }
-
-    function tokensOfOwner(address owner) public view returns (uint256[] memory) {
-        return _tokensOfOwner(owner);
-    }
-
-    function setTokenURI(uint256 tokenId, string memory uri) public {
-        _setTokenURI(tokenId, uri);
-    }
-
-    function setBaseURI(string memory baseURI) public {
-        _setBaseURI(baseURI);
-    }
-
-    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override(ERC721, ERC721Full) {
-        super._beforeTokenTransfer(from, to, tokenId);
-    }
-
-    function mint(address to, uint256 tokenId) public {
-        _mint(to, tokenId);
-    }
-}

+ 5 - 1
contracts/mocks/ERC721GSNRecipientMock.sol

@@ -9,7 +9,11 @@ import "../GSN/GSNRecipientSignature.sol";
  * A simple ERC721 mock that has GSN support enabled
  */
 contract ERC721GSNRecipientMock is ERC721, GSNRecipient, GSNRecipientSignature {
-    constructor(address trustedSigner) public GSNRecipientSignature(trustedSigner) { }
+    constructor(string memory name, string memory symbol, address trustedSigner)
+        public
+        ERC721(name, symbol)
+        GSNRecipientSignature(trustedSigner)
+    { }
 
     function mint(uint256 tokenId) public {
         _mint(_msgSender(), tokenId);

+ 18 - 4
contracts/mocks/ERC721Mock.sol

@@ -7,18 +7,32 @@ import "../token/ERC721/ERC721.sol";
  * This mock just provides a public safeMint, mint, and burn functions for testing purposes
  */
 contract ERC721Mock is ERC721 {
-    function safeMint(address to, uint256 tokenId) public {
-        _safeMint(to, tokenId);
+    constructor (string memory name, string memory symbol) public ERC721(name, symbol) { }
+
+    function exists(uint256 tokenId) public view returns (bool) {
+        return _exists(tokenId);
     }
 
-    function safeMint(address to, uint256 tokenId, bytes memory _data) public {
-        _safeMint(to, tokenId, _data);
+    function setTokenURI(uint256 tokenId, string memory uri) public {
+        _setTokenURI(tokenId, uri);
+    }
+
+    function setBaseURI(string memory baseURI) public {
+        _setBaseURI(baseURI);
     }
 
     function mint(address to, uint256 tokenId) public {
         _mint(to, tokenId);
     }
 
+    function safeMint(address to, uint256 tokenId) public {
+        _safeMint(to, tokenId);
+    }
+
+    function safeMint(address to, uint256 tokenId, bytes memory _data) public {
+        _safeMint(to, tokenId, _data);
+    }
+
     function burn(uint256 tokenId) public {
         _burn(tokenId);
     }

+ 2 - 0
contracts/mocks/ERC721PausableMock.sol

@@ -7,6 +7,8 @@ import "../token/ERC721/ERC721Pausable.sol";
  * This mock just provides a public mint, burn and exists functions for testing purposes
  */
 contract ERC721PausableMock is ERC721Pausable {
+    constructor (string memory name, string memory symbol) public ERC721(name, symbol) { }
+
     function mint(address to, uint256 tokenId) public {
         super._mint(to, tokenId);
     }

+ 6 - 6
contracts/mocks/ERC777SenderRecipientMock.sol

@@ -37,8 +37,8 @@ contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient,
 
     IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
 
-    bytes32 constant private TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
-    bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
+    bytes32 constant private _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
+    bytes32 constant private _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
 
     function tokensToSend(
         address operator,
@@ -103,7 +103,7 @@ contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient,
     }
 
     function senderFor(address account) public {
-        _registerInterfaceForAddress(TOKENS_SENDER_INTERFACE_HASH, account);
+        _registerInterfaceForAddress(_TOKENS_SENDER_INTERFACE_HASH, account);
 
         address self = address(this);
         if (account == self) {
@@ -112,11 +112,11 @@ contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient,
     }
 
     function registerSender(address sender) public {
-        _erc1820.setInterfaceImplementer(address(this), TOKENS_SENDER_INTERFACE_HASH, sender);
+        _erc1820.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, sender);
     }
 
     function recipientFor(address account) public {
-        _registerInterfaceForAddress(TOKENS_RECIPIENT_INTERFACE_HASH, account);
+        _registerInterfaceForAddress(_TOKENS_RECIPIENT_INTERFACE_HASH, account);
 
         address self = address(this);
         if (account == self) {
@@ -125,7 +125,7 @@ contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient,
     }
 
     function registerRecipient(address recipient) public {
-        _erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, recipient);
+        _erc1820.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, recipient);
     }
 
     function setShouldRevertSend(bool shouldRevert) public {

+ 38 - 0
contracts/mocks/EnumerableMapMock.sol

@@ -0,0 +1,38 @@
+pragma solidity ^0.6.0;
+
+import "../utils/EnumerableMap.sol";
+
+contract EnumerableMapMock {
+    using EnumerableMap for EnumerableMap.UintToAddressMap;
+
+    event OperationResult(bool result);
+
+    EnumerableMap.UintToAddressMap private _map;
+
+    function contains(uint256 key) public view returns (bool) {
+        return _map.contains(key);
+    }
+
+    function set(uint256 key, address value) public {
+        bool result = _map.set(key, value);
+        emit OperationResult(result);
+    }
+
+    function remove(uint256 key) public {
+        bool result = _map.remove(key);
+        emit OperationResult(result);
+    }
+
+    function length() public view returns (uint256) {
+        return _map.length();
+    }
+
+    function at(uint256 index) public view returns (uint256 key, address value) {
+        return _map.at(index);
+    }
+
+
+    function get(uint256 key) public view returns (address) {
+        return _map.get(key);
+    }
+}

+ 11 - 15
contracts/mocks/EnumerableSetMock.sol

@@ -2,36 +2,32 @@ pragma solidity ^0.6.0;
 
 import "../utils/EnumerableSet.sol";
 
-contract EnumerableSetMock{
+contract EnumerableSetMock {
     using EnumerableSet for EnumerableSet.AddressSet;
 
-    event TransactionResult(bool result);
+    event OperationResult(bool result);
 
-    EnumerableSet.AddressSet private set;
+    EnumerableSet.AddressSet private _set;
 
     function contains(address value) public view returns (bool) {
-        return set.contains(value);
+        return _set.contains(value);
     }
 
     function add(address value) public {
-        bool result = set.add(value);
-        emit TransactionResult(result);
+        bool result = _set.add(value);
+        emit OperationResult(result);
     }
 
     function remove(address value) public {
-        bool result = set.remove(value);
-        emit TransactionResult(result);
-    }
-
-    function enumerate() public view returns (address[] memory) {
-        return set.enumerate();
+        bool result = _set.remove(value);
+        emit OperationResult(result);
     }
 
     function length() public view returns (uint256) {
-        return set.length();
+        return _set.length();
     }
 
-    function get(uint256 index) public view returns (address) {
-        return set.get(index);
+    function at(uint256 index) public view returns (address) {
+        return _set.at(index);
     }
 }

+ 5 - 5
contracts/mocks/ReentrancyMock.sol

@@ -11,19 +11,19 @@ contract ReentrancyMock is ReentrancyGuard {
     }
 
     function callback() external nonReentrant {
-        count();
+        _count();
     }
 
     function countLocalRecursive(uint256 n) public nonReentrant {
         if (n > 0) {
-            count();
+            _count();
             countLocalRecursive(n - 1);
         }
     }
 
     function countThisRecursive(uint256 n) public nonReentrant {
         if (n > 0) {
-            count();
+            _count();
             // solhint-disable-next-line avoid-low-level-calls
             (bool success,) = address(this).call(abi.encodeWithSignature("countThisRecursive(uint256)", n - 1));
             require(success, "ReentrancyMock: failed call");
@@ -31,12 +31,12 @@ contract ReentrancyMock is ReentrancyGuard {
     }
 
     function countAndCall(ReentrancyAttack attacker) public nonReentrant {
-        count();
+        _count();
         bytes4 func = bytes4(keccak256("callback()"));
         attacker.callSender(func);
     }
 
-    function count() private {
+    function _count() private {
         counter += 1;
     }
 }

+ 9 - 0
contracts/mocks/SafeCastMock.sol

@@ -4,6 +4,15 @@ import "../utils/SafeCast.sol";
 
 contract SafeCastMock {
     using SafeCast for uint;
+    using SafeCast for int;
+
+    function toUint256(int a) public pure returns (uint256) {
+        return a.toUint256();
+    }
+
+    function toInt256(uint a) public pure returns (int256) {
+        return a.toInt256();
+    }
 
     function toUint128(uint a) public pure returns (uint128) {
         return a.toUint128();

+ 65 - 0
contracts/token/ERC20/ERC20.sol

@@ -3,6 +3,7 @@ pragma solidity ^0.6.0;
 import "../../GSN/Context.sol";
 import "./IERC20.sol";
 import "../../math/SafeMath.sol";
+import "../../utils/Address.sol";
 
 /**
  * @dev Implementation of the {IERC20} interface.
@@ -30,6 +31,7 @@ import "../../math/SafeMath.sol";
  */
 contract ERC20 is Context, IERC20 {
     using SafeMath for uint256;
+    using Address for address;
 
     mapping (address => uint256) private _balances;
 
@@ -37,6 +39,57 @@ contract ERC20 is Context, IERC20 {
 
     uint256 private _totalSupply;
 
+    string private _name;
+    string private _symbol;
+    uint8 private _decimals;
+
+    /**
+     * @dev Sets the values for {name} and {symbol}, initializes {decimals} with
+     * a default value of 18.
+     *
+     * To select a different value for {decimals}, use {_setupDecimals}.
+     *
+     * All three of these values are immutable: they can only be set once during
+     * construction.
+     */
+    constructor (string memory name, string memory symbol) public {
+        _name = name;
+        _symbol = symbol;
+        _decimals = 18;
+    }
+
+    /**
+     * @dev Returns the name of the token.
+     */
+    function name() public view returns (string memory) {
+        return _name;
+    }
+
+    /**
+     * @dev Returns the symbol of the token, usually a shorter version of the
+     * name.
+     */
+    function symbol() public view returns (string memory) {
+        return _symbol;
+    }
+
+    /**
+     * @dev Returns the number of decimals used to get its user representation.
+     * For example, if `decimals` equals `2`, a balance of `505` tokens should
+     * be displayed to a user as `5,05` (`505 / 10 ** 2`).
+     *
+     * Tokens usually opt for a value of 18, imitating the relationship between
+     * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is
+     * called.
+     *
+     * NOTE: This information is only used for _display_ purposes: it in
+     * no way affects any of the arithmetic of the contract, including
+     * {IERC20-balanceOf} and {IERC20-transfer}.
+     */
+    function decimals() public view returns (uint8) {
+        return _decimals;
+    }
+
     /**
      * @dev See {IERC20-totalSupply}.
      */
@@ -223,6 +276,18 @@ contract ERC20 is Context, IERC20 {
         emit Approval(owner, spender, amount);
     }
 
+    /**
+     * @dev Sets {decimals} to a value other than the default one of 18.
+     *
+     * Requirements:
+     *
+     * - this function can only be called from a constructor.
+     */
+    function _setupDecimals(uint8 decimals_) internal {
+        require(!address(this).isContract(), "ERC20: decimals cannot be changed after construction");
+        _decimals = decimals_;
+    }
+
     /**
      * @dev Hook that is called before any transfer of tokens. This includes
      * minting and burning.

+ 1 - 1
contracts/token/ERC20/ERC20Burnable.sol

@@ -8,7 +8,7 @@ import "./ERC20.sol";
  * tokens and those that they have an allowance for, in a way that can be
  * recognized off-chain (via event analysis).
  */
-contract ERC20Burnable is Context, ERC20 {
+abstract contract ERC20Burnable is Context, ERC20 {
     /**
      * @dev Destroys `amount` tokens from the caller.
      *

+ 1 - 1
contracts/token/ERC20/ERC20Capped.sol

@@ -5,7 +5,7 @@ import "./ERC20.sol";
 /**
  * @dev Extension of {ERC20} that adds a cap to the supply of tokens.
  */
-contract ERC20Capped is ERC20 {
+abstract contract ERC20Capped is ERC20 {
     uint256 private _cap;
 
     /**

+ 0 - 54
contracts/token/ERC20/ERC20Detailed.sol

@@ -1,54 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "./IERC20.sol";
-
-/**
- * @dev Optional functions from the ERC20 standard.
- */
-abstract contract ERC20Detailed is IERC20 {
-    string private _name;
-    string private _symbol;
-    uint8 private _decimals;
-
-    /**
-     * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of
-     * these values are immutable: they can only be set once during
-     * construction.
-     */
-    constructor (string memory name, string memory symbol, uint8 decimals) public {
-        _name = name;
-        _symbol = symbol;
-        _decimals = decimals;
-    }
-
-    /**
-     * @dev Returns the name of the token.
-     */
-    function name() public view returns (string memory) {
-        return _name;
-    }
-
-    /**
-     * @dev Returns the symbol of the token, usually a shorter version of the
-     * name.
-     */
-    function symbol() public view returns (string memory) {
-        return _symbol;
-    }
-
-    /**
-     * @dev Returns the number of decimals used to get its user representation.
-     * For example, if `decimals` equals `2`, a balance of `505` tokens should
-     * be displayed to a user as `5,05` (`505 / 10 ** 2`).
-     *
-     * Tokens usually opt for a value of 18, imitating the relationship between
-     * Ether and Wei.
-     *
-     * NOTE: This information is only used for _display_ purposes: it in
-     * no way affects any of the arithmetic of the contract, including
-     * {IERC20-balanceOf} and {IERC20-transfer}.
-     */
-    function decimals() public view returns (uint8) {
-        return _decimals;
-    }
-}

+ 1 - 1
contracts/token/ERC20/ERC20Pausable.sol

@@ -11,7 +11,7 @@ import "../../utils/Pausable.sol";
  * period, or having an emergency switch for freezing all token transfers in the
  * event of a large bug.
  */
-contract ERC20Pausable is ERC20, Pausable {
+abstract contract ERC20Pausable is ERC20, Pausable {
     /**
      * @dev See {ERC20-_beforeTokenTransfer}.
      *

+ 1 - 1
contracts/token/ERC20/ERC20Snapshot.sol

@@ -17,7 +17,7 @@ import "./ERC20.sol";
  * account address.
  * @author Validity Labs AG <info@validitylabs.org>
  */
-contract ERC20Snapshot is ERC20 {
+abstract contract ERC20Snapshot is ERC20 {
     // Inspired by Jordi Baylina's MiniMeToken to record historical balances:
     // https://github.com/Giveth/minimd/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol
 

+ 10 - 18
contracts/token/ERC20/README.adoc

@@ -2,48 +2,40 @@
 
 This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-20[ERC20 Token Standard].
 
-TIP: For an overview of ERC20 tokens and a walkthrough on how to create a token contract read our xref:ROOT:tokens.adoc#ERC20[ERC20 guide].
+TIP: For an overview of ERC20 tokens and a walkthrough on how to create a token contract read our xref:ROOT:erc20.adoc[ERC20 guide].
 
 There a few core contracts that implement the behavior specified in the EIP:
 
-* {IERC20}: the interface all ERC20 implementations should conform to
-* {ERC20}: the base implementation of the ERC20 interface
-* {ERC20Detailed}: includes the <<ERC20Detailed-name,`name`>>,
-   <<ERC20Detailed-symbol,`symbol`>> and <<ERC20Detailed-decimals,`decimals`>>
-   optional standard extension to the base interface
+* {IERC20}: the interface all ERC20 implementations should conform to.
+* {ERC20}: the implementation of the ERC20 interface, including the <<ERC20-name,`name`>>, <<ERC20-symbol,`symbol`>> and <<ERC20-decimals,`decimals`>> optional standard extension to the base interface.
 
 Additionally there are multiple custom extensions, including:
 
-* designation of addresses that can create token supply ({ERC20Mintable}), with an optional maximum cap ({ERC20Capped})
-* destruction of own tokens ({ERC20Burnable})
-* designation of addresses that can pause token operations for all users ({ERC20Pausable}).
+* designation of addresses that can pause token transfers for all users ({ERC20Pausable}).
+* efficient storage of past token balances to be later queried at any point in time ({ERC20Snapshot}).
+* destruction of own tokens ({ERC20Burnable}).
+* enforcement of a cap to the total supply when minting tokens ({ERC20Capped}).
 
 Finally, there are some utilities to interact with ERC20 contracts in various ways.
 
 * {SafeERC20} is a wrapper around the interface that eliminates the need to handle boolean return values.
 * {TokenTimelock} can hold tokens for a beneficiary until a specified time.
 
-NOTE: This page is incomplete. We're working to improve it for the next release. Stay tuned!
-
 == Core
 
 {{IERC20}}
 
 {{ERC20}}
 
-{{ERC20Detailed}}
-
 == Extensions
 
-{{ERC20Mintable}}
-
-{{ERC20Burnable}}
+{{ERC20Snapshot}}
 
 {{ERC20Pausable}}
 
-{{ERC20Capped}}
+{{ERC20Burnable}}
 
-{{ERC20Snapshot}}
+{{ERC20Capped}}
 
 == Utilities
 

+ 6 - 6
contracts/token/ERC20/SafeERC20.sol

@@ -18,11 +18,11 @@ library SafeERC20 {
     using Address for address;
 
     function safeTransfer(IERC20 token, address to, uint256 value) internal {
-        callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
+        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
     }
 
     function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
-        callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
+        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
     }
 
     function safeApprove(IERC20 token, address spender, uint256 value) internal {
@@ -33,17 +33,17 @@ library SafeERC20 {
         require((value == 0) || (token.allowance(address(this), spender) == 0),
             "SafeERC20: approve from non-zero to non-zero allowance"
         );
-        callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
+        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
     }
 
     function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
         uint256 newAllowance = token.allowance(address(this), spender).add(value);
-        callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
+        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
     }
 
     function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
         uint256 newAllowance = token.allowance(address(this), spender).sub(value, "SafeERC20: decreased allowance below zero");
-        callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
+        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
     }
 
     /**
@@ -52,7 +52,7 @@ library SafeERC20 {
      * @param token The token targeted by the call.
      * @param data The call data (encoded using abi.encode or one of its variants).
      */
-    function callOptionalReturn(IERC20 token, bytes memory data) private {
+    function _callOptionalReturn(IERC20 token, bytes memory data) private {
         // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
         // we're implementing it ourselves.
 

+ 170 - 29
contracts/token/ERC721/ERC721.sol

@@ -2,37 +2,53 @@ pragma solidity ^0.6.0;
 
 import "../../GSN/Context.sol";
 import "./IERC721.sol";
+import "./IERC721Metadata.sol";
+import "./IERC721Enumerable.sol";
 import "./IERC721Receiver.sol";
+import "../../introspection/ERC165.sol";
 import "../../math/SafeMath.sol";
 import "../../utils/Address.sol";
-import "../../utils/Counters.sol";
-import "../../introspection/ERC165.sol";
+import "../../utils/EnumerableSet.sol";
+import "../../utils/EnumerableMap.sol";
 
 /**
  * @title ERC721 Non-Fungible Token Standard basic implementation
  * @dev see https://eips.ethereum.org/EIPS/eip-721
  */
-contract ERC721 is Context, ERC165, IERC721 {
+contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable {
     using SafeMath for uint256;
     using Address for address;
-    using Counters for Counters.Counter;
+    using EnumerableSet for EnumerableSet.UintSet;
+    using EnumerableMap for EnumerableMap.UintToAddressMap;
 
     // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
     // which can be also obtained as `IERC721Receiver(0).onERC721Received.selector`
     bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
 
-    // Mapping from token ID to owner
-    mapping (uint256 => address) private _tokenOwner;
+    // Mapping from holder address to their (enumerable) set of owned tokens
+    mapping (address => EnumerableSet.UintSet) private _holderTokens;
+
+    // Enumerable mapping from token ids to their owners
+    EnumerableMap.UintToAddressMap private _tokenOwners;
 
     // Mapping from token ID to approved address
     mapping (uint256 => address) private _tokenApprovals;
 
-    // Mapping from owner to number of owned token
-    mapping (address => Counters.Counter) private _ownedTokensCount;
-
     // Mapping from owner to operator approvals
     mapping (address => mapping (address => bool)) private _operatorApprovals;
 
+    // Token name
+    string private _name;
+
+    // Token symbol
+    string private _symbol;
+
+    // Optional mapping for token URIs
+    mapping(uint256 => string) private _tokenURIs;
+
+    // Base URI
+    string private _baseURI;
+
     /*
      *     bytes4(keccak256('balanceOf(address)')) == 0x70a08231
      *     bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
@@ -49,9 +65,32 @@ contract ERC721 is Context, ERC165, IERC721 {
      */
     bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
 
-    constructor () public {
+    /*
+     *     bytes4(keccak256('name()')) == 0x06fdde03
+     *     bytes4(keccak256('symbol()')) == 0x95d89b41
+     *     bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd
+     *
+     *     => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
+     */
+    bytes4 private constant _INTERFACE_ID_ERC721_METADATA = 0x5b5e139f;
+
+    /*
+     *     bytes4(keccak256('totalSupply()')) == 0x18160ddd
+     *     bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) == 0x2f745c59
+     *     bytes4(keccak256('tokenByIndex(uint256)')) == 0x4f6ccce7
+     *
+     *     => 0x18160ddd ^ 0x2f745c59 ^ 0x4f6ccce7 == 0x780e9d63
+     */
+    bytes4 private constant _INTERFACE_ID_ERC721_ENUMERABLE = 0x780e9d63;
+
+    constructor (string memory name, string memory symbol) public {
+        _name = name;
+        _symbol = symbol;
+
         // register the supported interfaces to conform to ERC721 via ERC165
         _registerInterface(_INTERFACE_ID_ERC721);
+        _registerInterface(_INTERFACE_ID_ERC721_METADATA);
+        _registerInterface(_INTERFACE_ID_ERC721_ENUMERABLE);
     }
 
     /**
@@ -62,7 +101,7 @@ contract ERC721 is Context, ERC165, IERC721 {
     function balanceOf(address owner) public view override returns (uint256) {
         require(owner != address(0), "ERC721: balance query for the zero address");
 
-        return _ownedTokensCount[owner].current();
+        return _holderTokens[owner].length();
     }
 
     /**
@@ -71,10 +110,84 @@ contract ERC721 is Context, ERC165, IERC721 {
      * @return address currently marked as the owner of the given token ID
      */
     function ownerOf(uint256 tokenId) public view override returns (address) {
-        address owner = _tokenOwner[tokenId];
-        require(owner != address(0), "ERC721: owner query for nonexistent token");
+        return _tokenOwners.get(tokenId, "ERC721: owner query for nonexistent token");
+    }
+
+    /**
+     * @dev Gets the token name.
+     * @return string representing the token name
+     */
+    function name() public view override returns (string memory) {
+        return _name;
+    }
+
+    /**
+     * @dev Gets the token symbol.
+     * @return string representing the token symbol
+     */
+    function symbol() public view override returns (string memory) {
+        return _symbol;
+    }
+
+    /**
+     * @dev Returns the URI for a given token ID. May return an empty string.
+     *
+     * If the token's URI is non-empty and a base URI was set (via
+     * {_setBaseURI}), it will be added to the token ID's URI as a prefix.
+     *
+     * Reverts if the token ID does not exist.
+     */
+    function tokenURI(uint256 tokenId) public view override returns (string memory) {
+        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
+
+        string memory _tokenURI = _tokenURIs[tokenId];
 
-        return owner;
+        // Even if there is a base URI, it is only appended to non-empty token-specific URIs
+        if (bytes(_tokenURI).length == 0) {
+            return "";
+        } else {
+            // abi.encodePacked is being used to concatenate strings
+            return string(abi.encodePacked(_baseURI, _tokenURI));
+        }
+    }
+
+    /**
+    * @dev Returns the base URI set via {_setBaseURI}. This will be
+    * automatically added as a preffix in {tokenURI} to each token's URI, when
+    * they are non-empty.
+    */
+    function baseURI() public view returns (string memory) {
+        return _baseURI;
+    }
+
+    /**
+     * @dev Gets the token ID at a given index of the tokens list of the requested owner.
+     * @param owner address owning the tokens list to be accessed
+     * @param index uint256 representing the index to be accessed of the requested tokens list
+     * @return uint256 token ID at the given index of the tokens list owned by the requested address
+     */
+    function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
+        return _holderTokens[owner].at(index);
+    }
+
+    /**
+     * @dev Gets the total amount of tokens stored by the contract.
+     * @return uint256 representing the total amount of tokens
+     */
+    function totalSupply() public view override returns (uint256) {
+        // _tokenOwners are indexed by tokenIds, so .length() returns the number of tokenIds
+        return _tokenOwners.length();
+    }
+
+    /**
+     * @dev Gets the token ID at a given index of all the tokens in this contract
+     * Reverts if the index is greater or equal to the total number of tokens.
+     * @param index uint256 representing the index to be accessed of the tokens list
+     * @return uint256 token ID at the given index of the tokens list
+     */
+    function tokenByIndex(uint256 index) public view override returns (uint256) {
+        (uint256 tokenId, ) = _tokenOwners.at(index);
+        return tokenId;
     }
 
     /**
@@ -143,7 +256,7 @@ contract ERC721 is Context, ERC165, IERC721 {
         //solhint-disable-next-line max-line-length
         require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
 
-        _transferFrom(from, to, tokenId);
+        _transfer(from, to, tokenId);
     }
 
     /**
@@ -175,7 +288,7 @@ contract ERC721 is Context, ERC165, IERC721 {
      */
     function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
         require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
-        _safeTransferFrom(from, to, tokenId, _data);
+        _safeTransfer(from, to, tokenId, _data);
     }
 
     /**
@@ -190,8 +303,8 @@ contract ERC721 is Context, ERC165, IERC721 {
      * @param tokenId uint256 ID of the token to be transferred
      * @param _data bytes data to send along with a safe transfer check
      */
-    function _safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
-        _transferFrom(from, to, tokenId);
+    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
+        _transfer(from, to, tokenId);
         require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
     }
 
@@ -201,8 +314,7 @@ contract ERC721 is Context, ERC165, IERC721 {
      * @return bool whether the token exists
      */
     function _exists(uint256 tokenId) internal view returns (bool) {
-        address owner = _tokenOwner[tokenId];
-        return owner != address(0);
+        return _tokenOwners.contains(tokenId);
     }
 
     /**
@@ -260,8 +372,9 @@ contract ERC721 is Context, ERC165, IERC721 {
 
         _beforeTokenTransfer(address(0), to, tokenId);
 
-        _tokenOwner[tokenId] = to;
-        _ownedTokensCount[to].increment();
+        _holderTokens[to].add(tokenId);
+
+        _tokenOwners.set(tokenId, to);
 
         emit Transfer(address(0), to, tokenId);
     }
@@ -279,8 +392,14 @@ contract ERC721 is Context, ERC165, IERC721 {
         // Clear approvals
         _approve(address(0), tokenId);
 
-        _ownedTokensCount[owner].decrement();
-        _tokenOwner[tokenId] = address(0);
+        // Clear metadata (if any)
+        if (bytes(_tokenURIs[tokenId]).length != 0) {
+            delete _tokenURIs[tokenId];
+        }
+
+        _holderTokens[owner].remove(tokenId);
+
+        _tokenOwners.remove(tokenId);
 
         emit Transfer(owner, address(0), tokenId);
     }
@@ -292,23 +411,45 @@ contract ERC721 is Context, ERC165, IERC721 {
      * @param to address to receive the ownership of the given token ID
      * @param tokenId uint256 ID of the token to be transferred
      */
-    function _transferFrom(address from, address to, uint256 tokenId) internal virtual {
+    function _transfer(address from, address to, uint256 tokenId) internal virtual {
         require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
         require(to != address(0), "ERC721: transfer to the zero address");
 
         _beforeTokenTransfer(from, to, tokenId);
 
-        // Clear approvals
+        // Clear approvals from the previous owner
         _approve(address(0), tokenId);
 
-        _ownedTokensCount[from].decrement();
-        _ownedTokensCount[to].increment();
+        _holderTokens[from].remove(tokenId);
+        _holderTokens[to].add(tokenId);
 
-        _tokenOwner[tokenId] = to;
+        _tokenOwners.set(tokenId, to);
 
         emit Transfer(from, to, tokenId);
     }
 
+    /**
+     * @dev Internal function to set the token URI for a given token.
+     *
+     * Reverts if the token ID does not exist.
+     *
+     * TIP: If all token IDs share a prefix (for example, if your URIs look like
+     * `https://api.myproject.com/token/<id>`), use {_setBaseURI} to store
+     * it and save gas.
+     */
+    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
+        require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
+        _tokenURIs[tokenId] = _tokenURI;
+    }
+
+    /**
+     * @dev Internal function to set the base URI for all token IDs. It is
+     * automatically added as a prefix to the value returned in {tokenURI}.
+     */
+    function _setBaseURI(string memory baseURI_) internal virtual {
+        _baseURI = baseURI_;
+    }
+
     /**
      * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
      * The call is not executed if the target address is not a contract.

+ 1 - 1
contracts/token/ERC721/ERC721Burnable.sol

@@ -7,7 +7,7 @@ import "./ERC721.sol";
  * @title ERC721 Burnable Token
  * @dev ERC721 Token that can be irreversibly burned (destroyed).
  */
-contract ERC721Burnable is Context, ERC721 {
+abstract contract ERC721Burnable is Context, ERC721 {
     /**
      * @dev Burns a specific ERC721 token.
      * @param tokenId uint256 id of the ERC721 token to be burned.

+ 0 - 175
contracts/token/ERC721/ERC721Enumerable.sol

@@ -1,175 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "../../GSN/Context.sol";
-import "./IERC721Enumerable.sol";
-import "./ERC721.sol";
-import "../../introspection/ERC165.sol";
-
-/**
- * @title ERC-721 Non-Fungible Token with optional enumeration extension logic
- * @dev See https://eips.ethereum.org/EIPS/eip-721
- */
-contract ERC721Enumerable is Context, ERC165, ERC721, IERC721Enumerable {
-    // Mapping from owner to list of owned token IDs
-    mapping(address => uint256[]) private _ownedTokens;
-
-    // Mapping from token ID to index of the owner tokens list
-    mapping(uint256 => uint256) private _ownedTokensIndex;
-
-    // Array with all token ids, used for enumeration
-    uint256[] private _allTokens;
-
-    // Mapping from token id to position in the allTokens array
-    mapping(uint256 => uint256) private _allTokensIndex;
-
-    /*
-     *     bytes4(keccak256('totalSupply()')) == 0x18160ddd
-     *     bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) == 0x2f745c59
-     *     bytes4(keccak256('tokenByIndex(uint256)')) == 0x4f6ccce7
-     *
-     *     => 0x18160ddd ^ 0x2f745c59 ^ 0x4f6ccce7 == 0x780e9d63
-     */
-    bytes4 private constant _INTERFACE_ID_ERC721_ENUMERABLE = 0x780e9d63;
-
-    /**
-     * @dev Constructor function.
-     */
-    constructor () public {
-        // register the supported interface to conform to ERC721Enumerable via ERC165
-        _registerInterface(_INTERFACE_ID_ERC721_ENUMERABLE);
-    }
-
-    /**
-     * @dev Gets the token ID at a given index of the tokens list of the requested owner.
-     * @param owner address owning the tokens list to be accessed
-     * @param index uint256 representing the index to be accessed of the requested tokens list
-     * @return uint256 token ID at the given index of the tokens list owned by the requested address
-     */
-    function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
-        require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
-        return _ownedTokens[owner][index];
-    }
-
-    /**
-     * @dev Gets the total amount of tokens stored by the contract.
-     * @return uint256 representing the total amount of tokens
-     */
-    function totalSupply() public view override returns (uint256) {
-        return _allTokens.length;
-    }
-
-    /**
-     * @dev Gets the token ID at a given index of all the tokens in this contract
-     * Reverts if the index is greater or equal to the total number of tokens.
-     * @param index uint256 representing the index to be accessed of the tokens list
-     * @return uint256 token ID at the given index of the tokens list
-     */
-    function tokenByIndex(uint256 index) public view override returns (uint256) {
-        require(index < totalSupply(), "ERC721Enumerable: global index out of bounds");
-        return _allTokens[index];
-    }
-
-    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override {
-        super._beforeTokenTransfer(from, to, tokenId);
-
-        if (from == address(0)) {
-            // When minting
-            _addTokenToOwnerEnumeration(to, tokenId);
-            _addTokenToAllTokensEnumeration(tokenId);
-        } else if (to == address(0)) {
-            // When burning
-            _removeTokenFromOwnerEnumeration(from, tokenId);
-            // Since tokenId will be deleted, we can clear its slot in _ownedTokensIndex to trigger a gas refund
-            _ownedTokensIndex[tokenId] = 0;
-
-            _removeTokenFromAllTokensEnumeration(tokenId);
-        } else {
-            _removeTokenFromOwnerEnumeration(from, tokenId);
-            _addTokenToOwnerEnumeration(to, tokenId);
-        }
-    }
-
-    /**
-     * @dev Gets the list of token IDs of the requested owner.
-     * @param owner address owning the tokens
-     * @return uint256[] List of token IDs owned by the requested address
-     */
-    function _tokensOfOwner(address owner) internal view returns (uint256[] storage) {
-        return _ownedTokens[owner];
-    }
-
-    /**
-     * @dev Private function to add a token to this extension's ownership-tracking data structures.
-     * @param to address representing the new owner of the given token ID
-     * @param tokenId uint256 ID of the token to be added to the tokens list of the given address
-     */
-    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
-        _ownedTokensIndex[tokenId] = _ownedTokens[to].length;
-        _ownedTokens[to].push(tokenId);
-    }
-
-    /**
-     * @dev Private function to add a token to this extension's token tracking data structures.
-     * @param tokenId uint256 ID of the token to be added to the tokens list
-     */
-    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
-        _allTokensIndex[tokenId] = _allTokens.length;
-        _allTokens.push(tokenId);
-    }
-
-    /**
-     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
-     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
-     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
-     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
-     * @param from address representing the previous owner of the given token ID
-     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
-     */
-    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
-        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
-        // then delete the last slot (swap and pop).
-
-        uint256 lastTokenIndex = _ownedTokens[from].length.sub(1);
-        uint256 tokenIndex = _ownedTokensIndex[tokenId];
-
-        // When the token to delete is the last token, the swap operation is unnecessary
-        if (tokenIndex != lastTokenIndex) {
-            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];
-
-            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
-            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
-        }
-
-        // Deletes the contents at the last position of the array
-        _ownedTokens[from].pop();
-
-        // Note that _ownedTokensIndex[tokenId] hasn't been cleared: it still points to the old slot (now occupied by
-        // lastTokenId, or just over the end of the array if the token was the last one).
-    }
-
-    /**
-     * @dev Private function to remove a token from this extension's token tracking data structures.
-     * This has O(1) time complexity, but alters the order of the _allTokens array.
-     * @param tokenId uint256 ID of the token to be removed from the tokens list
-     */
-    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
-        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
-        // then delete the last slot (swap and pop).
-
-        uint256 lastTokenIndex = _allTokens.length.sub(1);
-        uint256 tokenIndex = _allTokensIndex[tokenId];
-
-        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
-        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
-        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
-        uint256 lastTokenId = _allTokens[lastTokenIndex];
-
-        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
-        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
-
-        // Delete the contents at the last position of the array
-        _allTokens.pop();
-
-        _allTokensIndex[tokenId] = 0;
-    }
-}

+ 0 - 23
contracts/token/ERC721/ERC721Full.sol

@@ -1,23 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "./ERC721Enumerable.sol";
-import "./ERC721Metadata.sol";
-
-/**
- * @title Full ERC721 Token
- * @dev This implementation includes all the required and some optional functionality of the ERC721 standard
- * Moreover, it includes approve all functionality using operator terminology.
- *
- * See https://eips.ethereum.org/EIPS/eip-721
- */
-contract ERC721Full is ERC721Enumerable, ERC721Metadata {
-    constructor (string memory name, string memory symbol) public ERC721Metadata(name, symbol) { }
-
-    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
-        virtual
-        override(ERC721Enumerable, ERC721Metadata)
-        internal
-    {
-        super._beforeTokenTransfer(from, to, tokenId);
-    }
-}

+ 0 - 121
contracts/token/ERC721/ERC721Metadata.sol

@@ -1,121 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "../../GSN/Context.sol";
-import "./ERC721.sol";
-import "./IERC721Metadata.sol";
-import "../../introspection/ERC165.sol";
-
-contract ERC721Metadata is Context, ERC165, ERC721, IERC721Metadata {
-    // Token name
-    string private _name;
-
-    // Token symbol
-    string private _symbol;
-
-    // Optional mapping for token URIs
-    mapping(uint256 => string) private _tokenURIs;
-
-    // Base URI
-    string private _baseURI;
-
-    /*
-     *     bytes4(keccak256('name()')) == 0x06fdde03
-     *     bytes4(keccak256('symbol()')) == 0x95d89b41
-     *     bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd
-     *
-     *     => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
-     */
-    bytes4 private constant _INTERFACE_ID_ERC721_METADATA = 0x5b5e139f;
-
-    /**
-     * @dev Constructor function
-     */
-    constructor (string memory name, string memory symbol) public {
-        _name = name;
-        _symbol = symbol;
-
-        // register the supported interfaces to conform to ERC721 via ERC165
-        _registerInterface(_INTERFACE_ID_ERC721_METADATA);
-    }
-
-    /**
-     * @dev Gets the token name.
-     * @return string representing the token name
-     */
-    function name() external view override returns (string memory) {
-        return _name;
-    }
-
-    /**
-     * @dev Gets the token symbol.
-     * @return string representing the token symbol
-     */
-    function symbol() external view override returns (string memory) {
-        return _symbol;
-    }
-
-    /**
-     * @dev Returns the URI for a given token ID. May return an empty string.
-     *
-     * If the token's URI is non-empty and a base URI was set (via
-     * {_setBaseURI}), it will be added to the token ID's URI as a prefix.
-     *
-     * Reverts if the token ID does not exist.
-     */
-    function tokenURI(uint256 tokenId) external view override returns (string memory) {
-        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
-
-        string memory _tokenURI = _tokenURIs[tokenId];
-
-        // Even if there is a base URI, it is only appended to non-empty token-specific URIs
-        if (bytes(_tokenURI).length == 0) {
-            return "";
-        } else {
-            // abi.encodePacked is being used to concatenate strings
-            return string(abi.encodePacked(_baseURI, _tokenURI));
-        }
-    }
-
-    /**
-     * @dev Internal function to set the token URI for a given token.
-     *
-     * Reverts if the token ID does not exist.
-     *
-     * TIP: if all token IDs share a prefix (e.g. if your URIs look like
-     * `http://api.myproject.com/token/<id>`), use {_setBaseURI} to store
-     * it and save gas.
-     */
-    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
-        require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
-        _tokenURIs[tokenId] = _tokenURI;
-    }
-
-    /**
-     * @dev Internal function to set the base URI for all token IDs. It is
-     * automatically added as a prefix to the value returned in {tokenURI}.
-     */
-    function _setBaseURI(string memory baseURI) internal virtual {
-        _baseURI = baseURI;
-    }
-
-    /**
-    * @dev Returns the base URI set via {_setBaseURI}. This will be
-    * automatically added as a preffix in {tokenURI} to each token's URI, when
-    * they are non-empty.
-    */
-    function baseURI() external view returns (string memory) {
-        return _baseURI;
-    }
-
-
-    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override {
-        super._beforeTokenTransfer(from, to, tokenId);
-
-        if (to == address(0)) { // When burning tokens
-            // Clear metadata (if any)
-            if (bytes(_tokenURIs[tokenId]).length != 0) {
-                delete _tokenURIs[tokenId];
-            }
-        }
-    }
-}

+ 1 - 1
contracts/token/ERC721/ERC721Pausable.sol

@@ -7,7 +7,7 @@ import "../../utils/Pausable.sol";
  * @title ERC721 Non-Fungible Pausable token
  * @dev ERC721 modified with pausable transfers.
  */
-contract ERC721Pausable is ERC721, Pausable {
+abstract contract ERC721Pausable is ERC721, Pausable {
     function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override {
         super._beforeTokenTransfer(from, to, tokenId);
 

+ 0 - 11
contracts/token/ERC721/IERC721Full.sol

@@ -1,11 +0,0 @@
-pragma solidity ^0.6.0;
-
-import "./IERC721.sol";
-import "./IERC721Enumerable.sol";
-import "./IERC721Metadata.sol";
-
-/**
- * @title ERC-721 Non-Fungible Token Standard, full implementation interface
- * @dev See https://eips.ethereum.org/EIPS/eip-721
- */
-abstract contract IERC721Full is IERC721, IERC721Enumerable, IERC721Metadata { }

+ 7 - 24
contracts/token/ERC721/README.adoc

@@ -2,54 +2,37 @@
 
 This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC721 Non-Fungible Token Standard].
 
-TIP: For a walkthrough on how to create an ERC721 token read our xref:ROOT:tokens.adoc#ERC721[ERC721 guide].
+TIP: For a walkthrough on how to create an ERC721 token read our xref:ROOT:erc721.adoc[ERC721 guide].
 
-The EIP consists of three interfaces, found here as {IERC721}, {IERC721Metadata}, and {IERC721Enumerable}. Only the first one is required in a contract to be ERC721 compliant.
-
-Each interface is implemented separately in {ERC721}, {ERC721Metadata}, and {ERC721Enumerable}. You can choose the subset of functionality you would like to support in your token by combining the
-desired subset through inheritance.
-
-The fully featured token implementing all three interfaces is prepackaged as {ERC721Full}.
+The EIP consists of three interfaces, found here as {IERC721}, {IERC721Metadata}, and {IERC721Enumerable}. Only the first one is required in a contract to be ERC721 compliant. However, all three are implemented in {ERC721}.
 
 Additionally, {IERC721Receiver} can be used to prevent tokens from becoming forever locked in contracts. Imagine sending an in-game item to an exchange address that can't send it back!. When using <<IERC721-safeTransferFrom,`safeTransferFrom`>>, the token contract checks to see that the receiver is an {IERC721Receiver}, which implies that it knows how to handle {ERC721} tokens. If you're writing a contract that needs to receive {ERC721} tokens, you'll want to include this interface.
 
 Finally, some custom extensions are also included:
 
-* {ERC721Mintable} — like the ERC20 version, this allows certain addresses to mint new tokens
-* {ERC721Pausable} — like the ERC20 version, this allows addresses to freeze transfers of tokens
+Additionally there are multiple custom extensions, including:
 
-NOTE: This page is incomplete. We're working to improve it for the next release. Stay tuned!
+* designation of addresses that can pause token transfers for all users ({ERC721Pausable}).
+* destruction of own tokens ({ERC721Burnable}).
 
 == Core
 
 {{IERC721}}
 
-{{ERC721}}
-
 {{IERC721Metadata}}
 
-{{ERC721Metadata}}
-
-{{ERC721Enumerable}}
-
 {{IERC721Enumerable}}
 
-{{IERC721Full}}
-
-{{ERC721Full}}
+{{ERC721}}
 
 {{IERC721Receiver}}
 
 == Extensions
 
-{{ERC721Mintable}}
-
-{{ERC721MetadataMintable}}
+{{ERC721Pausable}}
 
 {{ERC721Burnable}}
 
-{{ERC721Pausable}}
-
 == Convenience
 
 {{ERC721Holder}}

+ 7 - 7
contracts/token/ERC777/ERC777.sol

@@ -28,7 +28,7 @@ contract ERC777 is Context, IERC777, IERC20 {
     using SafeMath for uint256;
     using Address for address;
 
-    IERC1820Registry constant internal ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
+    IERC1820Registry constant internal _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
 
     mapping(address => uint256) private _balances;
 
@@ -41,11 +41,11 @@ contract ERC777 is Context, IERC777, IERC20 {
     // See https://github.com/ethereum/solidity/issues/4024.
 
     // keccak256("ERC777TokensSender")
-    bytes32 constant private TOKENS_SENDER_INTERFACE_HASH =
+    bytes32 constant private _TOKENS_SENDER_INTERFACE_HASH =
         0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;
 
     // keccak256("ERC777TokensRecipient")
-    bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH =
+    bytes32 constant private _TOKENS_RECIPIENT_INTERFACE_HASH =
         0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;
 
     // This isn't ever read from - it's only used to respond to the defaultOperators query.
@@ -78,8 +78,8 @@ contract ERC777 is Context, IERC777, IERC20 {
         }
 
         // register interfaces
-        ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
-        ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
+        _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
+        _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
     }
 
     /**
@@ -440,7 +440,7 @@ contract ERC777 is Context, IERC777, IERC20 {
     )
         private
     {
-        address implementer = ERC1820_REGISTRY.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
+        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH);
         if (implementer != address(0)) {
             IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
         }
@@ -468,7 +468,7 @@ contract ERC777 is Context, IERC777, IERC20 {
     )
         private
     {
-        address implementer = ERC1820_REGISTRY.getInterfaceImplementer(to, TOKENS_RECIPIENT_INTERFACE_HASH);
+        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
         if (implementer != address(0)) {
             IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
         } else if (requireReceptionAck) {

+ 1 - 1
contracts/token/ERC777/README.adoc

@@ -1,7 +1,7 @@
 = ERC 777
 This set of interfaces and contracts are all related to the [ERC777 token standard](https://eips.ethereum.org/EIPS/eip-777).
 
-TIP: For an overview of ERC777 tokens and a walkthrough on how to create a token contract read our xref:ROOT:tokens.adoc#ERC777[ERC777 guide].
+TIP: For an overview of ERC777 tokens and a walkthrough on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide].
 
 The token behavior itself is implemented in the core contracts: {IERC777}, {ERC777}.
 

+ 17 - 6
contracts/utils/Create2.sol

@@ -12,22 +12,33 @@ pragma solidity ^0.6.0;
 library Create2 {
     /**
      * @dev Deploys a contract using `CREATE2`. The address where the contract
-     * will be deployed can be known in advance via {computeAddress}. Note that
-     * a contract cannot be deployed twice using the same salt.
+     * will be deployed can be known in advance via {computeAddress}.
+     *
+     * The bytecode for a contract can be obtained from Solidity with
+     * `type(contractName).creationCode`.
+     *
+     * Requirements:
+     *
+     * - `bytecode` must not be empty.
+     * - `salt` must have not been used for `bytecode` already.
+     * - the factory must have a balance of at least `amount`.
+     * - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
      */
-    function deploy(bytes32 salt, bytes memory bytecode) internal returns (address) {
+    function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address) {
         address addr;
+        require(address(this).balance >= amount, "Create2: insufficient balance");
+        require(bytecode.length != 0, "Create2: bytecode length is zero");
         // solhint-disable-next-line no-inline-assembly
         assembly {
-            addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
+            addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
         }
         require(addr != address(0), "Create2: Failed on deploy");
         return addr;
     }
 
     /**
-     * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the `bytecodeHash`
-     * or `salt` will result in a new destination address.
+     * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
+     * `bytecodeHash` or `salt` will result in a new destination address.
      */
     function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
         return computeAddress(salt, bytecodeHash, address(this));

+ 211 - 0
contracts/utils/EnumerableMap.sol

@@ -0,0 +1,211 @@
+pragma solidity ^0.6.0;
+
+library EnumerableMap {
+    // To implement this library for multiple types with as little code
+    // repetition as possible, we write it in terms of a generic Map type with
+    // bytes32 keys and values.
+    // The Map implementation uses private functions, and user-facing
+    // implementations (such as Uint256ToAddressMap) are just wrappers around
+    // the underlying Map.
+    // This means that we can only create new EnumerableMaps for types that fit
+    // in bytes32.
+
+    struct MapEntry {
+        bytes32 _key;
+        bytes32 _value;
+    }
+
+    struct Map {
+        // Storage of map keys and values
+        MapEntry[] _entries;
+
+        // Position of the entry defined by a key in the `entries` array, plus 1
+        // because index 0 means a key is not in the map.
+        mapping (bytes32 => uint256) _indexes;
+    }
+
+    /**
+     * @dev Adds a key-value pair to a map, or updates the value for an existing
+     * key. O(1).
+     *
+     * Returns true if the key was added to the map, that is if it was not
+     * already present.
+     */
+    function _set(Map storage map, bytes32 key, bytes32 value) private returns (bool) {
+        // We read and store the key's index to prevent multiple reads from the same storage slot
+        uint256 keyIndex = map._indexes[key];
+
+        if (keyIndex == 0) { // Equivalent to !contains(map, key)
+            map._entries.push(MapEntry({ _key: key, _value: value }));
+            // The entry is stored at length-1, but we add 1 to all indexes
+            // and use 0 as a sentinel value
+            map._indexes[key] = map._entries.length;
+            return true;
+        } else {
+            map._entries[keyIndex - 1]._value = value;
+            return false;
+        }
+    }
+
+    /**
+     * @dev Removes a key-value pair from a map. O(1).
+     *
+     * Returns true if the key was removed from the map, that is if it was present.
+     */
+    function _remove(Map storage map, bytes32 key) private returns (bool) {
+        // We read and store the key's index to prevent multiple reads from the same storage slot
+        uint256 keyIndex = map._indexes[key];
+
+        if (keyIndex != 0) { // Equivalent to contains(map, key)
+            // To delete a key-value pair from the _entries array in O(1), we swap the entry to delete with the last one
+            // in the array, and then remove the last entry (sometimes called as 'swap and pop').
+            // This modifies the order of the array, as noted in {at}.
+
+            uint256 toDeleteIndex = keyIndex - 1;
+            uint256 lastIndex = map._entries.length - 1;
+
+            // When the entry to delete is the last one, the swap operation is unnecessary. However, since this occurs
+            // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement.
+
+            MapEntry storage lastEntry = map._entries[lastIndex];
+
+            // Move the last entry to the index where the entry to delete is
+            map._entries[toDeleteIndex] = lastEntry;
+            // Update the index for the moved entry
+            map._indexes[lastEntry._key] = toDeleteIndex + 1; // All indexes are 1-based
+
+            // Delete the slot where the moved entry was stored
+            map._entries.pop();
+
+            // Delete the index for the deleted slot
+            delete map._indexes[key];
+
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * @dev Returns true if the key is in the map. O(1).
+     */
+    function _contains(Map storage map, bytes32 key) private view returns (bool) {
+        return map._indexes[key] != 0;
+    }
+
+    /**
+     * @dev Returns the number of key-value pairs in the map. O(1).
+     */
+    function _length(Map storage map) private view returns (uint256) {
+        return map._entries.length;
+    }
+
+   /**
+    * @dev Returns the key-value pair stored at position `index` in the map. O(1).
+    *
+    * Note that there are no guarantees on the ordering of entries inside the
+    * array, and it may change when more entries are added or removed.
+    *
+    * Requirements:
+    *
+    * - `index` must be strictly less than {length}.
+    */
+    function _at(Map storage map, uint256 index) private view returns (bytes32, bytes32) {
+        require(map._entries.length > index, "EnumerableMap: index out of bounds");
+
+        MapEntry storage entry = map._entries[index];
+        return (entry._key, entry._value);
+    }
+
+    /**
+     * @dev Returns the value associated with `key`.  O(1).
+     *
+     * Requirements:
+     *
+     * - `key` must be in the map.
+     */
+    function _get(Map storage map, bytes32 key) private view returns (bytes32) {
+        return _get(map, key, "EnumerableMap: nonexistent key");
+    }
+
+    /**
+     * @dev Same as {_get}, with a custom error message when `key` is not in the map.
+     */
+    function _get(Map storage map, bytes32 key, string memory errorMessage) private view returns (bytes32) {
+        uint256 keyIndex = map._indexes[key];
+        require(keyIndex != 0, errorMessage); // Equivalent to contains(map, key)
+        return map._entries[keyIndex - 1]._value; // All indexes are 1-based
+    }
+
+    // UintToAddressMap
+
+    struct UintToAddressMap {
+        Map _inner;
+    }
+
+    /**
+     * @dev Adds a key-value pair to a map, or updates the value for an existing
+     * key. O(1).
+     *
+     * Returns true if the key was added to the map, that is if it was not
+     * already present.
+     */
+    function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) {
+        return _set(map._inner, bytes32(key), bytes32(uint256(value)));
+    }
+
+    /**
+     * @dev Removes a value from a set. O(1).
+     *
+     * Returns true if the key was removed from the map, that is if it was present.
+     */
+    function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) {
+        return _remove(map._inner, bytes32(key));
+    }
+
+    /**
+     * @dev Returns true if the key is in the map. O(1).
+     */
+    function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) {
+        return _contains(map._inner, bytes32(key));
+    }
+
+    /**
+     * @dev Returns the number of elements in the map. O(1).
+     */
+    function length(UintToAddressMap storage map) internal view returns (uint256) {
+        return _length(map._inner);
+    }
+
+   /**
+    * @dev Returns the element stored at position `index` in the set. O(1).
+    * Note that there are no guarantees on the ordering of values inside the
+    * array, and it may change when more values are added or removed.
+    *
+    * Requirements:
+    *
+    * - `index` must be strictly less than {length}.
+    */
+    function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) {
+        (bytes32 key, bytes32 value) = _at(map._inner, index);
+        return (uint256(key), address(uint256(value)));
+    }
+
+    /**
+     * @dev Returns the value associated with `key`.  O(1).
+     *
+     * Requirements:
+     *
+     * - `key` must be in the map.
+     */
+    function get(UintToAddressMap storage map, uint256 key) internal view returns (address) {
+        return address(uint256(_get(map._inner, bytes32(key))));
+    }
+
+    /**
+     * @dev Same as {get}, with a custom error message when `key` is not in the map.
+     */
+    function get(UintToAddressMap storage map, uint256 key, string memory errorMessage) internal view returns (address) {
+        return address(uint256(_get(map._inner, bytes32(key), errorMessage)));
+    }
+}

+ 166 - 71
contracts/utils/EnumerableSet.sol

@@ -18,27 +18,36 @@ pragma solidity ^0.6.0;
  * @author Alberto Cuesta Cañada
  */
 library EnumerableSet {
+    // To implement this library for multiple types with as little code
+    // repetition as possible, we write it in terms of a generic Set type with
+    // bytes32 values.
+    // The Set implementation uses private functions, and user-facing
+    // implementations (such as AddressSet) are just wrappers around the
+    // underlying Set.
+    // This means that we can only create new EnumerableSets for types that fit
+    // in bytes32.
+
+    struct Set {
+        // Storage of set values
+        bytes32[] _values;
 
-    struct AddressSet {
         // Position of the value in the `values` array, plus 1 because index 0
         // means a value is not in the set.
-        mapping (address => uint256) index;
-        address[] values;
+        mapping (bytes32 => uint256) _indexes;
     }
 
     /**
      * @dev Add a value to a set. O(1).
-     * Returns false if the value was already in the set.
+     *
+     * Returns true if the value was added to the set, that is if it was not
+     * already present.
      */
-    function add(AddressSet storage set, address value)
-        internal
-        returns (bool)
-    {
-        if (!contains(set, value)) {
-            set.values.push(value);
-            // The element is stored at length-1, but we add 1 to all indexes
+    function _add(Set storage set, bytes32 value) private returns (bool) {
+        if (!_contains(set, value)) {
+            set._values.push(value);
+            // The value is stored at length-1, but we add 1 to all indexes
             // and use 0 as a sentinel value
-            set.index[value] = set.values.length;
+            set._indexes[value] = set._values.length;
             return true;
         } else {
             return false;
@@ -47,31 +56,37 @@ library EnumerableSet {
 
     /**
      * @dev Removes a value from a set. O(1).
-     * Returns false if the value was not present in the set.
+     *
+     * Returns true if the value was removed from the set, that is if it was
+     * present.
      */
-    function remove(AddressSet storage set, address value)
-        internal
-        returns (bool)
-    {
-        if (contains(set, value)){
-            uint256 toDeleteIndex = set.index[value] - 1;
-            uint256 lastIndex = set.values.length - 1;
-
-            // If the element we're deleting is the last one, we can just remove it without doing a swap
-            if (lastIndex != toDeleteIndex) {
-                address lastValue = set.values[lastIndex];
-
-                // Move the last value to the index where the deleted value is
-                set.values[toDeleteIndex] = lastValue;
-                // Update the index for the moved value
-                set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based
-            }
-
-            // Delete the index entry for the deleted value
-            delete set.index[value];
-
-            // Delete the old entry for the moved value
-            set.values.pop();
+    function _remove(Set storage set, bytes32 value) private returns (bool) {
+        // We read and store the value's index to prevent multiple reads from the same storage slot
+        uint256 valueIndex = set._indexes[value];
+
+        if (valueIndex != 0) { // Equivalent to contains(set, value)
+            // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
+            // the array, and then remove the last element (sometimes called as 'swap and pop').
+            // This modifies the order of the array, as noted in {at}.
+
+            uint256 toDeleteIndex = valueIndex - 1;
+            uint256 lastIndex = set._values.length - 1;
+
+            // When the value to delete is the last one, the swap operation is unnecessary. However, since this occurs
+            // so rarely, we still do the swap anyway to avoid the gas cost of adding an 'if' statement.
+
+            bytes32 lastvalue = set._values[lastIndex];
+
+            // Move the last value to the index where the value to delete is
+            set._values[toDeleteIndex] = lastvalue;
+            // Update the index for the moved value
+            set._indexes[lastvalue] = toDeleteIndex + 1; // All indexes are 1-based
+
+            // Delete the slot where the moved value was stored
+            set._values.pop();
+
+            // Delete the index for the deleted slot
+            delete set._indexes[value];
 
             return true;
         } else {
@@ -82,46 +97,130 @@ library EnumerableSet {
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
-    function contains(AddressSet storage set, address value)
-        internal
-        view
-        returns (bool)
-    {
-        return set.index[value] != 0;
+    function _contains(Set storage set, bytes32 value) private view returns (bool) {
+        return set._indexes[value] != 0;
+    }
+
+    /**
+     * @dev Returns the number of values on the set. O(1).
+     */
+    function _length(Set storage set) private view returns (uint256) {
+        return set._values.length;
+    }
+
+   /**
+    * @dev Returns the value stored at position `index` in the set. O(1).
+    *
+    * Note that there are no guarantees on the ordering of values inside the
+    * array, and it may change when more values are added or removed.
+    *
+    * Requirements:
+    *
+    * - `index` must be strictly less than {length}.
+    */
+    function _at(Set storage set, uint256 index) private view returns (bytes32) {
+        require(set._values.length > index, "EnumerableSet: index out of bounds");
+        return set._values[index];
+    }
+
+    // AddressSet
+
+    struct AddressSet {
+        Set _inner;
     }
 
     /**
-     * @dev Returns an array with all values in the set. O(N).
-     * Note that there are no guarantees on the ordering of values inside the
-     * array, and it may change when more values are added or removed.
+     * @dev Add a value to a set. O(1).
+     *
+     * Returns true if the value was added to the set, that is if it was not
+     * already present.
+     */
+    function add(AddressSet storage set, address value) internal returns (bool) {
+        return _add(set._inner, bytes32(uint256(value)));
+    }
 
-     * WARNING: This function may run out of gas on large sets: use {length} and
-     * {get} instead in these cases.
+    /**
+     * @dev Removes a value from a set. O(1).
+     *
+     * Returns true if the value was removed from the set, that is if it was
+     * present.
      */
-    function enumerate(AddressSet storage set)
-        internal
-        view
-        returns (address[] memory)
-    {
-        address[] memory output = new address[](set.values.length);
-        for (uint256 i; i < set.values.length; i++){
-            output[i] = set.values[i];
-        }
-        return output;
+    function remove(AddressSet storage set, address value) internal returns (bool) {
+        return _remove(set._inner, bytes32(uint256(value)));
+    }
+
+    /**
+     * @dev Returns true if the value is in the set. O(1).
+     */
+    function contains(AddressSet storage set, address value) internal view returns (bool) {
+        return _contains(set._inner, bytes32(uint256(value)));
+    }
+
+    /**
+     * @dev Returns the number of values in the set. O(1).
+     */
+    function length(AddressSet storage set) internal view returns (uint256) {
+        return _length(set._inner);
+    }
+
+   /**
+    * @dev Returns the value stored at position `index` in the set. O(1).
+    *
+    * Note that there are no guarantees on the ordering of values inside the
+    * array, and it may change when more values are added or removed.
+    *
+    * Requirements:
+    *
+    * - `index` must be strictly less than {length}.
+    */
+    function at(AddressSet storage set, uint256 index) internal view returns (address) {
+        return address(uint256(_at(set._inner, index)));
+    }
+
+
+    // UintSet
+
+    struct UintSet {
+        Set _inner;
+    }
+
+    /**
+     * @dev Add a value to a set. O(1).
+     *
+     * Returns true if the value was added to the set, that is if it was not
+     * already present.
+     */
+    function add(UintSet storage set, uint256 value) internal returns (bool) {
+        return _add(set._inner, bytes32(value));
+    }
+
+    /**
+     * @dev Removes a value from a set. O(1).
+     *
+     * Returns true if the value was removed from the set, that is if it was
+     * present.
+     */
+    function remove(UintSet storage set, uint256 value) internal returns (bool) {
+        return _remove(set._inner, bytes32(value));
     }
 
     /**
-     * @dev Returns the number of elements on the set. O(1).
+     * @dev Returns true if the value is in the set. O(1).
      */
-    function length(AddressSet storage set)
-        internal
-        view
-        returns (uint256)
-    {
-        return set.values.length;
+    function contains(UintSet storage set, uint256 value) internal view returns (bool) {
+        return _contains(set._inner, bytes32(value));
     }
 
-   /** @dev Returns the element stored at position `index` in the set. O(1).
+    /**
+     * @dev Returns the number of values on the set. O(1).
+     */
+    function length(UintSet storage set) internal view returns (uint256) {
+        return _length(set._inner);
+    }
+
+   /**
+    * @dev Returns the value stored at position `index` in the set. O(1).
+    *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
@@ -129,11 +228,7 @@ library EnumerableSet {
     *
     * - `index` must be strictly less than {length}.
     */
-    function get(AddressSet storage set, uint256 index)
-        internal
-        view
-        returns (address)
-    {
-        return set.values[index];
+    function at(UintSet storage set, uint256 index) internal view returns (uint256) {
+        return uint256(_at(set._inner, index));
     }
 }

+ 2 - 0
contracts/utils/README.adoc

@@ -16,6 +16,8 @@ Miscellaneous contracts containing utility functions, often related to working w
 
 {{EnumerableSet}}
 
+{{EnumerableMap}}
+
 {{Create2}}
 
 {{ReentrancyGuard}}

+ 24 - 0
contracts/utils/SafeCast.sol

@@ -92,4 +92,28 @@ library SafeCast {
         require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
         return uint8(value);
     }
+
+    /**
+     * @dev Converts a signed int256 into an unsigned uint256.
+     *
+     * Requirements:
+     *
+     * - input must be greater than or equal to 0.
+     */
+    function toUint256(int256 value) internal pure returns (uint256) {
+        require(value >= 0, "SafeCast: value must be positive");
+        return uint256(value);
+    }
+
+    /**
+     * @dev Converts an unsigned uint256 into a signed int256.
+     *
+     * Requirements:
+     *
+     * - input must be less than or equal to maxInt256.
+     */
+    function toInt256(uint256 value) internal pure returns (int256) {
+        require(value < 2**255, "SafeCast: value doesn't fit in an int256");
+        return int256(value);
+    }
 }

+ 4 - 4
docs/contract.hbs

@@ -1,5 +1,5 @@
 {{~#*inline "typed-variable-array"~}}
-{{#each .}}[.var-type\]#{{typeName}}#{{#if name}} [.var-name\]#{{name}}#{{/if}}{{#unless @last}}, {{/unless}}{{/each}}
+{{#each .}}[.var-type]#{{typeName}}#{{#if name}} [.var-name]#{{name}}#{{/if}}{{#unless @last}}, {{/unless}}{{/each}}
 {{~/inline~}}
 
 {{#each linkable}}
@@ -66,7 +66,7 @@
 {{#each ownModifiers}}
 [.contract-item]
 [[{{anchor}}]]
-==== `pass:normal[{{name}}({{> typed-variable-array args}})]` [.item-kind]#modifier#
+==== `{{name}}({{> typed-variable-array args}})` [.item-kind]#modifier#
 
 {{natspec.devdoc}}
 
@@ -75,7 +75,7 @@
 {{#each ownFunctions}}
 [.contract-item]
 [[{{anchor}}]]
-==== `pass:normal[{{name}}({{> typed-variable-array args}}){{#if outputs}} → {{> typed-variable-array outputs}}{{/if}}]` [.item-kind]#{{visibility}}#
+==== `{{name}}({{> typed-variable-array args}}){{#if outputs}} → {{> typed-variable-array outputs}}{{/if}}` [.item-kind]#{{visibility}}#
 
 {{natspec.devdoc}}
 
@@ -84,7 +84,7 @@
 {{#each ownEvents}}
 [.contract-item]
 [[{{anchor}}]]
-==== `pass:normal[{{name}}({{> typed-variable-array args}})]` [.item-kind]#event#
+==== `{{name}}({{> typed-variable-array args}})` [.item-kind]#event#
 
 {{natspec.devdoc}}
 

+ 8 - 8
docs/modules/ROOT/pages/access-control.adoc

@@ -11,7 +11,7 @@ OpenZeppelin provides xref:api:access.adoc#Ownable[`Ownable`] for implementing o
 
 [source,solidity]
 ----
-pragma solidity ^0.5.0;
+pragma solidity ^0.6.0;
 
 import "@openzeppelin/contracts/access/Ownable.sol";
 
@@ -69,7 +69,7 @@ contract MyToken is ERC20, AccessControl {
 
     constructor(address minter) public {
         // Grant the minter role to a specified account
-        _grantRole(MINTER_ROLE, minter);
+        _setupRole(MINTER_ROLE, minter);
     }
 
     function mint(address to, uint256 amount) public {
@@ -98,8 +98,8 @@ contract MyToken is ERC20, AccessControl {
     bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
 
     constructor(address minter, address burner) public {
-        _grantRole(MINTER_ROLE, minter);
-        _grantRole(BURNER_ROLE, burner);
+        _setupRole(MINTER_ROLE, minter);
+        _setupRole(BURNER_ROLE, burner);
     }
 
     function mint(address to, uint256 amount) public {
@@ -119,11 +119,11 @@ So clean! By splitting concerns this way, more granular levels of permission may
 [[granting-and-revoking]]
 === Granting and Revoking Roles
 
-The ERC20 token example above uses `\_grantRole`, an `internal` function that is useful when programmatically asigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts?
+The ERC20 token example above uses `\_setupRole`, an `internal` function that is useful when programmatically asigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts?
 
 By default, **accounts with a role cannot grant it or revoke it from other accounts**: all having a role does is making the `hasRole` check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_.
 
-Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` `external` functions. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role's admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it.
+Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` functions. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role's admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it.
 
 This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `\_setRoleAdmin` is used to select a new admin role.
 
@@ -140,10 +140,10 @@ contract MyToken is ERC20, AccessControl {
     bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
     bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
 
-    constructor() public {
+    constructor() ERC20("MyToken", "TKN") public {
         // Grant the contract deployer the default admin role: it will be able
         // to grant and revoke any roles
-        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
+        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
     }
 
     function mint(address to, uint256 amount) public {

+ 6 - 4
docs/modules/ROOT/pages/erc20-supply.adoc

@@ -55,7 +55,7 @@ As we can see, `_mint` makes it super easy to do this correctly.
 [[modularizing-the-mechanism]]
 == Modularizing the Mechanism
 
-There is one supply mechanism already included in Contracts: xref:api:token/ERC20.adoc#ERC20Mintable[`ERC20Mintable`]. This is a generic mechanism in which a set of accounts is assigned the `minter` role, granting them the permission to call a xref:api:token/ERC20.adoc#ERC20Mintable-mint-address-uint256-[`mint`] function, an external version of `_mint`.
+There is one supply mechanism already included in Contracts: `ERC20DeployReady`. This is a generic mechanism in which a set of accounts is assigned the `minter` role, granting them the permission to call a `mint` function, an external version of `_mint`.
 
 This can be used for centralized minting, where an externally owned account (i.e. someone with a pair of cryptographic keys) decides how much supply to create and to whom. There are very legitimate use cases for this mechanism, such as https://medium.com/reserve-currency/why-another-stablecoin-866f774afede#3aea[traditional asset-backed stablecoins].
 
@@ -64,9 +64,9 @@ The accounts with the minter role don't need to be externally owned, though, and
 [source,solidity]
 ----
 contract MinerRewardMinter {
-    ERC20Mintable _token;
+    ERC20DeployReady _token;
 
-    constructor(ERC20Mintable token) public {
+    constructor(ERC20DeployReady token) public {
         _token = token;
     }
 
@@ -76,7 +76,9 @@ contract MinerRewardMinter {
 }
 ----
 
-This contract, when initialized with an `ERC20Mintable` instance, will result in exactly the same behavior implemented in the previous section. What is interesting about using `ERC20Mintable` is that we can easily combine multiple supply mechanisms by assigning the role to multiple contracts, and moreover that we can do this dynamically.
+This contract, when initialized with an `ERC20DeployReady` instance, will result in exactly the same behavior implemented in the previous section. What is interesting about using `ERC20DeployReady` is that we can easily combine multiple supply mechanisms by assigning the role to multiple contracts, and moreover that we can do this dynamically.
+
+TIP: To learn more about roles and permissioned systems, head to our xref:access-control.adoc[Access Control guide].
 
 [[automating-the-reward]]
 == Automating the Reward

+ 7 - 6
docs/modules/ROOT/pages/erc20.adoc

@@ -13,19 +13,18 @@ Here's what our GLD token might look like.
 
 [source,solidity]
 ----
-pragma solidity ^0.5.0;
+pragma solidity ^0.6.0;
 
 import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
 
-contract GLDToken is ERC20, ERC20Detailed {
-    constructor(uint256 initialSupply) ERC20Detailed("Gold", "GLD", 18) public {
+contract GLDToken is ERC20 {
+    constructor(uint256 initialSupply) ERC20("Gold", "GLD") public {
         _mint(msg.sender, initialSupply);
     }
 }
 ----
 
-Our contracts are often used via https://solidity.readthedocs.io/en/latest/contracts.html#inheritance[inheritance], and here we're reusing xref:api:token/ERC20.adoc#erc20[`ERC20`] for the basic standard implementation and xref:api:token/ERC20.adoc#ERC20Detailed[`ERC20Detailed`] to get the xref:api:token/ERC20.adoc#ERC20Detailed-name--[`name`], xref:api:token/ERC20.adoc#ERC20Detailed-symbol--[`symbol`], and xref:api:token/ERC20.adoc#ERC20Detailed-decimals--[`decimals`] properties. Additionally, we're creating an `initialSupply` of tokens, which will be assigned to the address that deploys the contract.
+Our contracts are often used via https://solidity.readthedocs.io/en/latest/contracts.html#inheritance[inheritance], and here we're reusing xref:api:token/ERC20.adoc#erc20[`ERC20`] for both the basic standard implementation and the xref:api:token/ERC20.adoc#ERC20-name--[`name`], xref:api:token/ERC20.adoc#ERC20-symbol--[`symbol`], and xref:api:token/ERC20.adoc#ERC20-decimals--[`decimals`] optional extensions. Additionally, we're creating an `initialSupply` of tokens, which will be assigned to the address that deploys the contract.
 
 TIP: For a more complete discussion of ERC20 supply mechanisms, see xref:erc20-supply.adoc[Creating ERC20 Supply].
 
@@ -53,7 +52,7 @@ We can also xref:api:token/ERC20.adoc#IERC20-transfer-address-uint256-[transfer]
 
 Often, you'll want to be able to divide your tokens into arbitrary amounts: say, if you own `5 GLD`, you may want to send `1.5 GLD` to a friend, and keep `3.5 GLD` to yourself. Unfortunately, Solidity and the EVM do not support this behavior: only integer (whole) numbers can be used, which poses an issue. You may send `1` or `2` tokens, but not `1.5`.
 
-To work around this, xref:api:token/ERC20.adoc#ERC20Detailed[`ERC20Detailed`] provides a xref:api:token/ERC20.adoc#ERC20Detailed-decimals--[`decimals`] field, which is used to specify how many decimal places a token has. To be able to transfer `1.5 GLD`, `decimals` must be at least `1`, since that number has a single decimal place.
+To work around this, xref:api:token/ERC20.adoc#ERC20[`ERC20`] provides a xref:api:token/ERC20.adoc#ERC20-decimals--[`decimals`] field, which is used to specify how many decimal places a token has. To be able to transfer `1.5 GLD`, `decimals` must be at least `1`, since that number has a single decimal place.
 
 How can this be achieved? It's actually very simple: a token contract can use larger integer values, so that a balance of `50` will represent `5 GLD`, a transfer of `15` will correspond to `1.5 GLD` being sent, and so on.
 
@@ -61,6 +60,8 @@ It is important to understand that `decimals` is _only used for display purposes
 
 You'll probably want to use a `decimals` value of `18`, just like Ether and most ERC20 token contracts in use, unless you have a very special reason not to. When minting tokens or transferring them around, you will be actually sending the number `num GLD * 10^decimals`.
 
+NOTE: By default, `ERC20` uses a value of `18` for `decimals`. To use a different value, you will need to call xref:api:token/ERC20.adoc#ERC20-_setupDecimals-uint8-[_setupDecimals] in your constructor.
+
 So if you want to send `5` tokens using a token contract with 18 decimals, the the method to call will actually be:
 
 ```solidity

+ 5 - 5
docs/modules/ROOT/pages/erc721.adoc

@@ -12,16 +12,16 @@ Here's what a contract for tokenized items might look like:
 
 [source,solidity]
 ----
-pragma solidity ^0.5.0;
+pragma solidity ^0.6.0;
 
-import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
+import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
 import "@openzeppelin/contracts/utils/Counters.sol";
 
-contract GameItem is ERC721Full {
+contract GameItem is ERC721 {
     using Counters for Counters.Counter;
     Counters.Counter private _tokenIds;
 
-    constructor() ERC721Full("GameItem", "ITM") public {
+    constructor() ERC721("GameItem", "ITM") public {
     }
 
     function awardItem(address player, string memory tokenURI) public returns (uint256) {
@@ -36,7 +36,7 @@ contract GameItem is ERC721Full {
 }
 ----
 
-The xref:api:token/ERC721.adoc#ERC721Full[`ERC721Full`] contract includes all standard extensions, and is probably the one you want to use. In particular, it includes xref:api:token/ERC721.adoc#ERC721Metadata[`ERC721Metadata`], which provides the xref:api:token/ERC721.adoc#ERC721Metadata-_setTokenURI-uint256-string-[`_setTokenURI`] method we use to store an item's metadata.
+The xref:api:token/ERC721.adoc#ERC721[`ERC721`] contract includes all standard extensions (xref:api:token/ERC721.adoc#IERC721Metadata[`IERC721Metadata`] and xref:api:token/ERC721.adoc#IERC721Enumerable[`IERC721Enumerable`]). That's where the xref:api:token/ERC721.adoc#ERC721-_setTokenURI-uint256-string-[`_setTokenURI`] method comes from: we use it to store an item's metadata.
 
 Also note that, unlike ERC20, ERC721 lacks a `decimals` field, since each token is distinct and cannot be partitioned.
 

+ 8 - 3
docs/modules/ROOT/pages/gsn-strategies.adoc

@@ -102,17 +102,22 @@ NOTE: Always use `_preRelayedCall` and `_postRelayedCall` functions.  Internal `
 
 === How to Use `GSNRecipientERC20Fee`
 
-Your GSN recipient contract needs to inherit from `GSNRecipientERC20Fee` along with appropriate xref:access-control.adoc[access control] (for token minting), set the token details in the constructor of `GSNRecipientERC20Fee` and create a public `mint` function suitably protected by your chosen access control as per the following sample code (which uses the xref:api:access.adoc#MinterRole[MinterRole]):
+Your GSN recipient contract needs to inherit from `GSNRecipientERC20Fee` along with appropriate xref:access-control.adoc[access control] (for token minting), set the token details in the constructor of `GSNRecipientERC20Fee` and create a public `mint` function suitably protected by your chosen access control as per the following sample code (which uses xref:api:access.adoc#AccessControl[`AccessControl`]):
 
 [source,solidity]
 ----
 import "@openzeppelin/contracts/GSN/GSNRecipientERC20Fee";
+import "@openzeppelin/contracts/access/AccessControl";
+
+contract MyContract is GSNRecipientERC20Fee, AccessControl {
+    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
 
-contract MyContract is GSNRecipientERC20Fee, MinterRole {
     constructor() public GSNRecipientERC20Fee("FeeToken", "FEE") {
+        _setupRole(MINTER_ROLE, _msgSender());
     }
 
-    function mint(address account, uint256 amount) public onlyMinter {
+    function mint(address account, uint256 amount) public {
+        require(hasRole(MINTER_ROLE, _msgSender()));
         _mint(account, amount);
     }
 }

+ 4 - 5
docs/modules/ROOT/pages/index.adoc

@@ -26,13 +26,12 @@ Once installed, you can use the contracts in the library by importing them:
 
 [source,solidity]
 ----
-pragma solidity ^0.5.0;
+pragma solidity ^0.6.0;
 
-import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
-import "@openzeppelin/contracts/token/ERC721/ERC721Mintable.sol";
+import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
 
-contract MyNFT is ERC721Full, ERC721Mintable {
-    constructor() ERC721Full("MyNFT", "MNFT") public {
+contract MyNFT is ERC721 {
+    constructor() ERC721("MyNFT", "MNFT") public {
     }
 }
 ----

+ 2 - 2
docs/modules/ROOT/pages/releases-stability.adoc

@@ -28,7 +28,7 @@ Every several months a new major release may come out. These are not scheduled,
 
 On the https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v2.0.0[OpenZeppelin 2.0 release], we committed ourselves to keeping a stable API. We aim to more precisely define what we understand by _stable_ and _API_ here, so users of the library can understand these guarantees and be confident their project won't break unexpectedly.
 
-In a nutshell, the API being stable means _if your project is working today, it will continue to do so_. New contracts and features will be added in minor releases, but only in a backwards compatible way. The exception to this rule are contracts in the xref:api:drafts.adoc[Drafts] category, which should be considered unstable.
+In a nutshell, the API being stable means _if your project is working today, it will continue to do so_. New contracts and features will be added in minor releases, but only in a backwards compatible way.
 
 [[versioning-scheme]]
 === Versioning Scheme
@@ -54,7 +54,7 @@ Finally, sometimes language limitations will force us to e.g. make `internal` a
 [[libraries]]
 === Libraries
 
-Some of our Solidity libraries use `struct`s to handle internal data that should not be accessed directly (e.g. `Roles`). There's an https://github.com/ethereum/solidity/issues/4637[open issue] in the Solidity repository requesting a language feature to prevent said access, but it looks like it won't be implemented any time soon. Because of this, we will use leading underscores and mark said `struct` s to make it clear to the user that its contents and layout are _not_ part of the API.
+Some of our Solidity libraries use ``struct``s to handle internal data that should not be accessed directly (e.g. `Roles`). There's an https://github.com/ethereum/solidity/issues/4637[open issue] in the Solidity repository requesting a language feature to prevent said access, but it looks like it won't be implemented any time soon. Because of this, we will use leading underscores and mark said `struct` s to make it clear to the user that its contents and layout are _not_ part of the API.
 
 [[events]]
 === Events

+ 2 - 2
docs/modules/ROOT/pages/utilities.adoc

@@ -90,11 +90,11 @@ If you want to Escrow some funds, check out xref:api:payment.adoc#Escrow[`Escrow
 [[collections]]
 == Collections
 
-If you need support for more powerful collections than Solidity's native arrays and mappings, take a look at xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]. It is similar to a mapping in that it stores and removes elements in constant time and doesn't allow for repeated entries, but it also supports _enumeration_, which means you can easily query all elements of the set both on and off-chain.
+If you need support for more powerful collections than Solidity's native arrays and mappings, take a look at xref:api:utils.adoc#EnumerableSet[`EnumerableSet`] and xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]. They are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also supports _enumeration_, which means you can easily query all stored entries both on and off-chain.
 
 [[misc]]
 == Misc
 
 Want to check if an address is a contract? Use xref:api:utils.adoc#Address[`Address`] and xref:api:utils.adoc#Address-isContract-address-[`Address.isContract()`].
 
-Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:drafts.adoc#Counter[`Counter`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:721.adoc[ERC721 guide].
+Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide].

File diff suppressed because it is too large
+ 324 - 235
package-lock.json


+ 6 - 5
package.json

@@ -44,7 +44,7 @@
   },
   "homepage": "https://openzeppelin.com/contracts/",
   "devDependencies": {
-    "@openzeppelin/cli": "^2.7.2",
+    "@openzeppelin/cli": "^2.8.0",
     "@openzeppelin/docs-utils": "^0.1.0",
     "@openzeppelin/gsn-helpers": "^0.2.3",
     "@openzeppelin/gsn-provider": "^0.1.10",
@@ -52,7 +52,7 @@
     "@openzeppelin/test-helpers": "^0.5.5",
     "chai": "^4.2.0",
     "eslint": "^6.5.1",
-    "eslint-config-standard": "^14.1.0",
+    "eslint-config-standard": "^14.1.1",
     "eslint-plugin-import": "^2.20.0",
     "eslint-plugin-mocha-no-only": "^1.1.0",
     "eslint-plugin-node": "^10.0.0",
@@ -61,11 +61,12 @@
     "ethereumjs-util": "^6.2.0",
     "ganache-core-coverage": "https://github.com/OpenZeppelin/ganache-core-coverage/releases/download/2.5.3-coverage/ganache-core-coverage-2.5.3.tgz",
     "lodash.startcase": "^4.4.0",
+    "lodash.zip": "^4.2.0",
     "micromatch": "^4.0.2",
-    "mocha": "^7.1.0",
-    "solhint": "^3.0.0-rc.5",
+    "mocha": "^7.1.1",
+    "solhint": "^3.0.0-rc.6",
     "solidity-coverage": "github:rotcivegaf/solidity-coverage#5875f5b7bc74d447f3312c9c0e9fc7814b482477",
-    "solidity-docgen": "^0.4.0-beta.1"
+    "solidity-docgen": "^0.4.1"
   },
   "dependencies": {}
 }

+ 3 - 1
test/GSN/ERC721GSNRecipientMock.test.js

@@ -11,10 +11,12 @@ const ERC721GSNRecipientMock = contract.fromArtifact('ERC721GSNRecipientMock');
 describe('ERC721GSNRecipient (integration)', function () {
   const [ signer, sender ] = accounts;
 
+  const name = 'Non Fungible Token';
+  const symbol = 'NFT';
   const tokenId = '42';
 
   beforeEach(async function () {
-    this.token = await ERC721GSNRecipientMock.new(signer);
+    this.token = await ERC721GSNRecipientMock.new(name, symbol, signer);
   });
 
   async function testMintToken (token, from, tokenId, options = {}) {

+ 2 - 2
test/GSN/GSNRecipientERC20Fee.test.js

@@ -6,7 +6,7 @@ const gsn = require('@openzeppelin/gsn-helpers');
 const { expect } = require('chai');
 
 const GSNRecipientERC20FeeMock = contract.fromArtifact('GSNRecipientERC20FeeMock');
-const ERC20Detailed = contract.fromArtifact('ERC20Detailed');
+const ERC20 = contract.fromArtifact('ERC20');
 const IRelayHub = contract.fromArtifact('IRelayHub');
 
 describe('GSNRecipientERC20Fee', function () {
@@ -17,7 +17,7 @@ describe('GSNRecipientERC20Fee', function () {
 
   beforeEach(async function () {
     this.recipient = await GSNRecipientERC20FeeMock.new(name, symbol);
-    this.token = await ERC20Detailed.at(await this.recipient.token());
+    this.token = await ERC20.at(await this.recipient.token());
   });
 
   describe('token', function () {

+ 9 - 0
test/access/AccessControl.test.js

@@ -17,6 +17,15 @@ describe('AccessControl', function () {
     this.accessControl = await AccessControlMock.new({ from: admin });
   });
 
+  describe('_setupRole', function () {
+    it('cannot be called outside the constructor', async function () {
+      await expectRevert(
+        this.accessControl.setupRole(OTHER_ROLE, other),
+        'AccessControl: roles cannot be setup after construction'
+      );
+    });
+  });
+
   describe('default admin', function () {
     it('deployer has default admin role', async function () {
       expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.equal(true);

+ 0 - 149
test/behaviors/access/roles/PublicRole.behavior.js

@@ -1,149 +0,0 @@
-const { expectRevert, constants, expectEvent } = require('@openzeppelin/test-helpers');
-const { ZERO_ADDRESS } = constants;
-
-const { expect } = require('chai');
-
-function capitalize (str) {
-  return str.replace(/\b\w/g, l => l.toUpperCase());
-}
-
-// Tests that a role complies with the standard role interface, that is:
-//  * an onlyRole modifier
-//  * an isRole function
-//  * an addRole function, accessible to role havers
-//  * a renounceRole function
-//  * roleAdded and roleRemoved events
-// Both the modifier and an additional internal remove function are tested through a mock contract that exposes them.
-// This mock contract should be stored in this.contract.
-//
-// @param authorized an account that has the role
-// @param otherAuthorized another account that also has the role
-// @param other an account that doesn't have the role, passed inside an array for ergonomics
-// @param rolename a string with the name of the role
-// @param manager undefined for regular roles, or a manager account for managed roles. In these, only the manager
-// account can create and remove new role bearers.
-function shouldBehaveLikePublicRole (authorized, otherAuthorized, [other], rolename, manager) {
-  rolename = capitalize(rolename);
-
-  describe('should behave like public role', function () {
-    beforeEach('check preconditions', async function () {
-      expect(await this.contract[`is${rolename}`](authorized)).to.equal(true);
-      expect(await this.contract[`is${rolename}`](otherAuthorized)).to.equal(true);
-      expect(await this.contract[`is${rolename}`](other)).to.equal(false);
-    });
-
-    if (manager === undefined) { // Managed roles are only assigned by the manager, and none are set at construction
-      it('emits events during construction', async function () {
-        await expectEvent.inConstruction(this.contract, `${rolename}Added`, {
-          account: authorized,
-        });
-      });
-    }
-
-    it('reverts when querying roles for the null account', async function () {
-      await expectRevert(this.contract[`is${rolename}`](ZERO_ADDRESS),
-        'Roles: account is the zero address'
-      );
-    });
-
-    describe('access control', function () {
-      context('from authorized account', function () {
-        const from = authorized;
-
-        it('allows access', async function () {
-          await this.contract[`only${rolename}Mock`]({ from });
-        });
-      });
-
-      context('from unauthorized account', function () {
-        const from = other;
-
-        it('reverts', async function () {
-          await expectRevert(this.contract[`only${rolename}Mock`]({ from }),
-            `${rolename}Role: caller does not have the ${rolename} role`
-          );
-        });
-      });
-    });
-
-    describe('add', function () {
-      const from = manager === undefined ? authorized : manager;
-
-      context(`from ${manager ? 'the manager' : 'a role-haver'} account`, function () {
-        it('adds role to a new account', async function () {
-          await this.contract[`add${rolename}`](other, { from });
-          expect(await this.contract[`is${rolename}`](other)).to.equal(true);
-        });
-
-        it(`emits a ${rolename}Added event`, async function () {
-          const receipt = await this.contract[`add${rolename}`](other, { from });
-          expectEvent(receipt, `${rolename}Added`, { account: other });
-        });
-
-        it('reverts when adding role to an already assigned account', async function () {
-          await expectRevert(this.contract[`add${rolename}`](authorized, { from }),
-            'Roles: account already has role'
-          );
-        });
-
-        it('reverts when adding role to the null account', async function () {
-          await expectRevert(this.contract[`add${rolename}`](ZERO_ADDRESS, { from }),
-            'Roles: account is the zero address'
-          );
-        });
-      });
-    });
-
-    describe('remove', function () {
-      // Non-managed roles have no restrictions on the mocked '_remove' function (exposed via 'remove').
-      const from = manager || other;
-
-      context(`from ${manager ? 'the manager' : 'any'} account`, function () {
-        it('removes role from an already assigned account', async function () {
-          await this.contract[`remove${rolename}`](authorized, { from });
-          expect(await this.contract[`is${rolename}`](authorized)).to.equal(false);
-          expect(await this.contract[`is${rolename}`](otherAuthorized)).to.equal(true);
-        });
-
-        it(`emits a ${rolename}Removed event`, async function () {
-          const receipt = await this.contract[`remove${rolename}`](authorized, { from });
-          expectEvent(receipt, `${rolename}Removed`, { account: authorized });
-        });
-
-        it('reverts when removing from an unassigned account', async function () {
-          await expectRevert(this.contract[`remove${rolename}`](other, { from }),
-            'Roles: account does not have role'
-          );
-        });
-
-        it('reverts when removing role from the null account', async function () {
-          await expectRevert(this.contract[`remove${rolename}`](ZERO_ADDRESS, { from }),
-            'Roles: account is the zero address'
-          );
-        });
-      });
-    });
-
-    describe('renouncing roles', function () {
-      it('renounces an assigned role', async function () {
-        await this.contract[`renounce${rolename}`]({ from: authorized });
-        expect(await this.contract[`is${rolename}`](authorized)).to.equal(false);
-      });
-
-      it(`emits a ${rolename}Removed event`, async function () {
-        const receipt = await this.contract[`renounce${rolename}`]({ from: authorized });
-        expectEvent(receipt, `${rolename}Removed`, { account: authorized });
-      });
-
-      it('reverts when renouncing unassigned role', async function () {
-        await expectRevert(this.contract[`renounce${rolename}`]({ from: other }),
-          'Roles: account does not have role'
-        );
-      });
-    });
-  });
-}
-
-module.exports = {
-  shouldBehaveLikePublicRole,
-};

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

+ 32 - 1
test/token/ERC20/ERC20.test.js

@@ -11,14 +11,45 @@ const {
 } = require('./ERC20.behavior');
 
 const ERC20Mock = contract.fromArtifact('ERC20Mock');
+const ERC20DecimalsMock = contract.fromArtifact('ERC20DecimalsMock');
 
 describe('ERC20', function () {
   const [ initialHolder, recipient, anotherAccount ] = accounts;
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   const initialSupply = new BN(100);
 
   beforeEach(async function () {
-    this.token = await ERC20Mock.new(initialHolder, initialSupply);
+    this.token = await ERC20Mock.new(name, symbol, initialHolder, initialSupply);
+  });
+
+  it('has a name', async function () {
+    expect(await this.token.name()).to.equal(name);
+  });
+
+  it('has a symbol', async function () {
+    expect(await this.token.symbol()).to.equal(symbol);
+  });
+
+  it('has 18 decimals', async function () {
+    expect(await this.token.decimals()).to.be.bignumber.equal('18');
+  });
+
+  describe('_setupDecimals', function () {
+    const decimals = new BN(6);
+
+    it('can set decimals during construction', async function () {
+      const token = await ERC20DecimalsMock.new(name, symbol, decimals);
+      expect(await token.decimals()).to.be.bignumber.equal(decimals);
+    });
+
+    it('reverts if setting decimals after construction', async function () {
+      const token = await ERC20DecimalsMock.new(name, symbol, decimals);
+
+      await expectRevert(token.setupDecimals(decimals.addn(1)), 'ERC20: decimals cannot be changed after construction');
+    });
   });
 
   shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);

+ 4 - 1
test/token/ERC20/ERC20Burnable.test.js

@@ -10,8 +10,11 @@ describe('ERC20Burnable', function () {
 
   const initialBalance = new BN(1000);
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   beforeEach(async function () {
-    this.token = await ERC20BurnableMock.new(owner, initialBalance, { from: owner });
+    this.token = await ERC20BurnableMock.new(name, symbol, owner, initialBalance, { from: owner });
   });
 
   shouldBehaveLikeERC20Burnable(owner, initialBalance, otherAccounts);

+ 5 - 2
test/token/ERC20/ERC20Capped.test.js

@@ -10,15 +10,18 @@ describe('ERC20Capped', function () {
 
   const cap = ether('1000');
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   it('requires a non-zero cap', async function () {
     await expectRevert(
-      ERC20Capped.new(new BN(0), { from: minter }), 'ERC20Capped: cap is 0'
+      ERC20Capped.new(name, symbol, new BN(0), { from: minter }), 'ERC20Capped: cap is 0'
     );
   });
 
   context('once deployed', async function () {
     beforeEach(async function () {
-      this.token = await ERC20Capped.new(cap, { from: minter });
+      this.token = await ERC20Capped.new(name, symbol, cap, { from: minter });
     });
 
     shouldBehaveLikeERC20Capped(minter, otherAccounts, cap);

+ 0 - 28
test/token/ERC20/ERC20Detailed.test.js

@@ -1,28 +0,0 @@
-const { contract } = require('@openzeppelin/test-environment');
-const { BN } = require('@openzeppelin/test-helpers');
-
-const { expect } = require('chai');
-
-const ERC20DetailedMock = contract.fromArtifact('ERC20DetailedMock');
-
-describe('ERC20Detailed', function () {
-  const _name = 'My Detailed ERC20';
-  const _symbol = 'MDT';
-  const _decimals = new BN(18);
-
-  beforeEach(async function () {
-    this.detailedERC20 = await ERC20DetailedMock.new(_name, _symbol, _decimals);
-  });
-
-  it('has a name', async function () {
-    expect(await this.detailedERC20.name()).to.equal(_name);
-  });
-
-  it('has a symbol', async function () {
-    expect(await this.detailedERC20.symbol()).to.equal(_symbol);
-  });
-
-  it('has an amount of decimals', async function () {
-    expect(await this.detailedERC20.decimals()).to.be.bignumber.equal(_decimals);
-  });
-});

+ 4 - 1
test/token/ERC20/ERC20Pausable.test.js

@@ -11,8 +11,11 @@ describe('ERC20Pausable', function () {
 
   const initialSupply = new BN(100);
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   beforeEach(async function () {
-    this.token = await ERC20PausableMock.new(holder, initialSupply);
+    this.token = await ERC20PausableMock.new(name, symbol, holder, initialSupply);
   });
 
   describe('pausable token', function () {

+ 4 - 1
test/token/ERC20/ERC20Snapshot.test.js

@@ -10,8 +10,11 @@ describe('ERC20Snapshot', function () {
 
   const initialSupply = new BN(100);
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   beforeEach(async function () {
-    this.token = await ERC20SnapshotMock.new(initialHolder, initialSupply);
+    this.token = await ERC20SnapshotMock.new(name, symbol, initialHolder, initialSupply);
   });
 
   describe('snapshot', function () {

+ 4 - 1
test/token/ERC20/TokenTimelock.test.js

@@ -10,11 +10,14 @@ const TokenTimelock = contract.fromArtifact('TokenTimelock');
 describe('TokenTimelock', function () {
   const [ beneficiary ] = accounts;
 
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
   const amount = new BN(100);
 
   context('with token', function () {
     beforeEach(async function () {
-      this.token = await ERC20Mock.new(beneficiary, 0); // We're not using the preminted tokens
+      this.token = await ERC20Mock.new(name, symbol, beneficiary, 0); // We're not using the preminted tokens
     });
 
     it('rejects a release time in the past', async function () {

+ 0 - 628
test/token/ERC721/ERC721.behavior.js

@@ -1,628 +0,0 @@
-const { contract } = require('@openzeppelin/test-environment');
-const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
-const { expect } = require('chai');
-const { ZERO_ADDRESS } = constants;
-const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
-
-const ERC721Mock = contract.fromArtifact('ERC721Mock');
-const ERC721ReceiverMock = contract.fromArtifact('ERC721ReceiverMock');
-
-function shouldBehaveLikeERC721 (
-  [owner, approved, anotherApproved, operator, other]
-) {
-  const firstTokenId = new BN(1);
-  const secondTokenId = new BN(2);
-  const unknownTokenId = new BN(3);
-  const RECEIVER_MAGIC_VALUE = '0x150b7a02';
-
-  describe('like an ERC721', function () {
-    beforeEach(async function () {
-      await this.token.mint(owner, firstTokenId);
-      await this.token.mint(owner, secondTokenId);
-      this.toWhom = other; // default to anyone for toWhom in context-dependent tests
-    });
-
-    describe('balanceOf', function () {
-      context('when the given address owns some tokens', function () {
-        it('returns the amount of tokens owned by the given address', async function () {
-          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
-        });
-      });
-
-      context('when the given address does not own any tokens', function () {
-        it('returns 0', async function () {
-          expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0');
-        });
-      });
-
-      context('when querying the zero address', function () {
-        it('throws', async function () {
-          await expectRevert(
-            this.token.balanceOf(ZERO_ADDRESS), 'ERC721: balance query for the zero address'
-          );
-        });
-      });
-    });
-
-    describe('ownerOf', function () {
-      context('when the given token ID was tracked by this token', function () {
-        const tokenId = firstTokenId;
-
-        it('returns the owner of the given token ID', async function () {
-          expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
-        });
-      });
-
-      context('when the given token ID was not tracked by this token', function () {
-        const tokenId = unknownTokenId;
-
-        it('reverts', async function () {
-          await expectRevert(
-            this.token.ownerOf(tokenId), 'ERC721: owner query for nonexistent token'
-          );
-        });
-      });
-    });
-
-    describe('transfers', function () {
-      const tokenId = firstTokenId;
-      const data = '0x42';
-
-      let logs = null;
-
-      beforeEach(async function () {
-        await this.token.approve(approved, tokenId, { from: owner });
-        await this.token.setApprovalForAll(operator, true, { from: owner });
-      });
-
-      const transferWasSuccessful = function ({ owner, tokenId, approved }) {
-        it('transfers the ownership of the given token ID to the given address', async function () {
-          expect(await this.token.ownerOf(tokenId)).to.be.equal(this.toWhom);
-        });
-
-        it('clears the approval for the token ID', async function () {
-          expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
-        });
-
-        if (approved) {
-          it('emit only a transfer event', async function () {
-            expectEvent.inLogs(logs, 'Transfer', {
-              from: owner,
-              to: this.toWhom,
-              tokenId: tokenId,
-            });
-          });
-        } else {
-          it('emits only a transfer event', async function () {
-            expectEvent.inLogs(logs, 'Transfer', {
-              from: owner,
-              to: this.toWhom,
-              tokenId: tokenId,
-            });
-          });
-        }
-
-        it('adjusts owners balances', async function () {
-          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
-        });
-
-        it('adjusts owners tokens by index', async function () {
-          if (!this.token.tokenOfOwnerByIndex) return;
-
-          expect(await this.token.tokenOfOwnerByIndex(this.toWhom, 0)).to.be.bignumber.equal(tokenId);
-
-          expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.not.equal(tokenId);
-        });
-      };
-
-      const shouldTransferTokensByUsers = function (transferFunction) {
-        context('when called by the owner', function () {
-          beforeEach(async function () {
-            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner }));
-          });
-          transferWasSuccessful({ owner, tokenId, approved });
-        });
-
-        context('when called by the approved individual', function () {
-          beforeEach(async function () {
-            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: approved }));
-          });
-          transferWasSuccessful({ owner, tokenId, approved });
-        });
-
-        context('when called by the operator', function () {
-          beforeEach(async function () {
-            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }));
-          });
-          transferWasSuccessful({ owner, tokenId, approved });
-        });
-
-        context('when called by the owner without an approved user', function () {
-          beforeEach(async function () {
-            await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
-            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }));
-          });
-          transferWasSuccessful({ owner, tokenId, approved: null });
-        });
-
-        context('when sent to the owner', function () {
-          beforeEach(async function () {
-            ({ logs } = await transferFunction.call(this, owner, owner, tokenId, { from: owner }));
-          });
-
-          it('keeps ownership of the token', async function () {
-            expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
-          });
-
-          it('clears the approval for the token ID', async function () {
-            expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
-          });
-
-          it('emits only a transfer event', async function () {
-            expectEvent.inLogs(logs, 'Transfer', {
-              from: owner,
-              to: owner,
-              tokenId: tokenId,
-            });
-          });
-
-          it('keeps the owner balance', async function () {
-            expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
-          });
-
-          it('keeps same tokens by index', async function () {
-            if (!this.token.tokenOfOwnerByIndex) return;
-            const tokensListed = await Promise.all(
-              [0, 1].map(i => this.token.tokenOfOwnerByIndex(owner, i))
-            );
-            expect(tokensListed.map(t => t.toNumber())).to.have.members(
-              [firstTokenId.toNumber(), secondTokenId.toNumber()]
-            );
-          });
-        });
-
-        context('when the address of the previous owner is incorrect', function () {
-          it('reverts', async function () {
-            await expectRevert(
-              transferFunction.call(this, other, other, tokenId, { from: owner }),
-              'ERC721: transfer of token that is not own'
-            );
-          });
-        });
-
-        context('when the sender is not authorized for the token id', function () {
-          it('reverts', async function () {
-            await expectRevert(
-              transferFunction.call(this, owner, other, tokenId, { from: other }),
-              'ERC721: transfer caller is not owner nor approved'
-            );
-          });
-        });
-
-        context('when the given token ID does not exist', function () {
-          it('reverts', async function () {
-            await expectRevert(
-              transferFunction.call(this, owner, other, unknownTokenId, { from: owner }),
-              'ERC721: operator query for nonexistent token'
-            );
-          });
-        });
-
-        context('when the address to transfer the token to is the zero address', function () {
-          it('reverts', async function () {
-            await expectRevert(
-              transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }),
-              'ERC721: transfer to the zero address'
-            );
-          });
-        });
-      };
-
-      describe('via transferFrom', function () {
-        shouldTransferTokensByUsers(function (from, to, tokenId, opts) {
-          return this.token.transferFrom(from, to, tokenId, opts);
-        });
-      });
-
-      describe('via safeTransferFrom', function () {
-        const safeTransferFromWithData = function (from, to, tokenId, opts) {
-          return this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](from, to, tokenId, data, opts);
-        };
-
-        const safeTransferFromWithoutData = function (from, to, tokenId, opts) {
-          return this.token.methods['safeTransferFrom(address,address,uint256)'](from, to, tokenId, opts);
-        };
-
-        const shouldTransferSafely = function (transferFun, data) {
-          describe('to a user account', function () {
-            shouldTransferTokensByUsers(transferFun);
-          });
-
-          describe('to a valid receiver contract', function () {
-            beforeEach(async function () {
-              this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
-              this.toWhom = this.receiver.address;
-            });
-
-            shouldTransferTokensByUsers(transferFun);
-
-            it('should call onERC721Received', async function () {
-              const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner });
-
-              await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
-                operator: owner,
-                from: owner,
-                tokenId: tokenId,
-                data: data,
-              });
-            });
-
-            it('should call onERC721Received from approved', async function () {
-              const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved });
-
-              await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
-                operator: approved,
-                from: owner,
-                tokenId: tokenId,
-                data: data,
-              });
-            });
-
-            describe('with an invalid token id', function () {
-              it('reverts', async function () {
-                await expectRevert(
-                  transferFun.call(
-                    this,
-                    owner,
-                    this.receiver.address,
-                    unknownTokenId,
-                    { from: owner },
-                  ),
-                  'ERC721: operator query for nonexistent token'
-                );
-              });
-            });
-          });
-        };
-
-        describe('with data', function () {
-          shouldTransferSafely(safeTransferFromWithData, data);
-        });
-
-        describe('without data', function () {
-          shouldTransferSafely(safeTransferFromWithoutData, null);
-        });
-
-        describe('to a receiver contract returning unexpected value', function () {
-          it('reverts', async function () {
-            const invalidReceiver = await ERC721ReceiverMock.new('0x42', false);
-            await expectRevert(
-              this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }),
-              'ERC721: transfer to non ERC721Receiver implementer'
-            );
-          });
-        });
-
-        describe('to a receiver contract that throws', function () {
-          it('reverts', async function () {
-            const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, true);
-            await expectRevert(
-              this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
-              'ERC721ReceiverMock: reverting'
-            );
-          });
-        });
-
-        describe('to a contract that does not implement the required function', function () {
-          it('reverts', async function () {
-            const nonReceiver = this.token;
-            await expectRevert(
-              this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }),
-              'ERC721: transfer to non ERC721Receiver implementer'
-            );
-          });
-        });
-      });
-    });
-
-    describe('safe mint', function () {
-      const fourthTokenId = new BN(4);
-      const tokenId = fourthTokenId;
-      const data = '0x42';
-
-      beforeEach(async function () {
-        this.ERC721Mock = await ERC721Mock.new();
-      });
-
-      describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others
-        it('should call onERC721Received — with data', async function () {
-          this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
-          const receipt = await this.ERC721Mock.safeMint(this.receiver.address, tokenId, data);
-
-          await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
-            from: ZERO_ADDRESS,
-            tokenId: tokenId,
-            data: data,
-          });
-        });
-
-        it('should call onERC721Received — without data', async function () {
-          this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
-          const receipt = await this.ERC721Mock.safeMint(this.receiver.address, tokenId);
-
-          await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
-            from: ZERO_ADDRESS,
-            tokenId: tokenId,
-          });
-        });
-
-        context('to a receiver contract returning unexpected value', function () {
-          it('reverts', async function () {
-            const invalidReceiver = await ERC721ReceiverMock.new('0x42', false);
-            await expectRevert(
-              this.ERC721Mock.safeMint(invalidReceiver.address, tokenId),
-              'ERC721: transfer to non ERC721Receiver implementer'
-            );
-          });
-        });
-
-        context('to a receiver contract that throws', function () {
-          it('reverts', async function () {
-            const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, true);
-            await expectRevert(
-              this.ERC721Mock.safeMint(revertingReceiver.address, tokenId),
-              'ERC721ReceiverMock: reverting'
-            );
-          });
-        });
-
-        context('to a contract that does not implement the required function', function () {
-          it('reverts', async function () {
-            const nonReceiver = this.token;
-            await expectRevert(
-              this.ERC721Mock.safeMint(nonReceiver.address, tokenId),
-              'ERC721: transfer to non ERC721Receiver implementer'
-            );
-          });
-        });
-      });
-    });
-
-    describe('approve', function () {
-      const tokenId = firstTokenId;
-
-      let logs = null;
-
-      const itClearsApproval = function () {
-        it('clears approval for the token', async function () {
-          expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
-        });
-      };
-
-      const itApproves = function (address) {
-        it('sets the approval for the target address', async function () {
-          expect(await this.token.getApproved(tokenId)).to.be.equal(address);
-        });
-      };
-
-      const itEmitsApprovalEvent = function (address) {
-        it('emits an approval event', async function () {
-          expectEvent.inLogs(logs, 'Approval', {
-            owner: owner,
-            approved: address,
-            tokenId: tokenId,
-          });
-        });
-      };
-
-      context('when clearing approval', function () {
-        context('when there was no prior approval', function () {
-          beforeEach(async function () {
-            ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }));
-          });
-
-          itClearsApproval();
-          itEmitsApprovalEvent(ZERO_ADDRESS);
-        });
-
-        context('when there was a prior approval', function () {
-          beforeEach(async function () {
-            await this.token.approve(approved, tokenId, { from: owner });
-            ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }));
-          });
-
-          itClearsApproval();
-          itEmitsApprovalEvent(ZERO_ADDRESS);
-        });
-      });
-
-      context('when approving a non-zero address', function () {
-        context('when there was no prior approval', function () {
-          beforeEach(async function () {
-            ({ logs } = await this.token.approve(approved, tokenId, { from: owner }));
-          });
-
-          itApproves(approved);
-          itEmitsApprovalEvent(approved);
-        });
-
-        context('when there was a prior approval to the same address', function () {
-          beforeEach(async function () {
-            await this.token.approve(approved, tokenId, { from: owner });
-            ({ logs } = await this.token.approve(approved, tokenId, { from: owner }));
-          });
-
-          itApproves(approved);
-          itEmitsApprovalEvent(approved);
-        });
-
-        context('when there was a prior approval to a different address', function () {
-          beforeEach(async function () {
-            await this.token.approve(anotherApproved, tokenId, { from: owner });
-            ({ logs } = await this.token.approve(anotherApproved, tokenId, { from: owner }));
-          });
-
-          itApproves(anotherApproved);
-          itEmitsApprovalEvent(anotherApproved);
-        });
-      });
-
-      context('when the address that receives the approval is the owner', function () {
-        it('reverts', async function () {
-          await expectRevert(
-            this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner'
-          );
-        });
-      });
-
-      context('when the sender does not own the given token ID', function () {
-        it('reverts', async function () {
-          await expectRevert(this.token.approve(approved, tokenId, { from: other }),
-            'ERC721: approve caller is not owner nor approved');
-        });
-      });
-
-      context('when the sender is approved for the given token ID', function () {
-        it('reverts', async function () {
-          await this.token.approve(approved, tokenId, { from: owner });
-          await expectRevert(this.token.approve(anotherApproved, tokenId, { from: approved }),
-            'ERC721: approve caller is not owner nor approved for all');
-        });
-      });
-
-      context('when the sender is an operator', function () {
-        beforeEach(async function () {
-          await this.token.setApprovalForAll(operator, true, { from: owner });
-          ({ logs } = await this.token.approve(approved, tokenId, { from: operator }));
-        });
-
-        itApproves(approved);
-        itEmitsApprovalEvent(approved);
-      });
-
-      context('when the given token ID does not exist', function () {
-        it('reverts', async function () {
-          await expectRevert(this.token.approve(approved, unknownTokenId, { from: operator }),
-            'ERC721: owner query for nonexistent token');
-        });
-      });
-    });
-
-    describe('setApprovalForAll', function () {
-      context('when the operator willing to approve is not the owner', function () {
-        context('when there is no operator approval set by the sender', function () {
-          it('approves the operator', async function () {
-            await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
-          });
-
-          it('emits an approval event', async function () {
-            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expectEvent.inLogs(logs, 'ApprovalForAll', {
-              owner: owner,
-              operator: operator,
-              approved: true,
-            });
-          });
-        });
-
-        context('when the operator was set as not approved', function () {
-          beforeEach(async function () {
-            await this.token.setApprovalForAll(operator, false, { from: owner });
-          });
-
-          it('approves the operator', async function () {
-            await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
-          });
-
-          it('emits an approval event', async function () {
-            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expectEvent.inLogs(logs, 'ApprovalForAll', {
-              owner: owner,
-              operator: operator,
-              approved: true,
-            });
-          });
-
-          it('can unset the operator approval', async function () {
-            await this.token.setApprovalForAll(operator, false, { from: owner });
-
-            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false);
-          });
-        });
-
-        context('when the operator was already approved', function () {
-          beforeEach(async function () {
-            await this.token.setApprovalForAll(operator, true, { from: owner });
-          });
-
-          it('keeps the approval to the given address', async function () {
-            await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
-          });
-
-          it('emits an approval event', async function () {
-            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
-
-            expectEvent.inLogs(logs, 'ApprovalForAll', {
-              owner: owner,
-              operator: operator,
-              approved: true,
-            });
-          });
-        });
-      });
-
-      context('when the operator is the owner', function () {
-        it('reverts', async function () {
-          await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }),
-            'ERC721: approve to caller');
-        });
-      });
-    });
-
-    describe('getApproved', async function () {
-      context('when token is not minted', async function () {
-        it('reverts', async function () {
-          await expectRevert(
-            this.token.getApproved(unknownTokenId),
-            'ERC721: approved query for nonexistent token'
-          );
-        });
-      });
-
-      context('when token has been minted ', async function () {
-        it('should return the zero address', async function () {
-          expect(await this.token.getApproved(firstTokenId)).to.be.equal(
-            ZERO_ADDRESS
-          );
-        });
-
-        context('when account has been approved', async function () {
-          beforeEach(async function () {
-            await this.token.approve(approved, firstTokenId, { from: owner });
-          });
-
-          it('should return approved account', async function () {
-            expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved);
-          });
-        });
-      });
-    });
-
-    shouldSupportInterfaces([
-      'ERC165',
-      'ERC721',
-    ]);
-  });
-}
-
-module.exports = {
-  shouldBehaveLikeERC721,
-};

+ 833 - 36
test/token/ERC721/ERC721.test.js

@@ -5,83 +5,880 @@ const { ZERO_ADDRESS } = constants;
 
 const { expect } = require('chai');
 
-const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
+const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
+
 const ERC721Mock = contract.fromArtifact('ERC721Mock');
+const ERC721ReceiverMock = contract.fromArtifact('ERC721ReceiverMock');
 
 describe('ERC721', function () {
-  const [ owner ] = accounts;
+  const [owner, newOwner, approved, anotherApproved, operator, other] = accounts;
+
+  const name = 'Non Fungible Token';
+  const symbol = 'NFT';
+
+  const firstTokenId = new BN('5042');
+  const secondTokenId = new BN('79217');
+  const nonExistentTokenId = new BN('13');
+
+  const RECEIVER_MAGIC_VALUE = '0x150b7a02';
 
   beforeEach(async function () {
-    this.token = await ERC721Mock.new();
+    this.token = await ERC721Mock.new(name, symbol);
   });
 
-  shouldBehaveLikeERC721(accounts);
+  shouldSupportInterfaces([
+    'ERC165',
+    'ERC721',
+    'ERC721Enumerable',
+    'ERC721Metadata',
+  ]);
+
+  describe('metadata', function () {
+    it('has a name', async function () {
+      expect(await this.token.name()).to.be.equal(name);
+    });
+
+    it('has a symbol', async function () {
+      expect(await this.token.symbol()).to.be.equal(symbol);
+    });
+
+    describe('token URI', function () {
+      beforeEach(async function () {
+        await this.token.mint(owner, firstTokenId);
+      });
+
+      const baseURI = 'https://api.com/v1/';
+      const sampleUri = 'mock://mytoken';
 
-  describe('internal functions', function () {
-    const tokenId = new BN('5042');
+      it('it is empty by default', async function () {
+        expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
+      });
 
-    describe('_mint(address, uint256)', function () {
-      it('reverts with a null destination address', async function () {
+      it('reverts when queried for non existent token id', async function () {
         await expectRevert(
-          this.token.mint(ZERO_ADDRESS, tokenId), 'ERC721: mint to the zero address'
+          this.token.tokenURI(nonExistentTokenId), 'ERC721Metadata: URI query for nonexistent token'
         );
       });
 
-      context('with minted token', async function () {
-        beforeEach(async function () {
-          ({ logs: this.logs } = await this.token.mint(owner, tokenId));
+      it('can be set for a token id', async function () {
+        await this.token.setTokenURI(firstTokenId, sampleUri);
+        expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri);
+      });
+
+      it('reverts when setting for non existent token id', async function () {
+        await expectRevert(
+          this.token.setTokenURI(nonExistentTokenId, sampleUri), 'ERC721Metadata: URI set of nonexistent token'
+        );
+      });
+
+      it('base URI can be set', async function () {
+        await this.token.setBaseURI(baseURI);
+        expect(await this.token.baseURI()).to.equal(baseURI);
+      });
+
+      it('base URI is added as a prefix to the token URI', async function () {
+        await this.token.setBaseURI(baseURI);
+        await this.token.setTokenURI(firstTokenId, sampleUri);
+
+        expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri);
+      });
+
+      it('token URI can be changed by changing the base URI', async function () {
+        await this.token.setBaseURI(baseURI);
+        await this.token.setTokenURI(firstTokenId, sampleUri);
+
+        const newBaseURI = 'https://api.com/v2/';
+        await this.token.setBaseURI(newBaseURI);
+        expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri);
+      });
+
+      it('token URI is empty for tokens with no URI but with base URI', async function () {
+        await this.token.setBaseURI(baseURI);
+
+        expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
+      });
+
+      it('tokens with URI can be burnt ', async function () {
+        await this.token.setTokenURI(firstTokenId, sampleUri);
+
+        await this.token.burn(firstTokenId, { from: owner });
+
+        expect(await this.token.exists(firstTokenId)).to.equal(false);
+        await expectRevert(
+          this.token.tokenURI(firstTokenId), 'ERC721Metadata: URI query for nonexistent token'
+        );
+      });
+    });
+  });
+
+  context('with minted tokens', function () {
+    beforeEach(async function () {
+      await this.token.mint(owner, firstTokenId);
+      await this.token.mint(owner, secondTokenId);
+      this.toWhom = other; // default to other for toWhom in context-dependent tests
+    });
+
+    describe('balanceOf', function () {
+      context('when the given address owns some tokens', function () {
+        it('returns the amount of tokens owned by the given address', async function () {
+          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
         });
+      });
 
-        it('emits a Transfer event', function () {
-          expectEvent.inLogs(this.logs, 'Transfer', { from: ZERO_ADDRESS, to: owner, tokenId });
+      context('when the given address does not own any tokens', function () {
+        it('returns 0', async function () {
+          expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0');
         });
+      });
 
-        it('creates the token', async function () {
-          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
-          expect(await this.token.ownerOf(tokenId)).to.equal(owner);
+      context('when querying the zero address', function () {
+        it('throws', async function () {
+          await expectRevert(
+            this.token.balanceOf(ZERO_ADDRESS), 'ERC721: balance query for the zero address'
+          );
         });
+      });
+    });
+
+    describe('ownerOf', function () {
+      context('when the given token ID was tracked by this token', function () {
+        const tokenId = firstTokenId;
 
-        it('reverts when adding a token id that already exists', async function () {
-          await expectRevert(this.token.mint(owner, tokenId), 'ERC721: token already minted');
+        it('returns the owner of the given token ID', async function () {
+          expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
+        });
+      });
+
+      context('when the given token ID was not tracked by this token', function () {
+        const tokenId = nonExistentTokenId;
+
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.ownerOf(tokenId), 'ERC721: owner query for nonexistent token'
+          );
         });
       });
     });
 
-    describe('_burn', function () {
-      it('reverts when burning a non-existent token id', async function () {
-        await expectRevert(
-          this.token.burn(tokenId), 'ERC721: owner query for nonexistent token'
-        );
+    describe('transfers', function () {
+      const tokenId = firstTokenId;
+      const data = '0x42';
+
+      let logs = null;
+
+      beforeEach(async function () {
+        await this.token.approve(approved, tokenId, { from: owner });
+        await this.token.setApprovalForAll(operator, true, { from: owner });
       });
 
-      context('with minted token', function () {
-        beforeEach(async function () {
-          await this.token.mint(owner, tokenId);
+      const transferWasSuccessful = function ({ owner, tokenId, approved }) {
+        it('transfers the ownership of the given token ID to the given address', async function () {
+          expect(await this.token.ownerOf(tokenId)).to.be.equal(this.toWhom);
+        });
+
+        it('emits a Transfer event', async function () {
+          expectEvent.inLogs(logs, 'Transfer', { from: owner, to: this.toWhom, tokenId: tokenId });
         });
 
-        context('with burnt token', function () {
+        it('clears the approval for the token ID', async function () {
+          expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
+        });
+
+        it('emits an Approval event', async function () {
+          expectEvent.inLogs(logs, 'Approval', { owner, approved: ZERO_ADDRESS, tokenId: tokenId });
+        });
+
+        it('adjusts owners balances', async function () {
+          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
+        });
+
+        it('adjusts owners tokens by index', async function () {
+          if (!this.token.tokenOfOwnerByIndex) return;
+
+          expect(await this.token.tokenOfOwnerByIndex(this.toWhom, 0)).to.be.bignumber.equal(tokenId);
+
+          expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.not.equal(tokenId);
+        });
+      };
+
+      const shouldTransferTokensByUsers = function (transferFunction) {
+        context('when called by the owner', function () {
+          beforeEach(async function () {
+            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner }));
+          });
+          transferWasSuccessful({ owner, tokenId, approved });
+        });
+
+        context('when called by the approved individual', function () {
+          beforeEach(async function () {
+            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: approved }));
+          });
+          transferWasSuccessful({ owner, tokenId, approved });
+        });
+
+        context('when called by the operator', function () {
           beforeEach(async function () {
-            ({ logs: this.logs } = await this.token.burn(tokenId));
+            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }));
+          });
+          transferWasSuccessful({ owner, tokenId, approved });
+        });
+
+        context('when called by the owner without an approved user', function () {
+          beforeEach(async function () {
+            await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
+            ({ logs } = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }));
+          });
+          transferWasSuccessful({ owner, tokenId, approved: null });
+        });
+
+        context('when sent to the owner', function () {
+          beforeEach(async function () {
+            ({ logs } = await transferFunction.call(this, owner, owner, tokenId, { from: owner }));
+          });
+
+          it('keeps ownership of the token', async function () {
+            expect(await this.token.ownerOf(tokenId)).to.be.equal(owner);
           });
 
-          it('emits a Transfer event', function () {
-            expectEvent.inLogs(this.logs, 'Transfer', { from: owner, to: ZERO_ADDRESS, tokenId });
+          it('clears the approval for the token ID', async function () {
+            expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
           });
 
-          it('deletes the token', async function () {
-            expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0');
+          it('emits only a transfer event', async function () {
+            expectEvent.inLogs(logs, 'Transfer', {
+              from: owner,
+              to: owner,
+              tokenId: tokenId,
+            });
+          });
+
+          it('keeps the owner balance', async function () {
+            expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2');
+          });
+
+          it('keeps same tokens by index', async function () {
+            if (!this.token.tokenOfOwnerByIndex) return;
+            const tokensListed = await Promise.all(
+              [0, 1].map(i => this.token.tokenOfOwnerByIndex(owner, i))
+            );
+            expect(tokensListed.map(t => t.toNumber())).to.have.members(
+              [firstTokenId.toNumber(), secondTokenId.toNumber()]
+            );
+          });
+        });
+
+        context('when the address of the previous owner is incorrect', function () {
+          it('reverts', async function () {
+            await expectRevert(
+              transferFunction.call(this, other, other, tokenId, { from: owner }),
+              'ERC721: transfer of token that is not own'
+            );
+          });
+        });
+
+        context('when the sender is not authorized for the token id', function () {
+          it('reverts', async function () {
+            await expectRevert(
+              transferFunction.call(this, owner, other, tokenId, { from: other }),
+              'ERC721: transfer caller is not owner nor approved'
+            );
+          });
+        });
+
+        context('when the given token ID does not exist', function () {
+          it('reverts', async function () {
+            await expectRevert(
+              transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }),
+              'ERC721: operator query for nonexistent token'
+            );
+          });
+        });
+
+        context('when the address to transfer the token to is the zero address', function () {
+          it('reverts', async function () {
+            await expectRevert(
+              transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }),
+              'ERC721: transfer to the zero address'
+            );
+          });
+        });
+      };
+
+      describe('via transferFrom', function () {
+        shouldTransferTokensByUsers(function (from, to, tokenId, opts) {
+          return this.token.transferFrom(from, to, tokenId, opts);
+        });
+      });
+
+      describe('via safeTransferFrom', function () {
+        const safeTransferFromWithData = function (from, to, tokenId, opts) {
+          return this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](from, to, tokenId, data, opts);
+        };
+
+        const safeTransferFromWithoutData = function (from, to, tokenId, opts) {
+          return this.token.methods['safeTransferFrom(address,address,uint256)'](from, to, tokenId, opts);
+        };
+
+        const shouldTransferSafely = function (transferFun, data) {
+          describe('to a user account', function () {
+            shouldTransferTokensByUsers(transferFun);
+          });
+
+          describe('to a valid receiver contract', function () {
+            beforeEach(async function () {
+              this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
+              this.toWhom = this.receiver.address;
+            });
+
+            shouldTransferTokensByUsers(transferFun);
+
+            it('should call onERC721Received', async function () {
+              const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner });
+
+              await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
+                operator: owner,
+                from: owner,
+                tokenId: tokenId,
+                data: data,
+              });
+            });
+
+            it('should call onERC721Received from approved', async function () {
+              const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved });
+
+              await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
+                operator: approved,
+                from: owner,
+                tokenId: tokenId,
+                data: data,
+              });
+            });
+
+            describe('with an invalid token id', function () {
+              it('reverts', async function () {
+                await expectRevert(
+                  transferFun.call(
+                    this,
+                    owner,
+                    this.receiver.address,
+                    nonExistentTokenId,
+                    { from: owner },
+                  ),
+                  'ERC721: operator query for nonexistent token'
+                );
+              });
+            });
+          });
+        };
+
+        describe('with data', function () {
+          shouldTransferSafely(safeTransferFromWithData, data);
+        });
+
+        describe('without data', function () {
+          shouldTransferSafely(safeTransferFromWithoutData, null);
+        });
+
+        describe('to a receiver contract returning unexpected value', function () {
+          it('reverts', async function () {
+            const invalidReceiver = await ERC721ReceiverMock.new('0x42', false);
             await expectRevert(
-              this.token.ownerOf(tokenId), 'ERC721: owner query for nonexistent token'
+              this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }),
+              'ERC721: transfer to non ERC721Receiver implementer'
             );
           });
+        });
+
+        describe('to a receiver contract that throws', function () {
+          it('reverts', async function () {
+            const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, true);
+            await expectRevert(
+              this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
+              'ERC721ReceiverMock: reverting'
+            );
+          });
+        });
 
-          it('reverts when burning a token id that has been deleted', async function () {
+        describe('to a contract that does not implement the required function', function () {
+          it('reverts', async function () {
+            const nonReceiver = this.token;
             await expectRevert(
-              this.token.burn(tokenId), 'ERC721: owner query for nonexistent token'
+              this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }),
+              'ERC721: transfer to non ERC721Receiver implementer'
             );
           });
         });
       });
     });
+
+    describe('safe mint', function () {
+      const fourthTokenId = new BN(4);
+      const tokenId = fourthTokenId;
+      const data = '0x42';
+
+      describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others
+        it('should call onERC721Received — with data', async function () {
+          this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
+          const receipt = await this.token.safeMint(this.receiver.address, tokenId, data);
+
+          await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
+            from: ZERO_ADDRESS,
+            tokenId: tokenId,
+            data: data,
+          });
+        });
+
+        it('should call onERC721Received — without data', async function () {
+          this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false);
+          const receipt = await this.token.safeMint(this.receiver.address, tokenId);
+
+          await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
+            from: ZERO_ADDRESS,
+            tokenId: tokenId,
+          });
+        });
+
+        context('to a receiver contract returning unexpected value', function () {
+          it('reverts', async function () {
+            const invalidReceiver = await ERC721ReceiverMock.new('0x42', false);
+            await expectRevert(
+              this.token.safeMint(invalidReceiver.address, tokenId),
+              'ERC721: transfer to non ERC721Receiver implementer'
+            );
+          });
+        });
+
+        context('to a receiver contract that throws', function () {
+          it('reverts', async function () {
+            const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, true);
+            await expectRevert(
+              this.token.safeMint(revertingReceiver.address, tokenId),
+              'ERC721ReceiverMock: reverting'
+            );
+          });
+        });
+
+        context('to a contract that does not implement the required function', function () {
+          it('reverts', async function () {
+            const nonReceiver = this.token;
+            await expectRevert(
+              this.token.safeMint(nonReceiver.address, tokenId),
+              'ERC721: transfer to non ERC721Receiver implementer'
+            );
+          });
+        });
+      });
+    });
+
+    describe('approve', function () {
+      const tokenId = firstTokenId;
+
+      let logs = null;
+
+      const itClearsApproval = function () {
+        it('clears approval for the token', async function () {
+          expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS);
+        });
+      };
+
+      const itApproves = function (address) {
+        it('sets the approval for the target address', async function () {
+          expect(await this.token.getApproved(tokenId)).to.be.equal(address);
+        });
+      };
+
+      const itEmitsApprovalEvent = function (address) {
+        it('emits an approval event', async function () {
+          expectEvent.inLogs(logs, 'Approval', {
+            owner: owner,
+            approved: address,
+            tokenId: tokenId,
+          });
+        });
+      };
+
+      context('when clearing approval', function () {
+        context('when there was no prior approval', function () {
+          beforeEach(async function () {
+            ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }));
+          });
+
+          itClearsApproval();
+          itEmitsApprovalEvent(ZERO_ADDRESS);
+        });
+
+        context('when there was a prior approval', function () {
+          beforeEach(async function () {
+            await this.token.approve(approved, tokenId, { from: owner });
+            ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }));
+          });
+
+          itClearsApproval();
+          itEmitsApprovalEvent(ZERO_ADDRESS);
+        });
+      });
+
+      context('when approving a non-zero address', function () {
+        context('when there was no prior approval', function () {
+          beforeEach(async function () {
+            ({ logs } = await this.token.approve(approved, tokenId, { from: owner }));
+          });
+
+          itApproves(approved);
+          itEmitsApprovalEvent(approved);
+        });
+
+        context('when there was a prior approval to the same address', function () {
+          beforeEach(async function () {
+            await this.token.approve(approved, tokenId, { from: owner });
+            ({ logs } = await this.token.approve(approved, tokenId, { from: owner }));
+          });
+
+          itApproves(approved);
+          itEmitsApprovalEvent(approved);
+        });
+
+        context('when there was a prior approval to a different address', function () {
+          beforeEach(async function () {
+            await this.token.approve(anotherApproved, tokenId, { from: owner });
+            ({ logs } = await this.token.approve(anotherApproved, tokenId, { from: owner }));
+          });
+
+          itApproves(anotherApproved);
+          itEmitsApprovalEvent(anotherApproved);
+        });
+      });
+
+      context('when the address that receives the approval is the owner', function () {
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner'
+          );
+        });
+      });
+
+      context('when the sender does not own the given token ID', function () {
+        it('reverts', async function () {
+          await expectRevert(this.token.approve(approved, tokenId, { from: other }),
+            'ERC721: approve caller is not owner nor approved');
+        });
+      });
+
+      context('when the sender is approved for the given token ID', function () {
+        it('reverts', async function () {
+          await this.token.approve(approved, tokenId, { from: owner });
+          await expectRevert(this.token.approve(anotherApproved, tokenId, { from: approved }),
+            'ERC721: approve caller is not owner nor approved for all');
+        });
+      });
+
+      context('when the sender is an operator', function () {
+        beforeEach(async function () {
+          await this.token.setApprovalForAll(operator, true, { from: owner });
+          ({ logs } = await this.token.approve(approved, tokenId, { from: operator }));
+        });
+
+        itApproves(approved);
+        itEmitsApprovalEvent(approved);
+      });
+
+      context('when the given token ID does not exist', function () {
+        it('reverts', async function () {
+          await expectRevert(this.token.approve(approved, nonExistentTokenId, { from: operator }),
+            'ERC721: owner query for nonexistent token');
+        });
+      });
+    });
+
+    describe('setApprovalForAll', function () {
+      context('when the operator willing to approve is not the owner', function () {
+        context('when there is no operator approval set by the sender', function () {
+          it('approves the operator', async function () {
+            await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
+          });
+
+          it('emits an approval event', async function () {
+            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expectEvent.inLogs(logs, 'ApprovalForAll', {
+              owner: owner,
+              operator: operator,
+              approved: true,
+            });
+          });
+        });
+
+        context('when the operator was set as not approved', function () {
+          beforeEach(async function () {
+            await this.token.setApprovalForAll(operator, false, { from: owner });
+          });
+
+          it('approves the operator', async function () {
+            await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
+          });
+
+          it('emits an approval event', async function () {
+            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expectEvent.inLogs(logs, 'ApprovalForAll', {
+              owner: owner,
+              operator: operator,
+              approved: true,
+            });
+          });
+
+          it('can unset the operator approval', async function () {
+            await this.token.setApprovalForAll(operator, false, { from: owner });
+
+            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false);
+          });
+        });
+
+        context('when the operator was already approved', function () {
+          beforeEach(async function () {
+            await this.token.setApprovalForAll(operator, true, { from: owner });
+          });
+
+          it('keeps the approval to the given address', async function () {
+            await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true);
+          });
+
+          it('emits an approval event', async function () {
+            const { logs } = await this.token.setApprovalForAll(operator, true, { from: owner });
+
+            expectEvent.inLogs(logs, 'ApprovalForAll', {
+              owner: owner,
+              operator: operator,
+              approved: true,
+            });
+          });
+        });
+      });
+
+      context('when the operator is the owner', function () {
+        it('reverts', async function () {
+          await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }),
+            'ERC721: approve to caller');
+        });
+      });
+    });
+
+    describe('getApproved', async function () {
+      context('when token is not minted', async function () {
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.getApproved(nonExistentTokenId),
+            'ERC721: approved query for nonexistent token'
+          );
+        });
+      });
+
+      context('when token has been minted ', async function () {
+        it('should return the zero address', async function () {
+          expect(await this.token.getApproved(firstTokenId)).to.be.equal(
+            ZERO_ADDRESS
+          );
+        });
+
+        context('when account has been approved', async function () {
+          beforeEach(async function () {
+            await this.token.approve(approved, firstTokenId, { from: owner });
+          });
+
+          it('should return approved account', async function () {
+            expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved);
+          });
+        });
+      });
+    });
+
+    describe('totalSupply', function () {
+      it('returns total token supply', async function () {
+        expect(await this.token.totalSupply()).to.be.bignumber.equal('2');
+      });
+    });
+
+    describe('tokenOfOwnerByIndex', function () {
+      describe('when the given index is lower than the amount of tokens owned by the given address', function () {
+        it('returns the token ID placed at the given index', async function () {
+          expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId);
+        });
+      });
+
+      describe('when the index is greater than or equal to the total tokens owned by the given address', function () {
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.tokenOfOwnerByIndex(owner, 2), 'EnumerableSet: index out of bounds'
+          );
+        });
+      });
+
+      describe('when the given address does not own any token', function () {
+        it('reverts', async function () {
+          await expectRevert(
+            this.token.tokenOfOwnerByIndex(other, 0), 'EnumerableSet: index out of bounds'
+          );
+        });
+      });
+
+      describe('after transferring all tokens to another user', function () {
+        beforeEach(async function () {
+          await this.token.transferFrom(owner, other, firstTokenId, { from: owner });
+          await this.token.transferFrom(owner, other, secondTokenId, { from: owner });
+        });
+
+        it('returns correct token IDs for target', async function () {
+          expect(await this.token.balanceOf(other)).to.be.bignumber.equal('2');
+          const tokensListed = await Promise.all(
+            [0, 1].map(i => this.token.tokenOfOwnerByIndex(other, i))
+          );
+          expect(tokensListed.map(t => t.toNumber())).to.have.members([firstTokenId.toNumber(),
+            secondTokenId.toNumber()]);
+        });
+
+        it('returns empty collection for original owner', async function () {
+          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0');
+          await expectRevert(
+            this.token.tokenOfOwnerByIndex(owner, 0), 'EnumerableSet: index out of bounds'
+          );
+        });
+      });
+    });
+
+    describe('tokenByIndex', function () {
+      it('should return all tokens', async function () {
+        const tokensListed = await Promise.all(
+          [0, 1].map(i => this.token.tokenByIndex(i))
+        );
+        expect(tokensListed.map(t => t.toNumber())).to.have.members([firstTokenId.toNumber(),
+          secondTokenId.toNumber()]);
+      });
+
+      it('should revert if index is greater than supply', async function () {
+        await expectRevert(
+          this.token.tokenByIndex(2), 'EnumerableMap: index out of bounds'
+        );
+      });
+
+      [firstTokenId, secondTokenId].forEach(function (tokenId) {
+        it(`should return all tokens after burning token ${tokenId} and minting new tokens`, async function () {
+          const newTokenId = new BN(300);
+          const anotherNewTokenId = new BN(400);
+
+          await this.token.burn(tokenId);
+          await this.token.mint(newOwner, newTokenId);
+          await this.token.mint(newOwner, anotherNewTokenId);
+
+          expect(await this.token.totalSupply()).to.be.bignumber.equal('3');
+
+          const tokensListed = await Promise.all(
+            [0, 1, 2].map(i => this.token.tokenByIndex(i))
+          );
+          const expectedTokens = [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter(
+            x => (x !== tokenId)
+          );
+          expect(tokensListed.map(t => t.toNumber())).to.have.members(expectedTokens.map(t => t.toNumber()));
+        });
+      });
+    });
+  });
+
+  describe('_mint(address, uint256)', function () {
+    it('reverts with a null destination address', async function () {
+      await expectRevert(
+        this.token.mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'
+      );
+    });
+
+    context('with minted token', async function () {
+      beforeEach(async function () {
+        ({ logs: this.logs } = await this.token.mint(owner, firstTokenId));
+      });
+
+      it('emits a Transfer event', function () {
+        expectEvent.inLogs(this.logs, 'Transfer', { from: ZERO_ADDRESS, to: owner, tokenId: firstTokenId });
+      });
+
+      it('creates the token', async function () {
+        expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
+        expect(await this.token.ownerOf(firstTokenId)).to.equal(owner);
+      });
+
+      it('adjusts owner tokens by index', async function () {
+        expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId);
+      });
+
+      it('adjusts all tokens list', async function () {
+        expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(firstTokenId);
+      });
+
+      it('reverts when adding a token id that already exists', async function () {
+        await expectRevert(this.token.mint(owner, firstTokenId), 'ERC721: token already minted');
+      });
+    });
+  });
+
+  describe('_burn', function () {
+    it('reverts when burning a non-existent token id', async function () {
+      await expectRevert(
+        this.token.burn(firstTokenId), 'ERC721: owner query for nonexistent token'
+      );
+    });
+
+    context('with minted tokens', function () {
+      beforeEach(async function () {
+        await this.token.mint(owner, firstTokenId);
+        await this.token.mint(owner, secondTokenId);
+      });
+
+      context('with burnt token', function () {
+        beforeEach(async function () {
+          ({ logs: this.logs } = await this.token.burn(firstTokenId));
+        });
+
+        it('emits a Transfer event', function () {
+          expectEvent.inLogs(this.logs, 'Transfer', { from: owner, to: ZERO_ADDRESS, tokenId: firstTokenId });
+        });
+
+        it('emits an Approval event', function () {
+          expectEvent.inLogs(this.logs, 'Approval', { owner, approved: ZERO_ADDRESS, tokenId: firstTokenId });
+        });
+
+        it('deletes the token', async function () {
+          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
+          await expectRevert(
+            this.token.ownerOf(firstTokenId), 'ERC721: owner query for nonexistent token'
+          );
+        });
+
+        it('removes that token from the token list of the owner', async function () {
+          expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(secondTokenId);
+        });
+
+        it('adjusts all tokens list', async function () {
+          expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(secondTokenId);
+        });
+
+        it('burns all tokens', async function () {
+          await this.token.burn(secondTokenId, { from: owner });
+          expect(await this.token.totalSupply()).to.be.bignumber.equal('0');
+          await expectRevert(
+            this.token.tokenByIndex(0), 'EnumerableMap: index out of bounds'
+          );
+        });
+
+        it('reverts when burning a token id that has been deleted', async function () {
+          await expectRevert(
+            this.token.burn(firstTokenId), 'ERC721: owner query for nonexistent token'
+          );
+        });
+      });
+    });
   });
 });

+ 4 - 1
test/token/ERC721/ERC721Burnable.test.js

@@ -14,8 +14,11 @@ describe('ERC721Burnable', function () {
   const secondTokenId = new BN(2);
   const unknownTokenId = new BN(3);
 
+  const name = 'Non Fungible Token';
+  const symbol = 'NFT';
+
   beforeEach(async function () {
-    this.token = await ERC721BurnableMock.new();
+    this.token = await ERC721BurnableMock.new(name, symbol);
   });
 
   describe('like a burnable ERC721', function () {

+ 0 - 249
test/token/ERC721/ERC721Full.test.js

@@ -1,249 +0,0 @@
-const { accounts, contract } = require('@openzeppelin/test-environment');
-
-const { BN, expectRevert } = require('@openzeppelin/test-helpers');
-const { expect } = require('chai');
-
-const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
-const { shouldSupportInterfaces } = require('../../introspection/SupportsInterface.behavior');
-
-const ERC721FullMock = contract.fromArtifact('ERC721FullMock');
-
-describe('ERC721Full', function () {
-  const [owner, newOwner, other ] = accounts;
-
-  const name = 'Non Fungible Token';
-  const symbol = 'NFT';
-  const firstTokenId = new BN(100);
-  const secondTokenId = new BN(200);
-  const thirdTokenId = new BN(300);
-  const nonExistentTokenId = new BN(999);
-
-  beforeEach(async function () {
-    this.token = await ERC721FullMock.new(name, symbol);
-  });
-
-  describe('like a full ERC721', function () {
-    beforeEach(async function () {
-      await this.token.mint(owner, firstTokenId);
-      await this.token.mint(owner, secondTokenId);
-    });
-
-    describe('mint', function () {
-      beforeEach(async function () {
-        await this.token.mint(newOwner, thirdTokenId);
-      });
-
-      it('adjusts owner tokens by index', async function () {
-        expect(await this.token.tokenOfOwnerByIndex(newOwner, 0)).to.be.bignumber.equal(thirdTokenId);
-      });
-
-      it('adjusts all tokens list', async function () {
-        expect(await this.token.tokenByIndex(2)).to.be.bignumber.equal(thirdTokenId);
-      });
-    });
-
-    describe('burn', function () {
-      beforeEach(async function () {
-        await this.token.burn(firstTokenId, { from: owner });
-      });
-
-      it('removes that token from the token list of the owner', async function () {
-        expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(secondTokenId);
-      });
-
-      it('adjusts all tokens list', async function () {
-        expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(secondTokenId);
-      });
-
-      it('burns all tokens', async function () {
-        await this.token.burn(secondTokenId, { from: owner });
-        expect(await this.token.totalSupply()).to.be.bignumber.equal('0');
-        await expectRevert(
-          this.token.tokenByIndex(0), 'ERC721Enumerable: global index out of bounds'
-        );
-      });
-    });
-
-    describe('metadata', function () {
-      it('has a name', async function () {
-        expect(await this.token.name()).to.be.equal(name);
-      });
-
-      it('has a symbol', async function () {
-        expect(await this.token.symbol()).to.be.equal(symbol);
-      });
-
-      describe('token URI', function () {
-        const baseURI = 'https://api.com/v1/';
-        const sampleUri = 'mock://mytoken';
-
-        it('it is empty by default', async function () {
-          expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
-        });
-
-        it('reverts when queried for non existent token id', async function () {
-          await expectRevert(
-            this.token.tokenURI(nonExistentTokenId), 'ERC721Metadata: URI query for nonexistent token'
-          );
-        });
-
-        it('can be set for a token id', async function () {
-          await this.token.setTokenURI(firstTokenId, sampleUri);
-          expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri);
-        });
-
-        it('reverts when setting for non existent token id', async function () {
-          await expectRevert(
-            this.token.setTokenURI(nonExistentTokenId, sampleUri), 'ERC721Metadata: URI set of nonexistent token'
-          );
-        });
-
-        it('base URI can be set', async function () {
-          await this.token.setBaseURI(baseURI);
-          expect(await this.token.baseURI()).to.equal(baseURI);
-        });
-
-        it('base URI is added as a prefix to the token URI', async function () {
-          await this.token.setBaseURI(baseURI);
-          await this.token.setTokenURI(firstTokenId, sampleUri);
-
-          expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri);
-        });
-
-        it('token URI can be changed by changing the base URI', async function () {
-          await this.token.setBaseURI(baseURI);
-          await this.token.setTokenURI(firstTokenId, sampleUri);
-
-          const newBaseURI = 'https://api.com/v2/';
-          await this.token.setBaseURI(newBaseURI);
-          expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri);
-        });
-
-        it('token URI is empty for tokens with no URI but with base URI', async function () {
-          await this.token.setBaseURI(baseURI);
-
-          expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
-        });
-
-        it('tokens with URI can be burnt ', async function () {
-          await this.token.setTokenURI(firstTokenId, sampleUri);
-
-          await this.token.burn(firstTokenId, { from: owner });
-
-          expect(await this.token.exists(firstTokenId)).to.equal(false);
-          await expectRevert(
-            this.token.tokenURI(firstTokenId), 'ERC721Metadata: URI query for nonexistent token'
-          );
-        });
-      });
-    });
-
-    describe('tokensOfOwner', function () {
-      it('returns total tokens of owner', async function () {
-        const tokenIds = await this.token.tokensOfOwner(owner);
-        expect(tokenIds.length).to.equal(2);
-        expect(tokenIds[0]).to.be.bignumber.equal(firstTokenId);
-        expect(tokenIds[1]).to.be.bignumber.equal(secondTokenId);
-      });
-    });
-
-    describe('totalSupply', function () {
-      it('returns total token supply', async function () {
-        expect(await this.token.totalSupply()).to.be.bignumber.equal('2');
-      });
-    });
-
-    describe('tokenOfOwnerByIndex', function () {
-      describe('when the given index is lower than the amount of tokens owned by the given address', function () {
-        it('returns the token ID placed at the given index', async function () {
-          expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId);
-        });
-      });
-
-      describe('when the index is greater than or equal to the total tokens owned by the given address', function () {
-        it('reverts', async function () {
-          await expectRevert(
-            this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721Enumerable: owner index out of bounds'
-          );
-        });
-      });
-
-      describe('when the given address does not own any token', function () {
-        it('reverts', async function () {
-          await expectRevert(
-            this.token.tokenOfOwnerByIndex(other, 0), 'ERC721Enumerable: owner index out of bounds'
-          );
-        });
-      });
-
-      describe('after transferring all tokens to another user', function () {
-        beforeEach(async function () {
-          await this.token.transferFrom(owner, other, firstTokenId, { from: owner });
-          await this.token.transferFrom(owner, other, secondTokenId, { from: owner });
-        });
-
-        it('returns correct token IDs for target', async function () {
-          expect(await this.token.balanceOf(other)).to.be.bignumber.equal('2');
-          const tokensListed = await Promise.all(
-            [0, 1].map(i => this.token.tokenOfOwnerByIndex(other, i))
-          );
-          expect(tokensListed.map(t => t.toNumber())).to.have.members([firstTokenId.toNumber(),
-            secondTokenId.toNumber()]);
-        });
-
-        it('returns empty collection for original owner', async function () {
-          expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0');
-          await expectRevert(
-            this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721Enumerable: owner index out of bounds'
-          );
-        });
-      });
-    });
-
-    describe('tokenByIndex', function () {
-      it('should return all tokens', async function () {
-        const tokensListed = await Promise.all(
-          [0, 1].map(i => this.token.tokenByIndex(i))
-        );
-        expect(tokensListed.map(t => t.toNumber())).to.have.members([firstTokenId.toNumber(),
-          secondTokenId.toNumber()]);
-      });
-
-      it('should revert if index is greater than supply', async function () {
-        await expectRevert(
-          this.token.tokenByIndex(2), 'ERC721Enumerable: global index out of bounds'
-        );
-      });
-
-      [firstTokenId, secondTokenId].forEach(function (tokenId) {
-        it(`should return all tokens after burning token ${tokenId} and minting new tokens`, async function () {
-          const newTokenId = new BN(300);
-          const anotherNewTokenId = new BN(400);
-
-          await this.token.burn(tokenId, { from: owner });
-          await this.token.mint(newOwner, newTokenId);
-          await this.token.mint(newOwner, anotherNewTokenId);
-
-          expect(await this.token.totalSupply()).to.be.bignumber.equal('3');
-
-          const tokensListed = await Promise.all(
-            [0, 1, 2].map(i => this.token.tokenByIndex(i))
-          );
-          const expectedTokens = [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter(
-            x => (x !== tokenId)
-          );
-          expect(tokensListed.map(t => t.toNumber())).to.have.members(expectedTokens.map(t => t.toNumber()));
-        });
-      });
-    });
-  });
-
-  shouldBehaveLikeERC721(accounts);
-
-  shouldSupportInterfaces([
-    'ERC165',
-    'ERC721',
-    'ERC721Enumerable',
-    'ERC721Metadata',
-  ]);
-});

+ 4 - 1
test/token/ERC721/ERC721Holder.test.js

@@ -10,8 +10,11 @@ const ERC721Mock = contract.fromArtifact('ERC721Mock');
 describe('ERC721Holder', function () {
   const [ owner ] = accounts;
 
+  const name = 'Non Fungible Token';
+  const symbol = 'NFT';
+
   it('receives an ERC721 token', async function () {
-    const token = await ERC721Mock.new();
+    const token = await ERC721Mock.new(name, symbol);
     const tokenId = new BN(1);
     await token.mint(owner, tokenId);
 

+ 4 - 7
test/token/ERC721/ERC721Pausable.test.js

@@ -5,19 +5,16 @@ const { ZERO_ADDRESS } = constants;
 
 const { expect } = require('chai');
 
-const { shouldBehaveLikeERC721 } = require('./ERC721.behavior');
-
 const ERC721PausableMock = contract.fromArtifact('ERC721PausableMock');
 
 describe('ERC721Pausable', function () {
   const [ owner, receiver, operator ] = accounts;
 
-  beforeEach(async function () {
-    this.token = await ERC721PausableMock.new();
-  });
+  const name = 'Non Fungible Token';
+  const symbol = 'NFT';
 
-  context('when token is not paused yet', function () {
-    shouldBehaveLikeERC721(accounts);
+  beforeEach(async function () {
+    this.token = await ERC721PausableMock.new(name, symbol);
   });
 
   context('when token is paused', function () {

+ 46 - 16
test/utils/Create2.test.js

@@ -1,20 +1,24 @@
 const { contract, accounts, web3 } = require('@openzeppelin/test-environment');
-const { BN, expectRevert } = require('@openzeppelin/test-helpers');
+const { balance, BN, ether, expectRevert, send } = require('@openzeppelin/test-helpers');
 
 const { expect } = require('chai');
 
 const Create2Impl = contract.fromArtifact('Create2Impl');
 const ERC20Mock = contract.fromArtifact('ERC20Mock');
-const ERC20 = contract.fromArtifact('ERC20');
+const ERC1820Implementer = contract.fromArtifact('ERC1820Implementer');
 
 describe('Create2', function () {
   const [deployerAccount] = accounts;
 
   const salt = 'salt message';
   const saltHex = web3.utils.soliditySha3(salt);
-  const constructorByteCode = `${ERC20Mock.bytecode}${web3.eth.abi
-    .encodeParameters(['address', 'uint256'], [deployerAccount, 100]).slice(2)
-  }`;
+
+  const encodedParams = web3.eth.abi.encodeParameters(
+    ['string', 'string', 'address', 'uint256'],
+    ['MyToken', 'MTKN', deployerAccount, 100]
+  ).slice(2);
+
+  const constructorByteCode = `${ERC20Mock.bytecode}${encodedParams}`;
 
   beforeEach(async function () {
     this.factory = await Create2Impl.new();
@@ -36,27 +40,53 @@ describe('Create2', function () {
     expect(onChainComputed).to.equal(offChainComputed);
   });
 
-  it('should deploy a ERC20 from inline assembly code', async function () {
+  it('should deploy a ERC1820Implementer from inline assembly code', async function () {
     const offChainComputed =
-      computeCreate2Address(saltHex, ERC20.bytecode, this.factory.address);
-    await this.factory
-      .deploy(saltHex, ERC20.bytecode, { from: deployerAccount });
-    expect(ERC20.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2));
+      computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address);
+
+    await this.factory.deployERC1820Implementer(0, saltHex);
+
+    expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2));
   });
 
   it('should deploy a ERC20Mock with correct balances', async function () {
-    const offChainComputed =
-      computeCreate2Address(saltHex, constructorByteCode, this.factory.address);
-    await this.factory
-      .deploy(saltHex, constructorByteCode, { from: deployerAccount });
+    const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address);
+
+    await this.factory.deploy(0, saltHex, constructorByteCode);
+
     const erc20 = await ERC20Mock.at(offChainComputed);
     expect(await erc20.balanceOf(deployerAccount)).to.be.bignumber.equal(new BN(100));
   });
 
+  it('should deploy a contract with funds deposited in the factory', async function () {
+    const deposit = ether('2');
+    await send.ether(deployerAccount, this.factory.address, deposit);
+    expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit);
+
+    const onChainComputed = await this.factory
+      .computeAddressWithDeployer(saltHex, web3.utils.keccak256(constructorByteCode), this.factory.address);
+
+    await this.factory.deploy(deposit, saltHex, constructorByteCode);
+    expect(await balance.current(onChainComputed)).to.be.bignumber.equal(deposit);
+  });
+
   it('should failed deploying a contract in an existent address', async function () {
-    await this.factory.deploy(saltHex, constructorByteCode, { from: deployerAccount });
+    await this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount });
+    await expectRevert(
+      this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount }), 'Create2: Failed on deploy'
+    );
+  });
+
+  it('should fail deploying a contract if the bytecode length is zero', async function () {
+    await expectRevert(
+      this.factory.deploy(0, saltHex, '0x', { from: deployerAccount }), 'Create2: bytecode length is zero'
+    );
+  });
+
+  it('should fail deploying a contract if factory contract does not have sufficient balance', async function () {
     await expectRevert(
-      this.factory.deploy(saltHex, constructorByteCode, { from: deployerAccount }), 'Create2: Failed on deploy'
+      this.factory.deploy(1, saltHex, constructorByteCode, { from: deployerAccount }),
+      'Create2: insufficient balance'
     );
   });
 });

+ 139 - 0
test/utils/EnumerableMap.test.js

@@ -0,0 +1,139 @@
+const { accounts, contract } = require('@openzeppelin/test-environment');
+const { BN, expectEvent } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+
+const zip = require('lodash.zip');
+
+const EnumerableMapMock = contract.fromArtifact('EnumerableMapMock');
+
+describe('EnumerableMap', function () {
+  const [ accountA, accountB, accountC ] = accounts;
+
+  const keyA = new BN('7891');
+  const keyB = new BN('451');
+  const keyC = new BN('9592328');
+
+  beforeEach(async function () {
+    this.map = await EnumerableMapMock.new();
+  });
+
+  async function expectMembersMatch (map, keys, values) {
+    expect(keys.length).to.equal(values.length);
+
+    await Promise.all(keys.map(async key =>
+      expect(await map.contains(key)).to.equal(true)
+    ));
+
+    expect(await map.length()).to.bignumber.equal(keys.length.toString());
+
+    expect(await Promise.all(keys.map(key =>
+      map.get(key)
+    ))).to.have.same.members(values);
+
+    // To compare key-value pairs, we zip keys and values, and convert BNs to
+    // strings to workaround Chai limitations when dealing with nested arrays
+    expect(await Promise.all([...Array(keys.length).keys()].map(async (index) => {
+      const entry = await map.at(index);
+      return [entry.key.toString(), entry.value];
+    }))).to.have.same.deep.members(
+      zip(keys.map(k => k.toString()), values)
+    );
+  }
+
+  it('starts empty', async function () {
+    expect(await this.map.contains(keyA)).to.equal(false);
+
+    await expectMembersMatch(this.map, [], []);
+  });
+
+  it('adds a key', async function () {
+    const receipt = await this.map.set(keyA, accountA);
+    expectEvent(receipt, 'OperationResult', { result: true });
+
+    await expectMembersMatch(this.map, [keyA], [accountA]);
+  });
+
+  it('adds several keys', async function () {
+    await this.map.set(keyA, accountA);
+    await this.map.set(keyB, accountB);
+
+    await expectMembersMatch(this.map, [keyA, keyB], [accountA, accountB]);
+    expect(await this.map.contains(keyC)).to.equal(false);
+  });
+
+  it('returns false when adding keys already in the set', async function () {
+    await this.map.set(keyA, accountA);
+
+    const receipt = (await this.map.set(keyA, accountA));
+    expectEvent(receipt, 'OperationResult', { result: false });
+
+    await expectMembersMatch(this.map, [keyA], [accountA]);
+  });
+
+  it('updates values for keys already in the set', async function () {
+    await this.map.set(keyA, accountA);
+
+    await this.map.set(keyA, accountB);
+
+    await expectMembersMatch(this.map, [keyA], [accountB]);
+  });
+
+  it('removes added keys', async function () {
+    await this.map.set(keyA, accountA);
+
+    const receipt = await this.map.remove(keyA);
+    expectEvent(receipt, 'OperationResult', { result: true });
+
+    expect(await this.map.contains(keyA)).to.equal(false);
+    await expectMembersMatch(this.map, [], []);
+  });
+
+  it('returns false when removing keys not in the set', async function () {
+    const receipt = await this.map.remove(keyA);
+    expectEvent(receipt, 'OperationResult', { result: false });
+
+    expect(await this.map.contains(keyA)).to.equal(false);
+  });
+
+  it('adds and removes multiple keys', async function () {
+    // []
+
+    await this.map.set(keyA, accountA);
+    await this.map.set(keyC, accountC);
+
+    // [A, C]
+
+    await this.map.remove(keyA);
+    await this.map.remove(keyB);
+
+    // [C]
+
+    await this.map.set(keyB, accountB);
+
+    // [C, B]
+
+    await this.map.set(keyA, accountA);
+    await this.map.remove(keyC);
+
+    // [A, B]
+
+    await this.map.set(keyA, accountA);
+    await this.map.set(keyB, accountB);
+
+    // [A, B]
+
+    await this.map.set(keyC, accountC);
+    await this.map.remove(keyA);
+
+    // [B, C]
+
+    await this.map.set(keyA, accountA);
+    await this.map.remove(keyB);
+
+    // [A, C]
+
+    await expectMembersMatch(this.map, [keyA, keyC], [accountA, accountC]);
+
+    expect(await this.map.contains(keyB)).to.equal(false);
+  });
+});

+ 17 - 15
test/utils/EnumerableSet.test.js

@@ -1,5 +1,5 @@
 const { accounts, contract } = require('@openzeppelin/test-environment');
-const { expectEvent } = require('@openzeppelin/test-helpers');
+const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 
 const EnumerableSetMock = contract.fromArtifact('EnumerableSetMock');
@@ -11,18 +11,16 @@ describe('EnumerableSet', function () {
     this.set = await EnumerableSetMock.new();
   });
 
-  async function expectMembersMatch (set, members) {
-    await Promise.all(members.map(async account =>
+  async function expectMembersMatch (set, values) {
+    await Promise.all(values.map(async account =>
       expect(await set.contains(account)).to.equal(true)
     ));
 
-    expect(await set.enumerate()).to.have.same.members(members);
+    expect(await set.length()).to.bignumber.equal(values.length.toString());
 
-    expect(await set.length()).to.bignumber.equal(members.length.toString());
-
-    expect(await Promise.all([...Array(members.length).keys()].map(index =>
-      set.get(index)
-    ))).to.have.same.members(members);
+    expect(await Promise.all([...Array(values.length).keys()].map(index =>
+      set.at(index)
+    ))).to.have.same.members(values);
   }
 
   it('starts empty', async function () {
@@ -33,7 +31,7 @@ describe('EnumerableSet', function () {
 
   it('adds a value', async function () {
     const receipt = await this.set.add(accountA);
-    expectEvent(receipt, 'TransactionResult', { result: true });
+    expectEvent(receipt, 'OperationResult', { result: true });
 
     await expectMembersMatch(this.set, [accountA]);
   });
@@ -46,28 +44,32 @@ describe('EnumerableSet', function () {
     expect(await this.set.contains(accountC)).to.equal(false);
   });
 
-  it('returns false when adding elements already in the set', async function () {
+  it('returns false when adding values already in the set', async function () {
     await this.set.add(accountA);
 
     const receipt = (await this.set.add(accountA));
-    expectEvent(receipt, 'TransactionResult', { result: false });
+    expectEvent(receipt, 'OperationResult', { result: false });
 
     await expectMembersMatch(this.set, [accountA]);
   });
 
+  it('reverts when retrieving non-existent elements', async function () {
+    await expectRevert(this.set.at(0), 'EnumerableSet: index out of bounds');
+  });
+
   it('removes added values', async function () {
     await this.set.add(accountA);
 
     const receipt = await this.set.remove(accountA);
-    expectEvent(receipt, 'TransactionResult', { result: true });
+    expectEvent(receipt, 'OperationResult', { result: true });
 
     expect(await this.set.contains(accountA)).to.equal(false);
     await expectMembersMatch(this.set, []);
   });
 
-  it('returns false when removing elements not in the set', async function () {
+  it('returns false when removing values not in the set', async function () {
     const receipt = await this.set.remove(accountA);
-    expectEvent(receipt, 'TransactionResult', { result: false });
+    expectEvent(receipt, 'OperationResult', { result: false });
 
     expect(await this.set.contains(accountA)).to.equal(false);
   });

+ 70 - 0
test/utils/SafeCast.test.js

@@ -43,4 +43,74 @@ describe('SafeCast', async () => {
   }
 
   [8, 16, 32, 64, 128].forEach(bits => testToUint(bits));
+
+  describe('toUint256', () => {
+    const maxInt256 = new BN('2').pow(new BN(255)).subn(1);
+    const minInt256 = new BN('2').pow(new BN(255)).neg();
+    const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
+
+    it('casts 0', async function () {
+      expect(await this.safeCast.toUint256(0)).to.be.bignumber.equal('0');
+    });
+
+    it('casts 1', async function () {
+      expect(await this.safeCast.toUint256(1)).to.be.bignumber.equal('1');
+    });
+
+    it(`casts INT256_MAX (${maxInt256})`, async function () {
+      expect(await this.safeCast.toUint256(maxInt256)).to.be.bignumber.equal(maxInt256);
+    });
+
+    it('reverts when casting -1', async function () {
+      await expectRevert(
+        this.safeCast.toUint256(-1),
+        'SafeCast: value must be positive'
+      );
+    });
+
+    it(`reverts when casting INT256_MIN (${minInt256})`, async function () {
+      await expectRevert(
+        this.safeCast.toUint256(minInt256),
+        'SafeCast: value must be positive'
+      );
+    });
+
+    it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () {
+      await expectRevert(
+        this.safeCast.toUint256(maxUint256),
+        'SafeCast: value must be positive'
+      );
+    });
+  });
+
+  describe('toInt256', () => {
+    const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
+    const maxInt256 = new BN('2').pow(new BN(255)).subn(1);
+
+    it('casts 0', async function () {
+      expect(await this.safeCast.toInt256(0)).to.be.bignumber.equal('0');
+    });
+
+    it('casts 1', async function () {
+      expect(await this.safeCast.toInt256(1)).to.be.bignumber.equal('1');
+    });
+
+    it(`casts INT256_MAX (${maxInt256})`, async function () {
+      expect(await this.safeCast.toInt256(maxInt256)).to.be.bignumber.equal(maxInt256);
+    });
+
+    it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () {
+      await expectRevert(
+        this.safeCast.toInt256(maxInt256.addn(1)),
+        'SafeCast: value doesn\'t fit in an int256'
+      );
+    });
+
+    it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () {
+      await expectRevert(
+        this.safeCast.toInt256(maxUint256),
+        'SafeCast: value doesn\'t fit in an int256'
+      );
+    });
+  });
 });

Some files were not shown because too many files changed in this diff