From 9a47209b7610f80d47c5ca3c8bd7664c1ed55d5b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 24 May 2023 11:21:53 +0200 Subject: [PATCH 01/10] Remove TokenTimelock in favor of VestingWallet --- .changeset/stupid-sheep-enjoy.md | 5 ++ contracts/finance/VestingWallet.sol | 12 +++- contracts/token/ERC20/README.adoc | 9 +-- contracts/token/ERC20/utils/TokenTimelock.sol | 72 ------------------- test/finance/VestingWallet.test.js | 1 + test/token/ERC20/utils/TokenTimelock.test.js | 71 ------------------ 6 files changed, 22 insertions(+), 148 deletions(-) create mode 100644 .changeset/stupid-sheep-enjoy.md delete mode 100644 contracts/token/ERC20/utils/TokenTimelock.sol delete mode 100644 test/token/ERC20/utils/TokenTimelock.test.js diff --git a/.changeset/stupid-sheep-enjoy.md b/.changeset/stupid-sheep-enjoy.md new file mode 100644 index 00000000000..65d7eb4e359 --- /dev/null +++ b/.changeset/stupid-sheep-enjoy.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`TokenTimelock`: removed in favor of `VestingWallet`. diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index fe67eb54ff6..4e080f2155a 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -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. * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) * 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 { event EtherReleased(uint256 amount); @@ -62,6 +65,13 @@ contract VestingWallet is Context { return _duration; } + /** + * @dev Getter for the end timestamp. + */ + function end() public view virtual returns (uint256) { + return _start + _duration; + } + /** * @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) { if (timestamp < start()) { return 0; - } else if (timestamp > start() + duration()) { + } else if (timestamp > end()) { return totalAllocation; } else { return (totalAllocation * (timestamp - start())) / duration(); diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 326aae68454..ccb5f6c020e 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -26,10 +26,13 @@ Additionally there are multiple custom extensions, including: * {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). -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. -* {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 <>) and expose them as external functions in the way they prefer. @@ -66,5 +69,3 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Utilities {{SafeERC20}} - -{{TokenTimelock}} diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol deleted file mode 100644 index ed855b7bcb4..00000000000 --- a/contracts/token/ERC20/utils/TokenTimelock.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.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); - } -} diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index a110b2cd968..09205a99cd4 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -30,6 +30,7 @@ contract('VestingWallet', function (accounts) { expect(await this.mock.beneficiary()).to.be.equal(beneficiary); 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.end()).to.be.bignumber.equal(this.start.add(duration)); }); describe('vesting schedule', function () { diff --git a/test/token/ERC20/utils/TokenTimelock.test.js b/test/token/ERC20/utils/TokenTimelock.test.js deleted file mode 100644 index 22e8071eb7e..00000000000 --- a/test/token/ERC20/utils/TokenTimelock.test.js +++ /dev/null @@ -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); - }); - }); - }); -}); From 642044441218b97910359481398939b069641ea4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 24 May 2023 18:17:26 +0200 Subject: [PATCH 02/10] Remove ERC20Snapshot --- .changeset/ninety-ghosts-check.md | 5 + contracts/token/ERC20/README.adoc | 3 - .../token/ERC20/extensions/ERC20Snapshot.sol | 189 ---------------- .../ERC20/extensions/ERC20Snapshot.test.js | 207 ------------------ 4 files changed, 5 insertions(+), 399 deletions(-) create mode 100644 .changeset/ninety-ghosts-check.md delete mode 100644 contracts/token/ERC20/extensions/ERC20Snapshot.sol delete mode 100644 test/token/ERC20/extensions/ERC20Snapshot.test.js diff --git a/.changeset/ninety-ghosts-check.md b/.changeset/ninety-ghosts-check.md new file mode 100644 index 00000000000..dde21e497bc --- /dev/null +++ b/.changeset/ninety-ghosts-check.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC20Snapshot`: removed. diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index ccb5f6c020e..8e7216a8dc1 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -18,7 +18,6 @@ Additionally there are multiple custom extensions, including: * {ERC20Burnable}: destruction of own tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {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). * {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. @@ -54,8 +53,6 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC20Permit}} -{{ERC20Snapshot}} - {{ERC20Votes}} {{ERC20VotesComp}} diff --git a/contracts/token/ERC20/extensions/ERC20Snapshot.sol b/contracts/token/ERC20/extensions/ERC20Snapshot.sol deleted file mode 100644 index 4eae931bcfd..00000000000 --- a/contracts/token/ERC20/extensions/ERC20Snapshot.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.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]; - } - } -} diff --git a/test/token/ERC20/extensions/ERC20Snapshot.test.js b/test/token/ERC20/extensions/ERC20Snapshot.test.js deleted file mode 100644 index fb0bb31d34d..00000000000 --- a/test/token/ERC20/extensions/ERC20Snapshot.test.js +++ /dev/null @@ -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)); - } - }); - }); - }); - }); - }); -}); From 95a524d5f1c2526b1093c833f97e8a3457df70a6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 24 May 2023 18:20:28 +0200 Subject: [PATCH 03/10] remove PaymentSplitter --- .changeset/clever-eagles-unite.md | 5 + contracts/finance/PaymentSplitter.sol | 214 ------------------------- contracts/finance/README.adoc | 6 - test/finance/PaymentSplitter.test.js | 217 -------------------------- 4 files changed, 5 insertions(+), 437 deletions(-) create mode 100644 .changeset/clever-eagles-unite.md delete mode 100644 contracts/finance/PaymentSplitter.sol delete mode 100644 test/finance/PaymentSplitter.test.js diff --git a/.changeset/clever-eagles-unite.md b/.changeset/clever-eagles-unite.md new file mode 100644 index 00000000000..f85c62a1004 --- /dev/null +++ b/.changeset/clever-eagles-unite.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`PaymentSplitter`: removed. diff --git a/contracts/finance/PaymentSplitter.sol b/contracts/finance/PaymentSplitter.sol deleted file mode 100644 index daa9090eba9..00000000000 --- a/contracts/finance/PaymentSplitter.sol +++ /dev/null @@ -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_); - } -} diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index b64af312556..ac7e4f015fa 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -5,16 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ 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 be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule. == Contracts -{{PaymentSplitter}} - {{VestingWallet}} diff --git a/test/finance/PaymentSplitter.test.js b/test/finance/PaymentSplitter.test.js deleted file mode 100644 index 1408c9f51a5..00000000000 --- a/test/finance/PaymentSplitter.test.js +++ /dev/null @@ -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')); - }); - }); - }); -}); From 7fdc428221cbb8dd54ac9968f944db48fcc6df9e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 May 2023 10:16:28 +0200 Subject: [PATCH 04/10] update end() to reflect start() and duration() overrides --- contracts/finance/VestingWallet.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 4e080f2155a..857a6a3c63c 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -69,7 +69,7 @@ contract VestingWallet is Context { * @dev Getter for the end timestamp. */ function end() public view virtual returns (uint256) { - return _start + _duration; + return start() + end(); } /** From 1abd2a23d97d5a94638382cd502d2d552e4b8c42 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 May 2023 10:32:14 +0200 Subject: [PATCH 05/10] Add a removals section in the Changelog --- .changeset/clever-eagles-unite.md | 5 ----- .changeset/ninety-ghosts-check.md | 5 ----- .changeset/stupid-sheep-enjoy.md | 5 ----- CHANGELOG.md | 8 ++++++++ 4 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 .changeset/clever-eagles-unite.md delete mode 100644 .changeset/ninety-ghosts-check.md delete mode 100644 .changeset/stupid-sheep-enjoy.md diff --git a/.changeset/clever-eagles-unite.md b/.changeset/clever-eagles-unite.md deleted file mode 100644 index f85c62a1004..00000000000 --- a/.changeset/clever-eagles-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`PaymentSplitter`: removed. diff --git a/.changeset/ninety-ghosts-check.md b/.changeset/ninety-ghosts-check.md deleted file mode 100644 index dde21e497bc..00000000000 --- a/.changeset/ninety-ghosts-check.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`ERC20Snapshot`: removed. diff --git a/.changeset/stupid-sheep-enjoy.md b/.changeset/stupid-sheep-enjoy.md deleted file mode 100644 index 65d7eb4e359..00000000000 --- a/.changeset/stupid-sheep-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`TokenTimelock`: removed in favor of `VestingWallet`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 534c4e876b2..2e02ec31b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### Removals + +The following contracts were removed: + +- `ERC20Snapshot` +- `PaymentSplitter` +- `TokenTimelock` (removed in favor of `VestinWallet`) + ### How to upgrade from 4.x #### ERC20, ERC721, and ERC1155 From d1bd4459cd746f8326ad7b9c9a4b946eaaa73b74 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 May 2023 10:35:55 +0200 Subject: [PATCH 06/10] FIx typo --- contracts/finance/VestingWallet.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 2da9dc2377e..bb70d19fb31 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -69,7 +69,7 @@ contract VestingWallet is Context { * @dev Getter for the end timestamp. */ function end() public view virtual returns (uint256) { - return start() + end(); + return start() + duration(); } /** From baaf484b16804653770147cf35673d2a4ad3251c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 May 2023 10:47:02 +0200 Subject: [PATCH 07/10] remove GovernorVotesComp and GovernorVotesComp --- CHANGELOG.md | 2 + contracts/governance/README.adoc | 4 - .../extensions/GovernorVotesComp.sol | 55 -- .../mocks/governance/GovernorCompMock.sol | 20 - .../GovernorCompatibilityBravoMock.sol | 4 +- contracts/mocks/token/VotesTimestamp.sol | 12 - contracts/token/ERC20/README.adoc | 3 - .../token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC20/extensions/ERC20VotesComp.sol | 46 -- .../GovernorCompatibilityBravo.test.js | 4 +- .../extensions/GovernorComp.test.js | 89 --- .../ERC20/extensions/ERC20VotesComp.test.js | 579 ------------------ 12 files changed, 7 insertions(+), 813 deletions(-) delete mode 100644 contracts/governance/extensions/GovernorVotesComp.sol delete mode 100644 contracts/mocks/governance/GovernorCompMock.sol delete mode 100644 contracts/token/ERC20/extensions/ERC20VotesComp.sol delete mode 100644 test/governance/extensions/GovernorComp.test.js delete mode 100644 test/token/ERC20/extensions/ERC20VotesComp.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e02ec31b17..c2a9fde89e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The following contracts were removed: - `ERC20Snapshot` +- `ERC20VotesComp` +- `GovernorVotesComp` - `PaymentSplitter` - `TokenTimelock` (removed in favor of `VestinWallet`) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 171283662dc..00edfe23d1b 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -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. -* {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. Counting modules determine valid voting options. @@ -66,8 +64,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} -{{GovernorVotesComp}} - === Extensions {{GovernorTimelockControl}} diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol deleted file mode 100644 index e7d7c2c7c8f..00000000000 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ /dev/null @@ -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); - } -} diff --git a/contracts/mocks/governance/GovernorCompMock.sol b/contracts/mocks/governance/GovernorCompMock.sol deleted file mode 100644 index cc368c42fb9..00000000000 --- a/contracts/mocks/governance/GovernorCompMock.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 1b87d143390..4cdc0b8884f 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -5,13 +5,13 @@ pragma solidity ^0.8.0; import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorSettings.sol"; -import "../../governance/extensions/GovernorVotesComp.sol"; +import "../../governance/extensions/GovernorVotes.sol"; abstract contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorSettings, GovernorTimelockCompound, - GovernorVotesComp + GovernorVotes { function quorum(uint256) public pure override returns (uint256) { return 0; diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/VotesTimestamp.sol index 179c500f4e7..630e778f1ab 100644 --- a/contracts/mocks/token/VotesTimestamp.sol +++ b/contracts/mocks/token/VotesTimestamp.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "../../token/ERC20/extensions/ERC20Votes.sol"; -import "../../token/ERC20/extensions/ERC20VotesComp.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol"; 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 { function clock() public view virtual override returns (uint48) { return SafeCast.toUint48(block.timestamp); diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 8e7216a8dc1..9482b581b4b 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -21,7 +21,6 @@ Additionally there are multiple custom extensions, including: * {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). * {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}. * {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20). @@ -55,8 +54,6 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC20Votes}} -{{ERC20VotesComp}} - {{ERC20Wrapper}} {{ERC20FlashMint}} diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 24fc7c2e55d..e7cda4f3501 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -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, * 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 * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting diff --git a/contracts/token/ERC20/extensions/ERC20VotesComp.sol b/contracts/token/ERC20/extensions/ERC20VotesComp.sol deleted file mode 100644 index 0461310a444..00000000000 --- a/contracts/token/ERC20/extensions/ERC20VotesComp.sol +++ /dev/null @@ -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; - } -} diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index d6d9f6a3c88..9c45277d18f 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -21,8 +21,8 @@ function makeContractAddress(creator, nonce) { } const TOKENS = [ - { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, ]; contract('GovernorCompatibilityBravo', function (accounts) { diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js deleted file mode 100644 index 92a59d05f27..00000000000 --- a/test/governance/extensions/GovernorComp.test.js +++ /dev/null @@ -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(), - }, - ], - '', - ); - }); - - 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')); - }); - }); - }); - } -}); diff --git a/test/token/ERC20/extensions/ERC20VotesComp.test.js b/test/token/ERC20/extensions/ERC20VotesComp.test.js deleted file mode 100644 index 815f22de04a..00000000000 --- a/test/token/ERC20/extensions/ERC20VotesComp.test.js +++ /dev/null @@ -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', - ); - }); - }); - }); - } -}); From 76e4284276b69c7d08a363fc9ac95061c1a659b6 Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 26 May 2023 17:53:43 -0300 Subject: [PATCH 08/10] remove conflict markers --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 117077b5ee6..6fde4f136f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,10 +42,6 @@ function supportsInterface(bytes4 interfaceId) public view virtual override retu return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); } ``` -<<<<<<< HEAD - -======= ->>>>>>> next-v5.0 ## 4.9.0 (2023-05-23) From 1d7daaa905ce15b1a5d21e8f495cfbcdaf5e0795 Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 26 May 2023 17:55:09 -0300 Subject: [PATCH 09/10] fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fde4f136f0..8d2853cf887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The following contracts were removed: - `ERC20VotesComp` - `GovernorVotesComp` - `PaymentSplitter` -- `TokenTimelock` (removed in favor of `VestinWallet`) +- `TokenTimelock` (removed in favor of `VestingWallet`) ### How to upgrade from 4.x From 9e5047b623fc3a1a3b294f9f43e8dc5a276deaa1 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 26 May 2023 18:04:34 -0300 Subject: [PATCH 10/10] update patch --- scripts/upgradeable/upgradeable.patch | 36 ++++----------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index bc675ab8a18..efc89cab527 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -101,13 +101,13 @@ index 9fc95518..53130e3c 100644 } ``` 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 +++ b/contracts/finance/VestingWallet.sol -@@ -15,6 +15,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) - * be immediately releasable. +@@ -18,6 +18,8 @@ import "../utils/Context.sol"; + * + * 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. + * + * @custom:storage-size 52 */ @@ -126,19 +126,6 @@ index 64431711..885f0e42 100644 */ abstract contract GovernorVotes is Governor { 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 index 55e70b17..ceefb984 100644 --- a/contracts/package.json @@ -198,19 +185,6 @@ index bfe782e4..7264fe32 100644 */ abstract contract ERC20Wrapper is ERC20 { 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 index 6a4e1cad..55d8eced 100644 --- a/contracts/utils/cryptography/EIP712.sol