Browse Source

Split ERC20Votes and ERC20VotesComp (#2706)

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

+ 5 - 4
CHANGELOG.md

@@ -2,18 +2,19 @@
 
 
 ## Unreleased
 ## Unreleased
 
 
- * `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. This extension is compatible with Compound's `Comp` token interface. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632))
+ * `ERC20Votes`: add a new extension of the `ERC20` token with support for voting snapshots and delegation. ([#2632](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2632))
+ * `ERC20VotesComp`: Variant of `ERC20Votes` that is compatible with Compound's `Comp` token interface but restricts supply to `uint96`. ([#2706](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2706))
  * Enumerables: Improve gas cost of removal in `EnumerableSet` and `EnumerableMap`.
  * Enumerables: Improve gas cost of removal in `EnumerableSet` and `EnumerableMap`.
  * Enumerables: Improve gas cost of lookup in `EnumerableSet` and `EnumerableMap`.
  * Enumerables: Improve gas cost of lookup in `EnumerableSet` and `EnumerableMap`.
  * `Counter`: add a reset method. ([#2678](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2678))
  * `Counter`: add a reset method. ([#2678](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2678))
- * Tokens: Wrap definitely safe subtractions in `unchecked` blocks. 
+ * Tokens: Wrap definitely safe subtractions in `unchecked` blocks.
  * `Math`: Add a `ceilDiv` method for performing ceiling division.
  * `Math`: Add a `ceilDiv` method for performing ceiling division.
  * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593))
  * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593))
 
 
  ### Breaking Changes
  ### Breaking Changes
- 
+
  * `ERC20FlashMint` is no longer a Draft ERC. ([#2673](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2673)))
  * `ERC20FlashMint` is no longer a Draft ERC. ([#2673](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2673)))
- 
+
 **How to update:** Change your import paths by removing the `draft-` prefix from `@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20FlashMint.sol`.
 **How to update:** Change your import paths by removing the `draft-` prefix from `@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20FlashMint.sol`.
 
 
 > See [Releases and Stability: Drafts](https://docs.openzeppelin.com/contracts/4.x/releases-stability#drafts).
 > See [Releases and Stability: Drafts](https://docs.openzeppelin.com/contracts/4.x/releases-stability#drafts).

+ 25 - 0
contracts/mocks/ERC20VotesCompMock.sol

@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+
+import "../token/ERC20/extensions/ERC20VotesComp.sol";
+
+contract ERC20VotesCompMock is ERC20VotesComp {
+    constructor (string memory name, string memory symbol)
+    ERC20(name, symbol)
+    ERC20Permit(name)
+    {}
+
+    function mint(address account, uint256 amount) public {
+        _mint(account, amount);
+    }
+
+    function burn(address account, uint256 amount) public {
+        _burn(account, amount);
+    }
+
+    function getChainId() external view returns (uint256) {
+        return block.chainid;
+    }
+}

+ 10 - 2
contracts/mocks/SafeCastMock.sol

@@ -12,14 +12,18 @@ contract SafeCastMock {
         return a.toUint256();
         return a.toUint256();
     }
     }
 
 
-    function toInt256(uint a) public pure returns (int256) {
-        return a.toInt256();
+    function toUint224(uint a) public pure returns (uint224) {
+        return a.toUint224();
     }
     }
 
 
     function toUint128(uint a) public pure returns (uint128) {
     function toUint128(uint a) public pure returns (uint128) {
         return a.toUint128();
         return a.toUint128();
     }
     }
 
 
+    function toUint96(uint a) public pure returns (uint96) {
+        return a.toUint96();
+    }
+
     function toUint64(uint a) public pure returns (uint64) {
     function toUint64(uint a) public pure returns (uint64) {
         return a.toUint64();
         return a.toUint64();
     }
     }
@@ -36,6 +40,10 @@ contract SafeCastMock {
         return a.toUint8();
         return a.toUint8();
     }
     }
 
 
+    function toInt256(uint a) public pure returns (int256) {
+        return a.toInt256();
+    }
+
     function toInt128(int a) public pure returns (int128) {
     function toInt128(int a) public pure returns (int128) {
         return a.toInt128();
         return a.toInt128();
     }
     }

+ 6 - 4
contracts/token/ERC20/README.adoc

@@ -21,7 +21,8 @@ Additionally there are multiple custom extensions, including:
 * {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time.
 * {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time.
 * {ERC20Permit}: gasless approval of tokens (standardized as ERC2612).
 * {ERC20Permit}: gasless approval of tokens (standardized as ERC2612).
 * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156).
 * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156).
-* {ERC20Votes}: support for voting and vote delegation (compatible with Compound's token).
+* {ERC20Votes}: support for voting and vote delegation.
+* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's tokenn, with uint96 restrictions).
 
 
 Finally, there are some utilities to interact with ERC20 contracts in various ways.
 Finally, there are some utilities to interact with ERC20 contracts in various ways.
 
 
@@ -32,7 +33,6 @@ The following related EIPs are in draft status.
 
 
 - {ERC20Permit}
 - {ERC20Permit}
 - {ERC20FlashMint}
 - {ERC20FlashMint}
-- {ERC20Votes}
 
 
 NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <<ERC20-_mint-address-uint256-,`_mint`>>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts.
 NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <<ERC20-_mint-address-uint256-,`_mint`>>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc20.adoc#Presets[ERC20 Presets] (such as {ERC20PresetMinterPauser}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts.
 
 
@@ -54,6 +54,10 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 
 
 {{ERC20Snapshot}}
 {{ERC20Snapshot}}
 
 
+{{ERC20Votes}}
+
+{{ERC20VotesComp}}
+
 == Draft EIPs
 == Draft EIPs
 
 
 The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
 The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
@@ -62,8 +66,6 @@ The following EIPs are still in Draft status. Due to their nature as drafts, the
 
 
 {{ERC20FlashMint}}
 {{ERC20FlashMint}}
 
 
-{{ERC20Votes}}
-
 == Presets
 == Presets
 
 
 These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code.
 These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code.

+ 45 - 17
contracts/token/ERC20/extensions/ERC20Votes.sol

@@ -3,17 +3,19 @@
 pragma solidity ^0.8.0;
 pragma solidity ^0.8.0;
 
 
 import "./draft-ERC20Permit.sol";
 import "./draft-ERC20Permit.sol";
-import "./IERC20Votes.sol";
 import "../../../utils/math/Math.sol";
 import "../../../utils/math/Math.sol";
 import "../../../utils/math/SafeCast.sol";
 import "../../../utils/math/SafeCast.sol";
 import "../../../utils/cryptography/ECDSA.sol";
 import "../../../utils/cryptography/ECDSA.sol";
 
 
 /**
 /**
- * @dev Extension of the ERC20 token contract to support Compound's voting and delegation.
+ * @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's,
+ * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1.
  *
  *
- * This extensions keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either
+ * NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module.
+ *
+ * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either
  * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting
  * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting
- * power can be queried through the public accessors {getCurrentVotes} and {getPriorVotes}.
+ * power can be queried through the public accessors {getVotes} and {getPastVotes}.
  *
  *
  * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it
  * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it
  * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked.
  * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked.
@@ -22,38 +24,53 @@ import "../../../utils/cryptography/ECDSA.sol";
  *
  *
  * _Available since v4.2._
  * _Available since v4.2._
  */
  */
-abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
+abstract contract ERC20Votes is ERC20Permit {
+    struct Checkpoint {
+        uint32  fromBlock;
+        uint224 votes;
+    }
+
     bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
     bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
 
 
     mapping (address => address) private _delegates;
     mapping (address => address) private _delegates;
     mapping (address => Checkpoint[]) private _checkpoints;
     mapping (address => Checkpoint[]) private _checkpoints;
     Checkpoint[] private _totalSupplyCheckpoints;
     Checkpoint[] private _totalSupplyCheckpoints;
 
 
+    /**
+     * @dev Emitted when an account changes their delegate.
+     */
+    event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
+
+    /**
+     * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power.
+     */
+    event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
+
     /**
     /**
      * @dev Get the `pos`-th checkpoint for `account`.
      * @dev Get the `pos`-th checkpoint for `account`.
      */
      */
-    function checkpoints(address account, uint32 pos) external view virtual override returns (Checkpoint memory) {
+    function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) {
         return _checkpoints[account][pos];
         return _checkpoints[account][pos];
     }
     }
 
 
     /**
     /**
      * @dev Get number of checkpoints for `account`.
      * @dev Get number of checkpoints for `account`.
      */
      */
-    function numCheckpoints(address account) external view virtual override returns (uint32) {
+    function numCheckpoints(address account) public view virtual returns (uint32) {
         return SafeCast.toUint32(_checkpoints[account].length);
         return SafeCast.toUint32(_checkpoints[account].length);
     }
     }
 
 
     /**
     /**
      * @dev Get the address `account` is currently delegating to.
      * @dev Get the address `account` is currently delegating to.
      */
      */
-    function delegates(address account) public view virtual override returns (address) {
+    function delegates(address account) public view virtual returns (address) {
         return _delegates[account];
         return _delegates[account];
     }
     }
 
 
     /**
     /**
      * @dev Gets the current votes balance for `account`
      * @dev Gets the current votes balance for `account`
      */
      */
-    function getCurrentVotes(address account) external view override returns (uint256) {
+    function getVotes(address account) public view returns (uint256) {
         uint256 pos = _checkpoints[account].length;
         uint256 pos = _checkpoints[account].length;
         return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;
         return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;
     }
     }
@@ -65,7 +82,7 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
      *
      *
      * - `blockNumber` must have been already mined
      * - `blockNumber` must have been already mined
      */
      */
-    function getPriorVotes(address account, uint256 blockNumber) external view override returns (uint256) {
+    function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
         require(blockNumber < block.number, "ERC20Votes: block not yet mined");
         require(blockNumber < block.number, "ERC20Votes: block not yet mined");
         return _checkpointsLookup(_checkpoints[account], blockNumber);
         return _checkpointsLookup(_checkpoints[account], blockNumber);
     }
     }
@@ -78,7 +95,7 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
      *
      *
      * - `blockNumber` must have been already mined
      * - `blockNumber` must have been already mined
      */
      */
-    function getPriorTotalSupply(uint256 blockNumber) external view override returns (uint256) {
+    function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) {
         require(blockNumber < block.number, "ERC20Votes: block not yet mined");
         require(blockNumber < block.number, "ERC20Votes: block not yet mined");
         return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
         return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
     }
     }
@@ -115,7 +132,7 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
     /**
     /**
      * @dev Delegate votes from the sender to `delegatee`.
      * @dev Delegate votes from the sender to `delegatee`.
      */
      */
-    function delegate(address delegatee) public virtual override {
+    function delegate(address delegatee) public virtual {
         return _delegate(_msgSender(), delegatee);
         return _delegate(_msgSender(), delegatee);
     }
     }
 
 
@@ -123,7 +140,7 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
      * @dev Delegates votes from signer to `delegatee`
      * @dev Delegates votes from signer to `delegatee`
      */
      */
     function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
     function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
-        public virtual override
+        public virtual
     {
     {
         require(block.timestamp <= expiry, "ERC20Votes: signature expired");
         require(block.timestamp <= expiry, "ERC20Votes: signature expired");
         address signer = ECDSA.recover(
         address signer = ECDSA.recover(
@@ -140,17 +157,24 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
     }
     }
 
 
     /**
     /**
-     * @dev snapshot the totalSupply after it has been increassed.
+     * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1).
+     */
+    function _maxSupply() internal view virtual returns (uint224) {
+        return type(uint224).max;
+    }
+
+    /**
+     * @dev Snapshots the totalSupply after it has been increased.
      */
      */
     function _mint(address account, uint256 amount) internal virtual override {
     function _mint(address account, uint256 amount) internal virtual override {
         super._mint(account, amount);
         super._mint(account, amount);
-        require(totalSupply() <= type(uint224).max, "ERC20Votes: total supply exceeds 2**224");
+        require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes");
 
 
         _writeCheckpoint(_totalSupplyCheckpoints, add, amount);
         _writeCheckpoint(_totalSupplyCheckpoints, add, amount);
     }
     }
 
 
     /**
     /**
-     * @dev snapshot the totalSupply after it has been decreased.
+     * @dev Snapshots the totalSupply after it has been decreased.
      */
      */
     function _burn(address account, uint256 amount) internal virtual override {
     function _burn(address account, uint256 amount) internal virtual override {
         super._burn(account, amount);
         super._burn(account, amount);
@@ -159,7 +183,9 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
     }
     }
 
 
     /**
     /**
-     * @dev move voting power when tokens are transferred.
+     * @dev Move voting power when tokens are transferred.
+     *
+     * Emits a {DelegateVotesChanged} event.
      */
      */
     function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
     function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
         _moveVotingPower(delegates(from), delegates(to), amount);
         _moveVotingPower(delegates(from), delegates(to), amount);
@@ -167,6 +193,8 @@ abstract contract ERC20Votes is IERC20Votes, ERC20Permit {
 
 
     /**
     /**
      * @dev Change delegation for `delegator` to `delegatee`.
      * @dev Change delegation for `delegator` to `delegatee`.
+     *
+     * Emits events {DelegateChanged} and {DelegateVotesChanged}.
      */
      */
     function _delegate(address delegator, address delegatee) internal virtual {
     function _delegate(address delegator, address delegatee) internal virtual {
         address currentDelegate = delegates(delegator);
         address currentDelegate = delegates(delegator);

+ 46 - 0
contracts/token/ERC20/extensions/ERC20VotesComp.sol

@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "./ERC20Votes.sol";
+
+/**
+ * @dev Extension of ERC20 to support Compound's voting and delegation. This version exactly matches Compound's
+ * interface, with the drawback of only supporting supply up to (2^96^ - 1).
+ *
+ * NOTE: You should use this contract if you need exact compatibility with COMP (for example in order to use your token
+ * with Governor Alpha or Bravo) and if you are sure the supply cap of 2^96^ is enough for you. Otherwise, use the
+ * {ERC20Votes} variant of this module.
+ *
+ * This extensions keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either
+ * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting
+ * power can be queried through the public accessors {getCurrentVotes} and {getPriorVotes}.
+ *
+ * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it
+ * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked.
+ * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this
+ * will significantly increase the base gas cost of transfers.
+ *
+ * _Available since v4.2._
+ */
+abstract contract ERC20VotesComp is ERC20Votes {
+    /**
+     * @dev Comp version of the {getVotes} accessor, with `uint96` return type.
+     */
+    function getCurrentVotes(address account) external view returns (uint96) {
+        return SafeCast.toUint96(getVotes(account));
+    }
+    /**
+     * @dev Comp version of the {getPastVotes} accessor, with `uint96` return type.
+     */
+    function getPriorVotes(address account, uint256 blockNumber) external view returns (uint96) {
+        return SafeCast.toUint96(getPastVotes(account, blockNumber));
+    }
+
+    /**
+     * @dev Maximum token supply. Reduced to `type(uint96).max` (2^96^ - 1) to fit COMP interface.
+     */
+    function _maxSupply() internal view virtual override returns (uint224) {
+        return type(uint96).max;
+    }
+}

+ 0 - 24
contracts/token/ERC20/extensions/IERC20Votes.sol

@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-pragma solidity ^0.8.0;
-
-import "../IERC20.sol";
-
-interface IERC20Votes is IERC20 {
-    struct Checkpoint {
-        uint32  fromBlock;
-        uint224 votes;
-    }
-
-    event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);
-    event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
-
-    function delegates(address owner) external view returns (address);
-    function checkpoints(address account, uint32 pos) external view returns (Checkpoint memory);
-    function numCheckpoints(address account) external view returns (uint32);
-    function getCurrentVotes(address account) external view returns (uint256);
-    function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256);
-    function getPriorTotalSupply(uint256 blockNumber) external view returns(uint256);
-    function delegate(address delegatee) external;
-    function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) external;
-}

+ 28 - 12
contracts/utils/math/SafeCast.sol

@@ -29,7 +29,7 @@ library SafeCast {
      * - input must fit into 224 bits
      * - input must fit into 224 bits
      */
      */
     function toUint224(uint256 value) internal pure returns (uint224) {
     function toUint224(uint256 value) internal pure returns (uint224) {
-        require(value < 2**224, "SafeCast: value doesn\'t fit in 224 bits");
+        require(value <= type(uint224).max, "SafeCast: value doesn\'t fit in 224 bits");
         return uint224(value);
         return uint224(value);
     }
     }
 
 
@@ -44,10 +44,25 @@ library SafeCast {
      * - input must fit into 128 bits
      * - input must fit into 128 bits
      */
      */
     function toUint128(uint256 value) internal pure returns (uint128) {
     function toUint128(uint256 value) internal pure returns (uint128) {
-        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
+        require(value <= type(uint128).max, "SafeCast: value doesn\'t fit in 128 bits");
         return uint128(value);
         return uint128(value);
     }
     }
 
 
+    /**
+     * @dev Returns the downcasted uint96 from uint256, reverting on
+     * overflow (when the input is greater than largest uint96).
+     *
+     * Counterpart to Solidity's `uint96` operator.
+     *
+     * Requirements:
+     *
+     * - input must fit into 96 bits
+     */
+    function toUint96(uint256 value) internal pure returns (uint96) {
+        require(value <= type(uint96).max, "SafeCast: value doesn\'t fit in 96 bits");
+        return uint96(value);
+    }
+
     /**
     /**
      * @dev Returns the downcasted uint64 from uint256, reverting on
      * @dev Returns the downcasted uint64 from uint256, reverting on
      * overflow (when the input is greater than largest uint64).
      * overflow (when the input is greater than largest uint64).
@@ -59,7 +74,7 @@ library SafeCast {
      * - input must fit into 64 bits
      * - input must fit into 64 bits
      */
      */
     function toUint64(uint256 value) internal pure returns (uint64) {
     function toUint64(uint256 value) internal pure returns (uint64) {
-        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
+        require(value <= type(uint64).max, "SafeCast: value doesn\'t fit in 64 bits");
         return uint64(value);
         return uint64(value);
     }
     }
 
 
@@ -74,7 +89,7 @@ library SafeCast {
      * - input must fit into 32 bits
      * - input must fit into 32 bits
      */
      */
     function toUint32(uint256 value) internal pure returns (uint32) {
     function toUint32(uint256 value) internal pure returns (uint32) {
-        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
+        require(value <= type(uint32).max, "SafeCast: value doesn\'t fit in 32 bits");
         return uint32(value);
         return uint32(value);
     }
     }
 
 
@@ -89,7 +104,7 @@ library SafeCast {
      * - input must fit into 16 bits
      * - input must fit into 16 bits
      */
      */
     function toUint16(uint256 value) internal pure returns (uint16) {
     function toUint16(uint256 value) internal pure returns (uint16) {
-        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
+        require(value <= type(uint16).max, "SafeCast: value doesn\'t fit in 16 bits");
         return uint16(value);
         return uint16(value);
     }
     }
 
 
@@ -104,7 +119,7 @@ library SafeCast {
      * - input must fit into 8 bits.
      * - input must fit into 8 bits.
      */
      */
     function toUint8(uint256 value) internal pure returns (uint8) {
     function toUint8(uint256 value) internal pure returns (uint8) {
-        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
+        require(value <= type(uint8).max, "SafeCast: value doesn\'t fit in 8 bits");
         return uint8(value);
         return uint8(value);
     }
     }
 
 
@@ -134,7 +149,7 @@ library SafeCast {
      * _Available since v3.1._
      * _Available since v3.1._
      */
      */
     function toInt128(int256 value) internal pure returns (int128) {
     function toInt128(int256 value) internal pure returns (int128) {
-        require(value >= -2**127 && value < 2**127, "SafeCast: value doesn\'t fit in 128 bits");
+        require(value >= type(int128).min && value <= type(int128).max, "SafeCast: value doesn\'t fit in 128 bits");
         return int128(value);
         return int128(value);
     }
     }
 
 
@@ -152,7 +167,7 @@ library SafeCast {
      * _Available since v3.1._
      * _Available since v3.1._
      */
      */
     function toInt64(int256 value) internal pure returns (int64) {
     function toInt64(int256 value) internal pure returns (int64) {
-        require(value >= -2**63 && value < 2**63, "SafeCast: value doesn\'t fit in 64 bits");
+        require(value >= type(int64).min && value <= type(int64).max, "SafeCast: value doesn\'t fit in 64 bits");
         return int64(value);
         return int64(value);
     }
     }
 
 
@@ -170,7 +185,7 @@ library SafeCast {
      * _Available since v3.1._
      * _Available since v3.1._
      */
      */
     function toInt32(int256 value) internal pure returns (int32) {
     function toInt32(int256 value) internal pure returns (int32) {
-        require(value >= -2**31 && value < 2**31, "SafeCast: value doesn\'t fit in 32 bits");
+        require(value >= type(int32).min && value <= type(int32).max, "SafeCast: value doesn\'t fit in 32 bits");
         return int32(value);
         return int32(value);
     }
     }
 
 
@@ -188,7 +203,7 @@ library SafeCast {
      * _Available since v3.1._
      * _Available since v3.1._
      */
      */
     function toInt16(int256 value) internal pure returns (int16) {
     function toInt16(int256 value) internal pure returns (int16) {
-        require(value >= -2**15 && value < 2**15, "SafeCast: value doesn\'t fit in 16 bits");
+        require(value >= type(int16).min && value <= type(int16).max, "SafeCast: value doesn\'t fit in 16 bits");
         return int16(value);
         return int16(value);
     }
     }
 
 
@@ -206,7 +221,7 @@ library SafeCast {
      * _Available since v3.1._
      * _Available since v3.1._
      */
      */
     function toInt8(int256 value) internal pure returns (int8) {
     function toInt8(int256 value) internal pure returns (int8) {
-        require(value >= -2**7 && value < 2**7, "SafeCast: value doesn\'t fit in 8 bits");
+        require(value >= type(int8).min && value <= type(int8).max, "SafeCast: value doesn\'t fit in 8 bits");
         return int8(value);
         return int8(value);
     }
     }
 
 
@@ -218,7 +233,8 @@ library SafeCast {
      * - input must be less than or equal to maxInt256.
      * - input must be less than or equal to maxInt256.
      */
      */
     function toInt256(uint256 value) internal pure returns (int256) {
     function toInt256(uint256 value) internal pure returns (int256) {
-        require(value < 2**255, "SafeCast: value doesn't fit in an int256");
+        // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
+        require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256");
         return int256(value);
         return int256(value);
     }
     }
 }
 }

+ 54 - 54
test/token/ERC20/extensions/ERC20Votes.test.js

@@ -85,7 +85,7 @@ contract('ERC20Votes', function (accounts) {
     const amount = new BN('2').pow(new BN('224'));
     const amount = new BN('2').pow(new BN('224'));
     await expectRevert(
     await expectRevert(
       this.token.mint(holder, amount),
       this.token.mint(holder, amount),
-      'ERC20Votes: total supply exceeds 2**224',
+      'ERC20Votes: total supply risks overflowing votes',
     );
     );
   });
   });
 
 
@@ -109,10 +109,10 @@ contract('ERC20Votes', function (accounts) {
 
 
         expect(await this.token.delegates(holder)).to.be.equal(holder);
         expect(await this.token.delegates(holder)).to.be.equal(holder);
 
 
-        expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
         await time.advanceBlock();
         await time.advanceBlock();
-        expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply);
       });
       });
 
 
       it('delegation without balance', async function () {
       it('delegation without balance', async function () {
@@ -172,10 +172,10 @@ contract('ERC20Votes', function (accounts) {
 
 
         expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
         expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
 
 
-        expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
         await time.advanceBlock();
         await time.advanceBlock();
-        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply);
       });
       });
 
 
       it('rejects reused signature', async function () {
       it('rejects reused signature', async function () {
@@ -275,13 +275,13 @@ contract('ERC20Votes', function (accounts) {
 
 
       expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
       expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
 
 
-      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
-      expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0');
+      expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
       await time.advanceBlock();
       await time.advanceBlock();
-      expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0');
-      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply);
     });
     });
   });
   });
 
 
@@ -335,14 +335,14 @@ contract('ERC20Votes', function (accounts) {
     });
     });
 
 
     afterEach(async function () {
     afterEach(async function () {
-      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
+      expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes);
+      expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
 
 
-      // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
+      // need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
       const blockNumber = await time.latestBlock();
       const blockNumber = await time.latestBlock();
       await time.advanceBlock();
       await time.advanceBlock();
-      expect(await this.token.getPriorVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getPriorVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes);
+      expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes);
+      expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes);
     });
     });
   });
   });
 
 
@@ -381,10 +381,10 @@ contract('ERC20Votes', function (accounts) {
         expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]);
         expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]);
 
 
         await time.advanceBlock();
         await time.advanceBlock();
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100');
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90');
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80');
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100');
+        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90');
+        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80');
+        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100');
       });
       });
 
 
       it('does not add more than one checkpoint in a block', async function () {
       it('does not add more than one checkpoint in a block', async function () {
@@ -407,16 +407,16 @@ contract('ERC20Votes', function (accounts) {
       });
       });
     });
     });
 
 
-    describe('getPriorVotes', function () {
+    describe('getPastVotes', function () {
       it('reverts if block number >= current block', async function () {
       it('reverts if block number >= current block', async function () {
         await expectRevert(
         await expectRevert(
-          this.token.getPriorVotes(other1, 5e10),
+          this.token.getPastVotes(other1, 5e10),
           'ERC20Votes: block not yet mined',
           'ERC20Votes: block not yet mined',
         );
         );
       });
       });
 
 
       it('returns 0 if there are no checkpoints', async function () {
       it('returns 0 if there are no checkpoints', async function () {
-        expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
+        expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0');
       });
       });
 
 
       it('returns the latest block if >= last checkpoint block', async function () {
       it('returns the latest block if >= last checkpoint block', async function () {
@@ -424,8 +424,8 @@ contract('ERC20Votes', function (accounts) {
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
 
 
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
       });
       });
 
 
       it('returns zero if < first checkpoint block', async function () {
       it('returns zero if < first checkpoint block', async function () {
@@ -434,8 +434,8 @@ contract('ERC20Votes', function (accounts) {
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
 
 
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
       });
       });
 
 
       it('generally returns the voting balance at the appropriate checkpoint', async function () {
       it('generally returns the voting balance at the appropriate checkpoint', async function () {
@@ -452,33 +452,33 @@ contract('ERC20Votes', function (accounts) {
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
         await time.advanceBlock();
 
 
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
+        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
+        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
+        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
+        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
       });
       });
     });
     });
   });
   });
 
 
-  describe('getPriorTotalSupply', function () {
+  describe('getPastTotalSupply', function () {
     beforeEach(async function () {
     beforeEach(async function () {
       await this.token.delegate(holder, { from: holder });
       await this.token.delegate(holder, { from: holder });
     });
     });
 
 
     it('reverts if block number >= current block', async function () {
     it('reverts if block number >= current block', async function () {
       await expectRevert(
       await expectRevert(
-        this.token.getPriorTotalSupply(5e10),
+        this.token.getPastTotalSupply(5e10),
         'ERC20Votes: block not yet mined',
         'ERC20Votes: block not yet mined',
       );
       );
     });
     });
 
 
     it('returns 0 if there are no checkpoints', async function () {
     it('returns 0 if there are no checkpoints', async function () {
-      expect(await this.token.getPriorTotalSupply(0)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
     });
     });
 
 
     it('returns the latest block if >= last checkpoint block', async function () {
     it('returns the latest block if >= last checkpoint block', async function () {
@@ -487,8 +487,8 @@ contract('ERC20Votes', function (accounts) {
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
 
 
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply);
     });
     });
 
 
     it('returns zero if < first checkpoint block', async function () {
     it('returns zero if < first checkpoint block', async function () {
@@ -497,8 +497,8 @@ contract('ERC20Votes', function (accounts) {
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
 
 
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
     });
     });
 
 
     it('generally returns the voting balance at the appropriate checkpoint', async function () {
     it('generally returns the voting balance at the appropriate checkpoint', async function () {
@@ -515,15 +515,15 @@ contract('ERC20Votes', function (accounts) {
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
       await time.advanceBlock();
 
 
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
-      expect(await this.token.getPriorTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
-      expect(await this.token.getPriorTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
-      expect(await this.token.getPriorTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
-      expect(await this.token.getPriorTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
-      expect(await this.token.getPriorTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
-      expect(await this.token.getPriorTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
-      expect(await this.token.getPriorTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
+      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
+      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
+      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
+      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
     });
     });
   });
   });
 });
 });

+ 529 - 0
test/token/ERC20/extensions/ERC20VotesComp.test.js

@@ -0,0 +1,529 @@
+/* eslint-disable */
+
+const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants;
+
+const { fromRpcSig } = require('ethereumjs-util');
+const ethSigUtil = require('eth-sig-util');
+const Wallet = require('ethereumjs-wallet').default;
+
+const { promisify } = require('util');
+const queue = promisify(setImmediate);
+
+const ERC20VotesCompMock = artifacts.require('ERC20VotesCompMock');
+
+const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712');
+
+const Delegation = [
+  { name: 'delegatee', type: 'address' },
+  { name: 'nonce', type: 'uint256' },
+  { name: 'expiry', type: 'uint256' },
+];
+
+async function countPendingTransactions() {
+  return parseInt(
+    await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending'])
+  );
+}
+
+async function batchInBlock (txs) {
+  try {
+    // disable auto-mining
+    await network.provider.send('evm_setAutomine', [false]);
+    // send all transactions
+    const promises = txs.map(fn => fn());
+    // wait for node to have all pending transactions
+    while (txs.length > await countPendingTransactions()) {
+      await queue();
+    }
+    // mine one block
+    await network.provider.send('evm_mine');
+    // fetch receipts
+    const receipts = await Promise.all(promises);
+    // Sanity check, all tx should be in the same block
+    const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber));
+    expect(minedBlocks.size).to.equal(1);
+
+    return receipts;
+  } finally {
+    // enable auto-mining
+    await network.provider.send('evm_setAutomine', [true]);
+  }
+}
+
+contract('ERC20Votes', function (accounts) {
+  const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts;
+
+  const name = 'My Token';
+  const symbol = 'MTKN';
+  const version = '1';
+  const supply = new BN('10000000000000000000000000');
+
+  beforeEach(async function () {
+    this.token = await ERC20VotesCompMock.new(name, symbol);
+
+    // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id
+    // from within the EVM as from the JSON RPC interface.
+    // See https://github.com/trufflesuite/ganache-core/issues/515
+    this.chainId = await this.token.getChainId();
+  });
+
+  it('initial nonce is 0', async function () {
+    expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
+  });
+
+  it('domain separator', async function () {
+    expect(
+      await this.token.DOMAIN_SEPARATOR(),
+    ).to.equal(
+      await domainSeparator(name, version, this.chainId, this.token.address),
+    );
+  });
+
+  it('minting restriction', async function () {
+    const amount = new BN('2').pow(new BN('96'));
+    await expectRevert(
+      this.token.mint(holder, amount),
+      'ERC20Votes: total supply risks overflowing votes',
+    );
+  });
+
+  describe('set delegation', function () {
+    describe('call', function () {
+      it('delegation with balance', async function () {
+        await this.token.mint(holder, supply);
+        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+        const { receipt } = await this.token.delegate(holder, { from: holder });
+        expectEvent(receipt, 'DelegateChanged', {
+          delegator: holder,
+          fromDelegate: ZERO_ADDRESS,
+          toDelegate: holder,
+        });
+        expectEvent(receipt, 'DelegateVotesChanged', {
+          delegate: holder,
+          previousBalance: '0',
+          newBalance: supply,
+        });
+
+        expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+        expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        await time.advanceBlock();
+        expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply);
+      });
+
+      it('delegation without balance', async function () {
+        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+        const { receipt } = await this.token.delegate(holder, { from: holder });
+        expectEvent(receipt, 'DelegateChanged', {
+          delegator: holder,
+          fromDelegate: ZERO_ADDRESS,
+          toDelegate: holder,
+        });
+        expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
+
+        expect(await this.token.delegates(holder)).to.be.equal(holder);
+      });
+    });
+
+    describe('with signature', function () {
+      const delegator = Wallet.generate();
+      const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
+      const nonce = 0;
+
+      const buildData = (chainId, verifyingContract, message) => ({ data: {
+        primaryType: 'Delegation',
+        types: { EIP712Domain, Delegation },
+        domain: { name, version, chainId, verifyingContract },
+        message,
+      }});
+
+      beforeEach(async function () {
+        await this.token.mint(delegatorAddress, supply);
+      });
+
+      it('accept signed delegation', async function () {
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          delegator.getPrivateKey(),
+          buildData(this.chainId, this.token.address, {
+            delegatee: delegatorAddress,
+            nonce,
+            expiry: MAX_UINT256,
+          }),
+        ));
+
+        expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
+
+        const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+        expectEvent(receipt, 'DelegateChanged', {
+          delegator: delegatorAddress,
+          fromDelegate: ZERO_ADDRESS,
+          toDelegate: delegatorAddress,
+        });
+        expectEvent(receipt, 'DelegateVotesChanged', {
+          delegate: delegatorAddress,
+          previousBalance: '0',
+          newBalance: supply,
+        });
+
+        expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
+
+        expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
+        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        await time.advanceBlock();
+        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply);
+      });
+
+      it('rejects reused signature', async function () {
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          delegator.getPrivateKey(),
+          buildData(this.chainId, this.token.address, {
+            delegatee: delegatorAddress,
+            nonce,
+            expiry: MAX_UINT256,
+          }),
+        ));
+
+        await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+
+        await expectRevert(
+          this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
+          'ERC20Votes: invalid nonce',
+        );
+      });
+
+      it('rejects bad delegatee', async function () {
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          delegator.getPrivateKey(),
+          buildData(this.chainId, this.token.address, {
+            delegatee: delegatorAddress,
+            nonce,
+            expiry: MAX_UINT256,
+          }),
+        ));
+
+        const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
+        const { args } = logs.find(({ event }) => event == 'DelegateChanged');
+        expect(args.delegator).to.not.be.equal(delegatorAddress);
+        expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
+        expect(args.toDelegate).to.be.equal(holderDelegatee);
+      });
+
+      it('rejects bad nonce', async function () {
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          delegator.getPrivateKey(),
+          buildData(this.chainId, this.token.address, {
+            delegatee: delegatorAddress,
+            nonce,
+            expiry: MAX_UINT256,
+          }),
+        ));
+        await expectRevert(
+          this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
+          'ERC20Votes: invalid nonce',
+        );
+      });
+
+      it('rejects expired permit', async function () {
+        const expiry = (await time.latest()) - time.duration.weeks(1);
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          delegator.getPrivateKey(),
+          buildData(this.chainId, this.token.address, {
+            delegatee: delegatorAddress,
+            nonce,
+            expiry,
+          }),
+        ));
+
+        await expectRevert(
+          this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
+          'ERC20Votes: signature expired',
+        );
+      });
+    });
+  });
+
+  describe('change delegation', function () {
+    beforeEach(async function () {
+      await this.token.mint(holder, supply);
+      await this.token.delegate(holder, { from: holder });
+    });
+
+    it('call', async function () {
+      expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+      const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
+      expectEvent(receipt, 'DelegateChanged', {
+        delegator: holder,
+        fromDelegate: holder,
+        toDelegate: holderDelegatee,
+      });
+      expectEvent(receipt, 'DelegateVotesChanged', {
+        delegate: holder,
+        previousBalance: supply,
+        newBalance: '0',
+      });
+      expectEvent(receipt, 'DelegateVotesChanged', {
+        delegate: holderDelegatee,
+        previousBalance: '0',
+        newBalance: supply,
+      });
+
+      expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
+
+      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
+      expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      await time.advanceBlock();
+      expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0');
+      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply);
+    });
+  });
+
+  describe('transfers', function () {
+    beforeEach(async function () {
+      await this.token.mint(holder, supply);
+    });
+
+    it('no delegation', async function () {
+      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+      expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
+
+      this.holderVotes = '0';
+      this.recipientVotes = '0';
+    });
+
+    it('sender delegation', async function () {
+      await this.token.delegate(holder, { from: holder });
+
+      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+      expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: supply, newBalance: supply.subn(1) });
+
+      this.holderVotes = supply.subn(1);
+      this.recipientVotes = '0';
+    });
+
+    it('receiver delegation', async function () {
+      await this.token.delegate(recipient, { from: recipient });
+
+      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+
+      this.holderVotes = '0';
+      this.recipientVotes = '1';
+    });
+
+    it('full delegation', async function () {
+      await this.token.delegate(holder, { from: holder });
+      await this.token.delegate(recipient, { from: recipient });
+
+      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+      expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: supply, newBalance: supply.subn(1) });
+      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+
+      this.holderVotes = supply.subn(1);
+      this.recipientVotes = '1';
+    });
+
+    afterEach(async function () {
+      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
+      expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
+
+      // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
+      const blockNumber = await time.latestBlock();
+      await time.advanceBlock();
+      expect(await this.token.getPriorVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes);
+      expect(await this.token.getPriorVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes);
+    });
+  });
+
+  // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
+  describe('Compound test suite', function () {
+    beforeEach(async function () {
+      await this.token.mint(holder, supply);
+    });
+
+    describe('balanceOf', function () {
+      it('grants to initial account', async function () {
+        expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
+      });
+    });
+
+    describe('numCheckpoints', function () {
+      it('returns the number of checkpoints for a delegate', async function () {
+        await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+        const t1 = await this.token.delegate(other1, { from: recipient });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+
+        const t2 = await this.token.transfer(other2, 10, { from: recipient });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+
+        const t3 = await this.token.transfer(other2, 10, { from: recipient });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
+
+        const t4 = await this.token.transfer(recipient, 20, { from: holder });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
+
+        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]);
+        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]);
+        expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]);
+        expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]);
+
+        await time.advanceBlock();
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100');
+        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90');
+        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80');
+        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100');
+      });
+
+      it('does not add more than one checkpoint in a block', async function () {
+        await this.token.transfer(recipient, '100', { from: holder });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+        const [ t1, t2, t3 ] = await batchInBlock([
+          () => this.token.delegate(other1, { from: recipient, gas: 100000 }),
+          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+        ]);
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]);
+        // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
+        // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
+
+        const t4 = await this.token.transfer(recipient, 20, { from: holder });
+        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]);
+      });
+    });
+
+    describe('getPriorVotes', function () {
+      it('reverts if block number >= current block', async function () {
+        await expectRevert(
+          this.token.getPriorVotes(other1, 5e10),
+          'ERC20Votes: block not yet mined',
+        );
+      });
+
+      it('returns 0 if there are no checkpoints', async function () {
+        expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
+      });
+
+      it('returns the latest block if >= last checkpoint block', async function () {
+        const t1 = await this.token.delegate(other1, { from: holder });
+        await time.advanceBlock();
+        await time.advanceBlock();
+
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      });
+
+      it('returns zero if < first checkpoint block', async function () {
+        await time.advanceBlock();
+        const t1 = await this.token.delegate(other1, { from: holder });
+        await time.advanceBlock();
+        await time.advanceBlock();
+
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      });
+
+      it('generally returns the voting balance at the appropriate checkpoint', async function () {
+        const t1 = await this.token.delegate(other1, { from: holder });
+        await time.advanceBlock();
+        await time.advanceBlock();
+        const t2 = await this.token.transfer(other2, 10, { from: holder });
+        await time.advanceBlock();
+        await time.advanceBlock();
+        const t3 = await this.token.transfer(other2, 10, { from: holder });
+        await time.advanceBlock();
+        await time.advanceBlock();
+        const t4 = await this.token.transfer(holder, 20, { from: other2 });
+        await time.advanceBlock();
+        await time.advanceBlock();
+
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
+        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
+        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
+        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
+        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      });
+    });
+  });
+
+  describe('getPastTotalSupply', function () {
+    beforeEach(async function () {
+      await this.token.delegate(holder, { from: holder });
+    });
+
+    it('reverts if block number >= current block', async function () {
+      await expectRevert(
+        this.token.getPastTotalSupply(5e10),
+        'ERC20Votes: block not yet mined',
+      );
+    });
+
+    it('returns 0 if there are no checkpoints', async function () {
+      expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
+    });
+
+    it('returns the latest block if >= last checkpoint block', async function () {
+      t1 = await this.token.mint(holder, supply);
+
+      await time.advanceBlock();
+      await time.advanceBlock();
+
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply);
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply);
+    });
+
+    it('returns zero if < first checkpoint block', async function () {
+      await time.advanceBlock();
+      const t1 = await this.token.mint(holder, supply);
+      await time.advanceBlock();
+      await time.advanceBlock();
+
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+    });
+
+    it('generally returns the voting balance at the appropriate checkpoint', async function () {
+      const t1 = await this.token.mint(holder, supply);
+      await time.advanceBlock();
+      await time.advanceBlock();
+      const t2 = await this.token.burn(holder, 10);
+      await time.advanceBlock();
+      await time.advanceBlock();
+      const t3 = await this.token.burn(holder, 10);
+      await time.advanceBlock();
+      await time.advanceBlock();
+      const t4 = await this.token.mint(holder, 20);
+      await time.advanceBlock();
+      await time.advanceBlock();
+
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990');
+      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990');
+      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980');
+      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980');
+      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000');
+      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000');
+    });
+  });
+});

+ 1 - 1
test/utils/math/SafeCast.test.js

@@ -41,7 +41,7 @@ contract('SafeCast', async (accounts) => {
     });
     });
   }
   }
 
 
-  [8, 16, 32, 64, 128].forEach(bits => testToUint(bits));
+  [8, 16, 32, 64, 96, 128, 224].forEach(bits => testToUint(bits));
 
 
   describe('toUint256', () => {
   describe('toUint256', () => {
     const maxInt256 = new BN('2').pow(new BN(255)).subn(1);
     const maxInt256 = new BN('2').pow(new BN(255)).subn(1);