Remove TokenTimelock, PaymentSplitter, ERC20Snapshot, ERC20VotesComp, GovernorVotesComp (#4276)

This commit is contained in:
Hadrien Croubois
2023-05-27 00:30:00 +02:00
committed by GitHub
parent 4448c13c3c
commit 15c5c71795
22 changed files with 37 additions and 1828 deletions

View File

@ -1,5 +1,15 @@
# Changelog # Changelog
### Removals
The following contracts were removed:
- `ERC20Snapshot`
- `ERC20VotesComp`
- `GovernorVotesComp`
- `PaymentSplitter`
- `TokenTimelock` (removed in favor of `VestingWallet`)
### How to upgrade from 4.x ### How to upgrade from 4.x
#### ERC20, ERC721, and ERC1155 #### ERC20, ERC721, and ERC1155

View File

@ -1,214 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (finance/PaymentSplitter.sol)
pragma solidity ^0.8.0;
import "../token/ERC20/utils/SafeERC20.sol";
import "../utils/Address.sol";
import "../utils/Context.sol";
/**
* @title PaymentSplitter
* @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware
* that the Ether will be split in this way, since it is handled transparently by the contract.
*
* The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each
* account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim
* an amount proportional to the percentage of total shares they were assigned. The distribution of shares is set at the
* time of contract deployment and can't be updated thereafter.
*
* `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the
* accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release}
* function.
*
* NOTE: This contract assumes that ERC20 tokens will behave similarly to native tokens (Ether). Rebasing tokens, and
* tokens that apply fees during transfers, are likely to not be supported as expected. If in doubt, we encourage you
* to run tests before sending real value to this contract.
*/
contract PaymentSplitter is Context {
event PayeeAdded(address account, uint256 shares);
event PaymentReleased(address to, uint256 amount);
event ERC20PaymentReleased(IERC20 indexed token, address to, uint256 amount);
event PaymentReceived(address from, uint256 amount);
uint256 private _totalShares;
uint256 private _totalReleased;
mapping(address => uint256) private _shares;
mapping(address => uint256) private _released;
address[] private _payees;
mapping(IERC20 => uint256) private _erc20TotalReleased;
mapping(IERC20 => mapping(address => uint256)) private _erc20Released;
/**
* @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at
* the matching position in the `shares` array.
*
* All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no
* duplicates in `payees`.
*/
constructor(address[] memory payees, uint256[] memory shares_) payable {
require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch");
require(payees.length > 0, "PaymentSplitter: no payees");
for (uint256 i = 0; i < payees.length; i++) {
_addPayee(payees[i], shares_[i]);
}
}
/**
* @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully
* reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the
* reliability of the events, and not the actual splitting of Ether.
*
* To learn more about this see the Solidity documentation for
* https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback
* functions].
*/
receive() external payable virtual {
emit PaymentReceived(_msgSender(), msg.value);
}
/**
* @dev Getter for the total shares held by payees.
*/
function totalShares() public view returns (uint256) {
return _totalShares;
}
/**
* @dev Getter for the total amount of Ether already released.
*/
function totalReleased() public view returns (uint256) {
return _totalReleased;
}
/**
* @dev Getter for the total amount of `token` already released. `token` should be the address of an IERC20
* contract.
*/
function totalReleased(IERC20 token) public view returns (uint256) {
return _erc20TotalReleased[token];
}
/**
* @dev Getter for the amount of shares held by an account.
*/
function shares(address account) public view returns (uint256) {
return _shares[account];
}
/**
* @dev Getter for the amount of Ether already released to a payee.
*/
function released(address account) public view returns (uint256) {
return _released[account];
}
/**
* @dev Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an
* IERC20 contract.
*/
function released(IERC20 token, address account) public view returns (uint256) {
return _erc20Released[token][account];
}
/**
* @dev Getter for the address of the payee number `index`.
*/
function payee(uint256 index) public view returns (address) {
return _payees[index];
}
/**
* @dev Getter for the amount of payee's releasable Ether.
*/
function releasable(address account) public view returns (uint256) {
uint256 totalReceived = address(this).balance + totalReleased();
return _pendingPayment(account, totalReceived, released(account));
}
/**
* @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an
* IERC20 contract.
*/
function releasable(IERC20 token, address account) public view returns (uint256) {
uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token);
return _pendingPayment(account, totalReceived, released(token, account));
}
/**
* @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the
* total shares and their previous withdrawals.
*/
function release(address payable account) public virtual {
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
uint256 payment = releasable(account);
require(payment != 0, "PaymentSplitter: account is not due payment");
// _totalReleased is the sum of all values in _released.
// If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow.
_totalReleased += payment;
unchecked {
_released[account] += payment;
}
Address.sendValue(account, payment);
emit PaymentReleased(account, payment);
}
/**
* @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their
* percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20
* contract.
*/
function release(IERC20 token, address account) public virtual {
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
uint256 payment = releasable(token, account);
require(payment != 0, "PaymentSplitter: account is not due payment");
// _erc20TotalReleased[token] is the sum of all values in _erc20Released[token].
// If "_erc20TotalReleased[token] += payment" does not overflow, then "_erc20Released[token][account] += payment"
// cannot overflow.
_erc20TotalReleased[token] += payment;
unchecked {
_erc20Released[token][account] += payment;
}
SafeERC20.safeTransfer(token, account, payment);
emit ERC20PaymentReleased(token, account, payment);
}
/**
* @dev internal logic for computing the pending payment of an `account` given the token historical balances and
* already released amounts.
*/
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
/**
* @dev Add a new payee to the contract.
* @param account The address of the payee to add.
* @param shares_ The number of shares owned by the payee.
*/
function _addPayee(address account, uint256 shares_) private {
require(account != address(0), "PaymentSplitter: account is the zero address");
require(shares_ > 0, "PaymentSplitter: shares are 0");
require(_shares[account] == 0, "PaymentSplitter: account already has shares");
_payees.push(account);
_shares[account] = shares_;
_totalShares = _totalShares + shares_;
emit PayeeAdded(account, shares_);
}
}

View File

@ -5,16 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/
This directory includes primitives for financial systems: This directory includes primitives for financial systems:
- {PaymentSplitter} allows to split Ether and ERC20 payments among a group of accounts. The sender does not need to be
aware that the assets will be split in this way, since it is handled transparently by the contract. The split can be
in equal parts or in any other arbitrary proportion.
- {VestingWallet} handles the vesting of Ether and ERC20 tokens for a given beneficiary. Custody of multiple tokens can - {VestingWallet} handles the vesting of Ether and ERC20 tokens for a given beneficiary. Custody of multiple tokens can
be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting
schedule. schedule.
== Contracts == Contracts
{{PaymentSplitter}}
{{VestingWallet}} {{VestingWallet}}

View File

@ -15,6 +15,9 @@ import "../utils/Context.sol";
* Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning.
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly)
* be immediately releasable. * be immediately releasable.
*
* By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for
* a beneficiary until a specified time.
*/ */
contract VestingWallet is Context { contract VestingWallet is Context {
event EtherReleased(uint256 amount); event EtherReleased(uint256 amount);
@ -62,6 +65,13 @@ contract VestingWallet is Context {
return _duration; return _duration;
} }
/**
* @dev Getter for the end timestamp.
*/
function end() public view virtual returns (uint256) {
return start() + duration();
}
/** /**
* @dev Amount of eth already released * @dev Amount of eth already released
*/ */
@ -136,7 +146,7 @@ contract VestingWallet is Context {
function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) { function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) {
if (timestamp < start()) { if (timestamp < start()) {
return 0; return 0;
} else if (timestamp > start() + duration()) { } else if (timestamp > end()) {
return totalAllocation; return totalAllocation;
} else { } else {
return (totalAllocation * (timestamp - start())) / duration(); return (totalAllocation * (timestamp - start())) / duration();

View File

@ -22,8 +22,6 @@ Votes modules determine the source of voting power, and sometimes quorum number.
* {GovernorVotes}: Extracts voting weight from an {ERC20Votes}, or since v4.5 an {ERC721Votes} token. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes}, or since v4.5 an {ERC721Votes} token.
* {GovernorVotesComp}: Extracts voting weight from a COMP-like or {ERC20VotesComp} token.
* {GovernorVotesQuorumFraction}: Combines with `GovernorVotes` to set the quorum as a fraction of the total token supply. * {GovernorVotesQuorumFraction}: Combines with `GovernorVotes` to set the quorum as a fraction of the total token supply.
Counting modules determine valid voting options. Counting modules determine valid voting options.
@ -66,8 +64,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
{{GovernorVotesQuorumFraction}} {{GovernorVotesQuorumFraction}}
{{GovernorVotesComp}}
=== Extensions === Extensions
{{GovernorTimelockControl}} {{GovernorTimelockControl}}

View File

@ -1,55 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesComp.sol)
pragma solidity ^0.8.0;
import "../Governor.sol";
import "../../token/ERC20/extensions/ERC20VotesComp.sol";
/**
* @dev Extension of {Governor} for voting weight extraction from a Comp token.
*
* _Available since v4.3._
*/
abstract contract GovernorVotesComp is Governor {
ERC20VotesComp public immutable token;
constructor(ERC20VotesComp token_) {
token = token_;
}
/**
* @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token
* does not implement EIP-6372.
*/
function clock() public view virtual override returns (uint48) {
try token.clock() returns (uint48 timepoint) {
return timepoint;
} catch {
return SafeCast.toUint48(block.number);
}
}
/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() public view virtual override returns (string memory) {
try token.CLOCK_MODE() returns (string memory clockmode) {
return clockmode;
} catch {
return "mode=blocknumber&from=default";
}
}
/**
* Read the voting weight from the token's built-in snapshot mechanism (see {Governor-_getVotes}).
*/
function _getVotes(
address account,
uint256 timepoint,
bytes memory /*params*/
) internal view virtual override returns (uint256) {
return token.getPriorVotes(account, timepoint);
}
}

View File

@ -1,20 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../../governance/extensions/GovernorCountingSimple.sol";
import "../../governance/extensions/GovernorVotesComp.sol";
abstract contract GovernorCompMock is GovernorVotesComp, GovernorCountingSimple {
function quorum(uint256) public pure override returns (uint256) {
return 0;
}
function votingDelay() public pure override returns (uint256) {
return 4;
}
function votingPeriod() public pure override returns (uint256) {
return 16;
}
}

View File

@ -5,13 +5,13 @@ pragma solidity ^0.8.0;
import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; import "../../governance/compatibility/GovernorCompatibilityBravo.sol";
import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorTimelockCompound.sol";
import "../../governance/extensions/GovernorSettings.sol"; import "../../governance/extensions/GovernorSettings.sol";
import "../../governance/extensions/GovernorVotesComp.sol"; import "../../governance/extensions/GovernorVotes.sol";
abstract contract GovernorCompatibilityBravoMock is abstract contract GovernorCompatibilityBravoMock is
GovernorCompatibilityBravo, GovernorCompatibilityBravo,
GovernorSettings, GovernorSettings,
GovernorTimelockCompound, GovernorTimelockCompound,
GovernorVotesComp GovernorVotes
{ {
function quorum(uint256) public pure override returns (uint256) { function quorum(uint256) public pure override returns (uint256) {
return 0; return 0;

View File

@ -3,7 +3,6 @@
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import "../../token/ERC20/extensions/ERC20Votes.sol"; import "../../token/ERC20/extensions/ERC20Votes.sol";
import "../../token/ERC20/extensions/ERC20VotesComp.sol";
import "../../token/ERC721/extensions/ERC721Votes.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol";
abstract contract ERC20VotesTimestampMock is ERC20Votes { abstract contract ERC20VotesTimestampMock is ERC20Votes {
@ -17,17 +16,6 @@ abstract contract ERC20VotesTimestampMock is ERC20Votes {
} }
} }
abstract contract ERC20VotesCompTimestampMock is ERC20VotesComp {
function clock() public view virtual override returns (uint48) {
return SafeCast.toUint48(block.timestamp);
}
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() public view virtual override returns (string memory) {
return "mode=timestamp";
}
}
abstract contract ERC721VotesTimestampMock is ERC721Votes { abstract contract ERC721VotesTimestampMock is ERC721Votes {
function clock() public view virtual override returns (uint48) { function clock() public view virtual override returns (uint48) {
return SafeCast.toUint48(block.timestamp); return SafeCast.toUint48(block.timestamp);

View File

@ -18,18 +18,19 @@ Additionally there are multiple custom extensions, including:
* {ERC20Burnable}: destruction of own tokens. * {ERC20Burnable}: destruction of own tokens.
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
* {ERC20Pausable}: ability to pause token transfers. * {ERC20Pausable}: ability to pause token transfers.
* {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. * {ERC20Votes}: support for voting and vote delegation.
* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's token, with uint96 restrictions).
* {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. * {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
* {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20). * {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20).
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:
* {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values.
* {TokenTimelock}: hold tokens for a beneficiary until a specified time.
Other utilities that support ERC20 assets can be found in codebase:
* ERC20 tokens can be timelocked (held tokens for a beneficiary until a specified time) or vested (released following a given schedule) using a {VestingWallet}.
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. 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.
@ -51,12 +52,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
{{ERC20Permit}} {{ERC20Permit}}
{{ERC20Snapshot}}
{{ERC20Votes}} {{ERC20Votes}}
{{ERC20VotesComp}}
{{ERC20Wrapper}} {{ERC20Wrapper}}
{{ERC20FlashMint}} {{ERC20FlashMint}}
@ -66,5 +63,3 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
== Utilities == Utilities
{{SafeERC20}} {{SafeERC20}}
{{TokenTimelock}}

View File

@ -1,189 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Snapshot.sol)
pragma solidity ^0.8.0;
import "../ERC20.sol";
import "../../../utils/Arrays.sol";
import "../../../utils/Counters.sol";
/**
* @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and
* total supply at the time are recorded for later access.
*
* This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting.
* In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different
* accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be
* used to create an efficient ERC20 forking mechanism.
*
* Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a
* snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot
* id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id
* and the account address.
*
* NOTE: Snapshot policy can be customized by overriding the {_getCurrentSnapshotId} method. For example, having it
* return `block.number` will trigger the creation of snapshot at the beginning of each new block. When overriding this
* function, be careful about the monotonicity of its result. Non-monotonic snapshot ids will break the contract.
*
* Implementing snapshots for every block using this method will incur significant gas costs. For a gas-efficient
* alternative consider {ERC20Votes}.
*
* ==== Gas Costs
*
* Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log
* n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much
* smaller since identical balances in subsequent snapshots are stored as a single entry.
*
* There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is
* only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent
* transfers will have normal cost until the next snapshot, and so on.
*/
abstract contract ERC20Snapshot is ERC20 {
// Inspired by Jordi Baylina's MiniMeToken to record historical balances:
// https://github.com/Giveth/minime/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol
using Arrays for uint256[];
using Counters for Counters.Counter;
// Snapshotted values have arrays of ids and the value corresponding to that id. These could be an array of a
// Snapshot struct, but that would impede usage of functions that work on an array.
struct Snapshots {
uint256[] ids;
uint256[] values;
}
mapping(address => Snapshots) private _accountBalanceSnapshots;
Snapshots private _totalSupplySnapshots;
// Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid.
Counters.Counter private _currentSnapshotId;
/**
* @dev Emitted by {_snapshot} when a snapshot identified by `id` is created.
*/
event Snapshot(uint256 id);
/**
* @dev Creates a new snapshot and returns its snapshot id.
*
* Emits a {Snapshot} event that contains the same id.
*
* {_snapshot} is `internal` and you have to decide how to expose it externally. Its usage may be restricted to a
* set of accounts, for example using {AccessControl}, or it may be open to the public.
*
* [WARNING]
* ====
* While an open way of calling {_snapshot} is required for certain trust minimization mechanisms such as forking,
* you must consider that it can potentially be used by attackers in two ways.
*
* First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow
* logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target
* specific accounts and increase the cost of ERC20 transfers for them, in the ways specified in the Gas Costs
* section above.
*
* We haven't measured the actual numbers; if this is something you're interested in please reach out to us.
* ====
*/
function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment();
uint256 currentId = _getCurrentSnapshotId();
emit Snapshot(currentId);
return currentId;
}
/**
* @dev Get the current snapshotId
*/
function _getCurrentSnapshotId() internal view virtual returns (uint256) {
return _currentSnapshotId.current();
}
/**
* @dev Retrieves the balance of `account` at the time `snapshotId` was created.
*/
function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]);
return snapshotted ? value : balanceOf(account);
}
/**
* @dev Retrieves the total supply at the time `snapshotId` was created.
*/
function totalSupplyAt(uint256 snapshotId) public view virtual returns (uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnapshots);
return snapshotted ? value : totalSupply();
}
// Update balance and/or total supply snapshots before the values are modified. This is executed
// for _mint, _burn, and _transfer operations.
function _update(address from, address to, uint256 amount) internal virtual override {
if (from == address(0)) {
_updateTotalSupplySnapshot();
} else {
_updateAccountSnapshot(from);
}
if (to == address(0)) {
_updateTotalSupplySnapshot();
} else {
_updateAccountSnapshot(to);
}
super._update(from, to, amount);
}
function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) {
require(snapshotId > 0, "ERC20Snapshot: id is 0");
require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");
// When a valid snapshot is queried, there are three possibilities:
// a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never
// created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds
// to this id is the current one.
// b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the
// requested id, and its value is the one to return.
// c) More snapshots were created after the requested one, and the queried value was later modified. There will be
// no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is
// larger than the requested one.
//
// In summary, we need to find an element in an array, returning the index of the smallest value that is larger if
// it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does
// exactly this.
uint256 index = snapshots.ids.findUpperBound(snapshotId);
if (index == snapshots.ids.length) {
return (false, 0);
} else {
return (true, snapshots.values[index]);
}
}
function _updateAccountSnapshot(address account) private {
_updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account));
}
function _updateTotalSupplySnapshot() private {
_updateSnapshot(_totalSupplySnapshots, totalSupply());
}
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
uint256 currentId = _getCurrentSnapshotId();
if (_lastSnapshotId(snapshots.ids) < currentId) {
snapshots.ids.push(currentId);
snapshots.values.push(currentValue);
}
}
function _lastSnapshotId(uint256[] storage ids) private view returns (uint256) {
if (ids.length == 0) {
return 0;
} else {
return ids[ids.length - 1];
}
}
}

View File

@ -11,7 +11,7 @@ import "../../../utils/math/SafeCast.sol";
* @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, * @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. * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1.
* *
* NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module. * NOTE: This contract does not provide interface compatibility with Compound's COMP token.
* *
* This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either * 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

View File

@ -1,46 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20VotesComp.sol)
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 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
* 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.
*
* _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 virtual 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 virtual 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;
}
}

View File

@ -1,72 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/TokenTimelock.sol)
pragma solidity ^0.8.0;
import "./SafeERC20.sol";
/**
* @dev A token holder contract that will allow a beneficiary to extract the
* tokens after a given release time.
*
* Useful for simple vesting schedules like "advisors get all of their tokens
* after 1 year".
*/
contract TokenTimelock {
using SafeERC20 for IERC20;
// ERC20 basic token contract being held
IERC20 private immutable _token;
// beneficiary of tokens after they are released
address private immutable _beneficiary;
// timestamp when token release is enabled
uint256 private immutable _releaseTime;
/**
* @dev Deploys a timelock instance that is able to hold the token specified, and will only release it to
* `beneficiary_` when {release} is invoked after `releaseTime_`. The release time is specified as a Unix timestamp
* (in seconds).
*/
constructor(IERC20 token_, address beneficiary_, uint256 releaseTime_) {
require(releaseTime_ > block.timestamp, "TokenTimelock: release time is before current time");
_token = token_;
_beneficiary = beneficiary_;
_releaseTime = releaseTime_;
}
/**
* @dev Returns the token being held.
*/
function token() public view virtual returns (IERC20) {
return _token;
}
/**
* @dev Returns the beneficiary that will receive the tokens.
*/
function beneficiary() public view virtual returns (address) {
return _beneficiary;
}
/**
* @dev Returns the time when the tokens are released in seconds since Unix epoch (i.e. Unix timestamp).
*/
function releaseTime() public view virtual returns (uint256) {
return _releaseTime;
}
/**
* @dev Transfers tokens held by the timelock to the beneficiary. Will only succeed if invoked after the release
* time.
*/
function release() public virtual {
require(block.timestamp >= releaseTime(), "TokenTimelock: current time is before release time");
uint256 amount = token().balanceOf(address(this));
require(amount > 0, "TokenTimelock: no tokens to release");
token().safeTransfer(beneficiary(), amount);
}
}

View File

@ -101,13 +101,13 @@ index 9fc95518..53130e3c 100644
} }
``` ```
diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol
index fe67eb54..d26ea4e1 100644 index bb70d19f..38513771 100644
--- a/contracts/finance/VestingWallet.sol --- a/contracts/finance/VestingWallet.sol
+++ b/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol
@@ -15,6 +15,8 @@ import "../utils/Context.sol"; @@ -18,6 +18,8 @@ import "../utils/Context.sol";
* Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. *
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) * By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for
* be immediately releasable. * a beneficiary until a specified time.
+ * + *
+ * @custom:storage-size 52 + * @custom:storage-size 52
*/ */
@ -126,19 +126,6 @@ index 64431711..885f0e42 100644
*/ */
abstract contract GovernorVotes is Governor { abstract contract GovernorVotes is Governor {
IERC5805 public immutable token; IERC5805 public immutable token;
diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol
index 17250ad7..1d26b72e 100644
--- a/contracts/governance/extensions/GovernorVotesComp.sol
+++ b/contracts/governance/extensions/GovernorVotesComp.sol
@@ -10,6 +10,8 @@ import "../../token/ERC20/extensions/ERC20VotesComp.sol";
* @dev Extension of {Governor} for voting weight extraction from a Comp token.
*
* _Available since v4.3._
+ *
+ * @custom:storage-size 51
*/
abstract contract GovernorVotesComp is Governor {
ERC20VotesComp public immutable token;
diff --git a/contracts/package.json b/contracts/package.json diff --git a/contracts/package.json b/contracts/package.json
index 55e70b17..ceefb984 100644 index 55e70b17..ceefb984 100644
--- a/contracts/package.json --- a/contracts/package.json
@ -198,19 +185,6 @@ index bfe782e4..7264fe32 100644
*/ */
abstract contract ERC20Wrapper is ERC20 { abstract contract ERC20Wrapper is ERC20 {
IERC20 private immutable _underlying; IERC20 private immutable _underlying;
diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol
index ed855b7b..3d30f59d 100644
--- a/contracts/token/ERC20/utils/TokenTimelock.sol
+++ b/contracts/token/ERC20/utils/TokenTimelock.sol
@@ -11,6 +11,8 @@ import "./SafeERC20.sol";
*
* Useful for simple vesting schedules like "advisors get all of their tokens
* after 1 year".
+ *
+ * @custom:storage-size 53
*/
contract TokenTimelock {
using SafeERC20 for IERC20;
diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol
index 6a4e1cad..55d8eced 100644 index 6a4e1cad..55d8eced 100644
--- a/contracts/utils/cryptography/EIP712.sol --- a/contracts/utils/cryptography/EIP712.sol

View File

@ -1,217 +0,0 @@
const { balance, constants, ether, expectEvent, send, expectRevert } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const { expect } = require('chai');
const PaymentSplitter = artifacts.require('PaymentSplitter');
const ERC20 = artifacts.require('$ERC20');
contract('PaymentSplitter', function (accounts) {
const [owner, payee1, payee2, payee3, nonpayee1, payer1] = accounts;
const amount = ether('1');
it('rejects an empty set of payees', async function () {
await expectRevert(PaymentSplitter.new([], []), 'PaymentSplitter: no payees');
});
it('rejects more payees than shares', async function () {
await expectRevert(
PaymentSplitter.new([payee1, payee2, payee3], [20, 30]),
'PaymentSplitter: payees and shares length mismatch',
);
});
it('rejects more shares than payees', async function () {
await expectRevert(
PaymentSplitter.new([payee1, payee2], [20, 30, 40]),
'PaymentSplitter: payees and shares length mismatch',
);
});
it('rejects null payees', async function () {
await expectRevert(
PaymentSplitter.new([payee1, ZERO_ADDRESS], [20, 30]),
'PaymentSplitter: account is the zero address',
);
});
it('rejects zero-valued shares', async function () {
await expectRevert(PaymentSplitter.new([payee1, payee2], [20, 0]), 'PaymentSplitter: shares are 0');
});
it('rejects repeated payees', async function () {
await expectRevert(PaymentSplitter.new([payee1, payee1], [20, 30]), 'PaymentSplitter: account already has shares');
});
context('once deployed', function () {
beforeEach(async function () {
this.payees = [payee1, payee2, payee3];
this.shares = [20, 10, 70];
this.contract = await PaymentSplitter.new(this.payees, this.shares);
this.token = await ERC20.new('MyToken', 'MT');
await this.token.$_mint(owner, ether('1000'));
});
it('has total shares', async function () {
expect(await this.contract.totalShares()).to.be.bignumber.equal('100');
});
it('has payees', async function () {
await Promise.all(
this.payees.map(async (payee, index) => {
expect(await this.contract.payee(index)).to.equal(payee);
expect(await this.contract.released(payee)).to.be.bignumber.equal('0');
expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0');
}),
);
});
describe('accepts payments', function () {
it('Ether', async function () {
await send.ether(owner, this.contract.address, amount);
expect(await balance.current(this.contract.address)).to.be.bignumber.equal(amount);
});
it('Token', async function () {
await this.token.transfer(this.contract.address, amount, { from: owner });
expect(await this.token.balanceOf(this.contract.address)).to.be.bignumber.equal(amount);
});
});
describe('shares', function () {
it('stores shares if address is payee', async function () {
expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0');
});
it('does not store shares if address is not payee', async function () {
expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0');
});
});
describe('release', function () {
describe('Ether', function () {
it('reverts if no funds to claim', async function () {
await expectRevert(this.contract.release(payee1), 'PaymentSplitter: account is not due payment');
});
it('reverts if non-payee want to claim', async function () {
await send.ether(payer1, this.contract.address, amount);
await expectRevert(this.contract.release(nonpayee1), 'PaymentSplitter: account has no shares');
});
});
describe('Token', function () {
it('reverts if no funds to claim', async function () {
await expectRevert(
this.contract.release(this.token.address, payee1),
'PaymentSplitter: account is not due payment',
);
});
it('reverts if non-payee want to claim', async function () {
await this.token.transfer(this.contract.address, amount, { from: owner });
await expectRevert(
this.contract.release(this.token.address, nonpayee1),
'PaymentSplitter: account has no shares',
);
});
});
});
describe('tracks releasable and released', function () {
it('Ether', async function () {
await send.ether(payer1, this.contract.address, amount);
const payment = amount.divn(10);
expect(await this.contract.releasable(payee2)).to.be.bignumber.equal(payment);
await this.contract.release(payee2);
expect(await this.contract.releasable(payee2)).to.be.bignumber.equal('0');
expect(await this.contract.released(payee2)).to.be.bignumber.equal(payment);
});
it('Token', async function () {
await this.token.transfer(this.contract.address, amount, { from: owner });
const payment = amount.divn(10);
expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal(payment);
await this.contract.release(this.token.address, payee2);
expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal('0');
expect(await this.contract.released(this.token.address, payee2)).to.be.bignumber.equal(payment);
});
});
describe('distributes funds to payees', function () {
it('Ether', async function () {
await send.ether(payer1, this.contract.address, amount);
// receive funds
const initBalance = await balance.current(this.contract.address);
expect(initBalance).to.be.bignumber.equal(amount);
// distribute to payees
const tracker1 = await balance.tracker(payee1);
const receipt1 = await this.contract.release(payee1);
const profit1 = await tracker1.delta();
expect(profit1).to.be.bignumber.equal(ether('0.20'));
expectEvent(receipt1, 'PaymentReleased', { to: payee1, amount: profit1 });
const tracker2 = await balance.tracker(payee2);
const receipt2 = await this.contract.release(payee2);
const profit2 = await tracker2.delta();
expect(profit2).to.be.bignumber.equal(ether('0.10'));
expectEvent(receipt2, 'PaymentReleased', { to: payee2, amount: profit2 });
const tracker3 = await balance.tracker(payee3);
const receipt3 = await this.contract.release(payee3);
const profit3 = await tracker3.delta();
expect(profit3).to.be.bignumber.equal(ether('0.70'));
expectEvent(receipt3, 'PaymentReleased', { to: payee3, amount: profit3 });
// end balance should be zero
expect(await balance.current(this.contract.address)).to.be.bignumber.equal('0');
// check correct funds released accounting
expect(await this.contract.totalReleased()).to.be.bignumber.equal(initBalance);
});
it('Token', async function () {
expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal('0');
expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal('0');
expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal('0');
await this.token.transfer(this.contract.address, amount, { from: owner });
expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', {
token: this.token.address,
to: payee1,
amount: ether('0.20'),
});
await this.token.transfer(this.contract.address, amount, { from: owner });
expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', {
token: this.token.address,
to: payee1,
amount: ether('0.20'),
});
expectEvent(await this.contract.release(this.token.address, payee2), 'ERC20PaymentReleased', {
token: this.token.address,
to: payee2,
amount: ether('0.20'),
});
expectEvent(await this.contract.release(this.token.address, payee3), 'ERC20PaymentReleased', {
token: this.token.address,
to: payee3,
amount: ether('1.40'),
});
expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal(ether('0.40'));
expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal(ether('0.20'));
expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal(ether('1.40'));
});
});
});
});

View File

@ -30,6 +30,7 @@ contract('VestingWallet', function (accounts) {
expect(await this.mock.beneficiary()).to.be.equal(beneficiary); expect(await this.mock.beneficiary()).to.be.equal(beneficiary);
expect(await this.mock.start()).to.be.bignumber.equal(this.start); expect(await this.mock.start()).to.be.bignumber.equal(this.start);
expect(await this.mock.duration()).to.be.bignumber.equal(duration); expect(await this.mock.duration()).to.be.bignumber.equal(duration);
expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration));
}); });
describe('vesting schedule', function () { describe('vesting schedule', function () {

View File

@ -21,8 +21,8 @@ function makeContractAddress(creator, nonce) {
} }
const TOKENS = [ const TOKENS = [
{ Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
{ Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
]; ];
contract('GovernorCompatibilityBravo', function (accounts) { contract('GovernorCompatibilityBravo', function (accounts) {

View File

@ -1,89 +0,0 @@
const { expect } = require('chai');
const Enums = require('../../helpers/enums');
const { GovernorHelper } = require('../../helpers/governance');
const Governor = artifacts.require('$GovernorCompMock');
const CallReceiver = artifacts.require('CallReceiverMock');
const TOKENS = [
{ Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' },
{ Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' },
];
contract('GovernorComp', function (accounts) {
const [owner, voter1, voter2, voter3, voter4] = accounts;
const name = 'OZ-Governor';
const version = '1';
const tokenName = 'MockToken';
const tokenSymbol = 'MTKN';
const tokenSupply = web3.utils.toWei('100');
const votingDelay = web3.utils.toBN(4);
const votingPeriod = web3.utils.toBN(16);
const value = web3.utils.toWei('1');
for (const { mode, Token } of TOKENS) {
describe(`using ${Token._json.contractName}`, function () {
beforeEach(async function () {
this.owner = owner;
this.token = await Token.new(tokenName, tokenSymbol, tokenName, version);
this.mock = await Governor.new(name, this.token.address);
this.receiver = await CallReceiver.new();
this.helper = new GovernorHelper(this.mock, mode);
await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
await this.token.$_mint(owner, tokenSupply);
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
// default proposal
this.proposal = this.helper.setProposal(
[
{
target: this.receiver.address,
value,
data: this.receiver.contract.methods.mockFunction().encodeABI(),
},
],
'<proposal description>',
);
});
it('deployment check', async function () {
expect(await this.mock.name()).to.be.equal(name);
expect(await this.mock.token()).to.be.equal(this.token.address);
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
});
it('voting with comp token', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
await this.helper.waitForDeadline();
await this.helper.execute();
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
await this.mock.proposalVotes(this.proposal.id).then(results => {
expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
});
});
});
}
});

View File

@ -1,207 +0,0 @@
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const ERC20Snapshot = artifacts.require('$ERC20Snapshot');
const { expect } = require('chai');
contract('ERC20Snapshot', function (accounts) {
const [initialHolder, recipient, other] = accounts;
const initialSupply = new BN(100);
const name = 'My Token';
const symbol = 'MTKN';
beforeEach(async function () {
this.token = await ERC20Snapshot.new(name, symbol);
await this.token.$_mint(initialHolder, initialSupply);
});
describe('snapshot', function () {
it('emits a snapshot event', async function () {
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot');
});
it('creates increasing snapshots ids, starting from 1', async function () {
for (const id of ['1', '2', '3', '4', '5']) {
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id });
}
});
});
describe('totalSupplyAt', function () {
it('reverts with a snapshot id of 0', async function () {
await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0');
});
it('reverts with a not-yet-created snapshot id', async function () {
await expectRevert(this.token.totalSupplyAt(1), 'ERC20Snapshot: nonexistent id');
});
context('with initial snapshot', function () {
beforeEach(async function () {
this.initialSnapshotId = new BN('1');
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
});
context('with no supply changes after the snapshot', function () {
it('returns the current total supply', async function () {
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
});
});
context('with supply changes after the snapshot', function () {
beforeEach(async function () {
await this.token.$_mint(other, new BN('50'));
await this.token.$_burn(initialHolder, new BN('20'));
});
it('returns the total supply before the changes', async function () {
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
});
context('with a second snapshot after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotId = new BN('2');
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
});
it('snapshots return the supply before and after the changes', async function () {
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
expect(await this.token.totalSupplyAt(this.secondSnapshotId)).to.be.bignumber.equal(
await this.token.totalSupply(),
);
});
});
context('with multiple snapshots after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotIds = ['2', '3', '4'];
for (const id of this.secondSnapshotIds) {
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id });
}
});
it('all posterior snapshots return the supply after the changes', async function () {
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
const currentSupply = await this.token.totalSupply();
for (const id of this.secondSnapshotIds) {
expect(await this.token.totalSupplyAt(id)).to.be.bignumber.equal(currentSupply);
}
});
});
});
});
});
describe('balanceOfAt', function () {
it('reverts with a snapshot id of 0', async function () {
await expectRevert(this.token.balanceOfAt(other, 0), 'ERC20Snapshot: id is 0');
});
it('reverts with a not-yet-created snapshot id', async function () {
await expectRevert(this.token.balanceOfAt(other, 1), 'ERC20Snapshot: nonexistent id');
});
context('with initial snapshot', function () {
beforeEach(async function () {
this.initialSnapshotId = new BN('1');
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
});
context('with no balance changes after the snapshot', function () {
it('returns the current balance for all accounts', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
initialSupply,
);
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
});
});
context('with balance changes after the snapshot', function () {
beforeEach(async function () {
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
await this.token.$_mint(other, new BN('50'));
await this.token.$_burn(initialHolder, new BN('20'));
});
it('returns the balances before the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
initialSupply,
);
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
});
context('with a second snapshot after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotId = new BN('2');
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
});
it('snapshots return the balances before and after the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
initialSupply,
);
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).to.be.bignumber.equal(
await this.token.balanceOf(initialHolder),
);
expect(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).to.be.bignumber.equal(
await this.token.balanceOf(recipient),
);
expect(await this.token.balanceOfAt(other, this.secondSnapshotId)).to.be.bignumber.equal(
await this.token.balanceOf(other),
);
});
});
context('with multiple snapshots after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotIds = ['2', '3', '4'];
for (const id of this.secondSnapshotIds) {
const receipt = await this.token.$_snapshot();
expectEvent(receipt, 'Snapshot', { id });
}
});
it('all posterior snapshots return the supply after the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
initialSupply,
);
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
for (const id of this.secondSnapshotIds) {
expect(await this.token.balanceOfAt(initialHolder, id)).to.be.bignumber.equal(
await this.token.balanceOf(initialHolder),
);
expect(await this.token.balanceOfAt(recipient, id)).to.be.bignumber.equal(
await this.token.balanceOf(recipient),
);
expect(await this.token.balanceOfAt(other, id)).to.be.bignumber.equal(await this.token.balanceOf(other));
}
});
});
});
});
});
});

View File

@ -1,579 +0,0 @@
/* eslint-disable */
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256, ZERO_ADDRESS } = constants;
const { batchInBlock } = require('../../../helpers/txpool');
const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior');
const { fromRpcSig } = require('ethereumjs-util');
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
const { clock, clockFromReceipt } = require('../../../helpers/time');
const Delegation = [
{ name: 'delegatee', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'expiry', type: 'uint256' },
];
const MODES = {
blocknumber: artifacts.require('$ERC20VotesComp'),
// no timestamp mode for ERC20VotesComp yet
};
contract('ERC20VotesComp', function (accounts) {
const [holder, recipient, holderDelegatee, other1, other2] = accounts;
const name = 'My Token';
const symbol = 'MTKN';
const version = '1';
const supply = new BN('10000000000000000000000000');
for (const [mode, artifact] of Object.entries(MODES)) {
describe(`vote with ${mode}`, function () {
beforeEach(async function () {
this.token = await artifact.new(name, symbol, name, version);
this.votes = this.token;
});
// includes EIP6372 behavior check
shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true });
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 getDomain(this.token).then(domainSeparator));
});
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');
});
it('recent checkpoints', async function () {
await this.token.delegate(holder, { from: holder });
for (let i = 0; i < 6; i++) {
await this.token.$_mint(holder, 1);
}
const timepoint = await clock[mode]();
expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6');
// recent
expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('5');
// non-recent
expect(await this.token.getPastVotes(holder, timepoint - 6)).to.be.bignumber.equal('0');
});
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 });
const timepoint = await clockFromReceipt[mode](receipt);
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, timepoint - 1)).to.be.bignumber.equal('0');
await time.advanceBlock();
expect(await this.token.getPriorVotes(holder, timepoint)).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 = (contract, message) =>
getDomain(contract).then(domain => ({
primaryType: 'Delegation',
types: { EIP712Domain: domainType(domain), Delegation },
domain,
message,
}));
beforeEach(async function () {
await this.token.$_mint(delegatorAddress, supply);
});
it('accept signed delegation', async function () {
const { v, r, s } = await buildData(this.token, {
delegatee: delegatorAddress,
nonce,
expiry: MAX_UINT256,
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
const timepoint = await clockFromReceipt[mode](receipt);
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, timepoint - 1)).to.be.bignumber.equal('0');
await time.advanceBlock();
expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
});
it('rejects reused signature', async function () {
const { v, r, s } = await buildData(this.token, {
delegatee: delegatorAddress,
nonce,
expiry: MAX_UINT256,
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
await expectRevert(
this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
'Votes: invalid nonce',
);
});
it('rejects bad delegatee', async function () {
const { v, r, s } = await buildData(this.token, {
delegatee: delegatorAddress,
nonce,
expiry: MAX_UINT256,
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
const { args } = receipt.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 } = await buildData(this.token, {
delegatee: delegatorAddress,
nonce,
expiry: MAX_UINT256,
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
await expectRevert(
this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
'Votes: invalid nonce',
);
});
it('rejects expired permit', async function () {
const expiry = (await time.latest()) - time.duration.weeks(1);
const { v, r, s } = await buildData(this.token, {
delegatee: delegatorAddress,
nonce,
expiry,
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
await expectRevert(
this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
'Votes: 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 });
const timepoint = await clockFromReceipt[mode](receipt);
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, timepoint - 1)).to.be.bignumber.equal(supply);
expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
await time.advanceBlock();
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0');
expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).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),
});
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
expect(
receipt.logs
.filter(({ event }) => event == 'DelegateVotesChanged')
.every(({ logIndex }) => transferLogIndex < logIndex),
).to.be.equal(true);
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' });
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
expect(
receipt.logs
.filter(({ event }) => event == 'DelegateVotesChanged')
.every(({ logIndex }) => transferLogIndex < logIndex),
).to.be.equal(true);
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' });
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
expect(
receipt.logs
.filter(({ event }) => event == 'DelegateVotesChanged')
.every(({ logIndex }) => transferLogIndex < logIndex),
).to.be.equal(true);
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 timepoint = await clock[mode]();
await time.advanceBlock();
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
expect(await this.token.getPriorVotes(recipient, timepoint)).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 });
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
const t2 = await this.token.transfer(other2, 10, { from: recipient });
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
const t3 = await this.token.transfer(other2, 10, { from: recipient });
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
const t4 = await this.token.transfer(recipient, 20, { from: holder });
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
await time.advanceBlock();
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
expect(await this.token.getPriorVotes(other1, t4.timepoint)).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: 200000 }),
() => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }),
() => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }),
]);
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
const t4 = await this.token.transfer(recipient, 20, { from: holder });
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
});
});
describe('getPriorVotes', function () {
it('reverts if block number >= current block', async function () {
await expectRevert(this.token.getPriorVotes(other1, 5e10), 'Votes: future lookup');
});
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 { receipt } = await this.token.delegate(other1, { from: holder });
const timepoint = await clockFromReceipt[mode](receipt);
await time.advanceBlock();
await time.advanceBlock();
expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal(
'10000000000000000000000000',
);
expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
'10000000000000000000000000',
);
});
it('returns zero if < first checkpoint block', async function () {
await time.advanceBlock();
const { receipt } = await this.token.delegate(other1, { from: holder });
const timepoint = await clockFromReceipt[mode](receipt);
await time.advanceBlock();
await time.advanceBlock();
expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
expect(await this.token.getPriorVotes(other1, timepoint + 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();
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal(
'10000000000000000000000000',
);
expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
'10000000000000000000000000',
);
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal(
'9999999999999999999999990',
);
expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
'9999999999999999999999990',
);
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal(
'9999999999999999999999980',
);
expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
'9999999999999999999999980',
);
expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal(
'10000000000000000000000000',
);
expect(await this.token.getPriorVotes(other1, t4.timepoint + 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), 'Votes: future lookup');
});
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 () {
const { receipt } = await this.token.$_mint(holder, supply);
const timepoint = await clockFromReceipt[mode](receipt);
await time.advanceBlock();
await time.advanceBlock();
expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
});
it('returns zero if < first checkpoint block', async function () {
await time.advanceBlock();
const { receipt } = await this.token.$_mint(holder, supply);
const timepoint = await clockFromReceipt[mode](receipt);
await time.advanceBlock();
await time.advanceBlock();
expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
expect(await this.token.getPastTotalSupply(timepoint + 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();
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
'10000000000000000000000000',
);
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
'9999999999999999999999990',
);
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
'9999999999999999999999980',
);
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
'10000000000000000000000000',
);
});
});
});
}
});

View File

@ -1,71 +0,0 @@
const { BN, expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const ERC20 = artifacts.require('$ERC20');
const TokenTimelock = artifacts.require('TokenTimelock');
contract('TokenTimelock', function (accounts) {
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 ERC20.new(name, symbol);
});
it('rejects a release time in the past', async function () {
const pastReleaseTime = (await time.latest()).sub(time.duration.years(1));
await expectRevert(
TokenTimelock.new(this.token.address, beneficiary, pastReleaseTime),
'TokenTimelock: release time is before current time',
);
});
context('once deployed', function () {
beforeEach(async function () {
this.releaseTime = (await time.latest()).add(time.duration.years(1));
this.timelock = await TokenTimelock.new(this.token.address, beneficiary, this.releaseTime);
await this.token.$_mint(this.timelock.address, amount);
});
it('can get state', async function () {
expect(await this.timelock.token()).to.equal(this.token.address);
expect(await this.timelock.beneficiary()).to.equal(beneficiary);
expect(await this.timelock.releaseTime()).to.be.bignumber.equal(this.releaseTime);
});
it('cannot be released before time limit', async function () {
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
});
it('cannot be released just before time limit', async function () {
await time.increaseTo(this.releaseTime.sub(time.duration.seconds(3)));
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
});
it('can be released just after limit', async function () {
await time.increaseTo(this.releaseTime.add(time.duration.seconds(1)));
await this.timelock.release();
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
});
it('can be released after time limit', async function () {
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
await this.timelock.release();
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
});
it('cannot be released twice', async function () {
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
await this.timelock.release();
await expectRevert(this.timelock.release(), 'TokenTimelock: no tokens to release');
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
});
});
});
});