Remove TokenTimelock, PaymentSplitter, ERC20Snapshot, ERC20VotesComp, GovernorVotesComp (#4276)
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@ -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
|
||||||
|
|||||||
@ -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_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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}}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}}
|
|
||||||
|
|||||||
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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 () {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user